matrice-streaming 0.1.58__tar.gz → 0.1.59__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/PKG-INFO +1 -1
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/matrice_streaming.egg-info/PKG-INFO +1 -1
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/async_ffmpeg_worker.py +105 -3
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_camera_streamer.py +386 -34
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/LICENSE.txt +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/README.md +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/matrice_streaming.egg-info/SOURCES.txt +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/matrice_streaming.egg-info/dependency_links.txt +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/matrice_streaming.egg-info/not-zip-safe +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/matrice_streaming.egg-info/top_level.txt +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/pyproject.toml +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/setup.cfg +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/setup.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/__init__.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/client/__init__.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/client/client.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/client/client_utils.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/deployment/__init__.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/deployment/camera_manager.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/deployment/deployment.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/deployment/inference_pipeline.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/deployment/streaming_gateway_manager.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/deployment/todo.txt +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/py.typed +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/__init__.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/ARCHITECTURE.md +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/__init__.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/async_camera_worker.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/camera_streamer.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/device_detection.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/encoder_manager.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/encoding_pool_manager.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_config.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_worker_manager.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/frame_processor.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/gstreamer_camera_streamer.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker_manager.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/message_builder.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/platform_pipelines.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/retry_manager.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/stream_statistics.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/video_capture_manager.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/camera_streamer/worker_manager.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/debug/README.md +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/debug/__init__.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/debug/benchmark.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/debug/debug_gstreamer_gateway.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/debug/debug_stream_backend.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/debug/debug_streaming_gateway.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/debug/debug_utils.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/debug/example_debug_streaming.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/debug/test_videoplayback.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/dynamic_camera_manager.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/event_listener.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/metrics_reporter.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/streaming_action.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/streaming_gateway.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/streaming_gateway_utils.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/streaming_gateway/streaming_status_listener.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/tests/test_async_infrastructure.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/tests/test_batch_auto_calculation.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/tests/test_batching_verification.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/tests/test_e2e_production.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/tests/test_flatten_binary.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/tests/test_gstreamer_integration.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/tests/test_msgpack_fix.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/tests/test_phase1_unit.py +0 -0
- {matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/tests/test_phase2_scaling.py +0 -0
|
@@ -20,6 +20,8 @@ import numpy as np
|
|
|
20
20
|
import cv2
|
|
21
21
|
import psutil
|
|
22
22
|
|
|
23
|
+
from matrice_common.optimize import FrameOptimizer
|
|
24
|
+
|
|
23
25
|
from .ffmpeg_config import FFmpegConfig, is_ffmpeg_available
|
|
24
26
|
from .ffmpeg_camera_streamer import FFmpegPipeline
|
|
25
27
|
|
|
@@ -83,6 +85,9 @@ class AsyncFFmpegWorker:
|
|
|
83
85
|
# Performance options
|
|
84
86
|
pin_cpu_affinity: bool = True,
|
|
85
87
|
total_workers: int = 1,
|
|
88
|
+
# Frame optimizer options
|
|
89
|
+
frame_optimizer_enabled: bool = True,
|
|
90
|
+
frame_optimizer_config: Optional[Dict[str, Any]] = None,
|
|
86
91
|
):
|
|
87
92
|
"""Initialize async FFmpeg worker.
|
|
88
93
|
|
|
@@ -100,6 +105,8 @@ class AsyncFFmpegWorker:
|
|
|
100
105
|
shm_frame_format: Frame format for SHM storage
|
|
101
106
|
pin_cpu_affinity: Pin worker to specific CPU cores
|
|
102
107
|
total_workers: Total number of workers for CPU affinity calculation
|
|
108
|
+
frame_optimizer_enabled: Enable frame optimizer for skipping similar frames
|
|
109
|
+
frame_optimizer_config: Frame optimizer configuration dict
|
|
103
110
|
"""
|
|
104
111
|
self.worker_id = worker_id
|
|
105
112
|
self.camera_configs = camera_configs
|
|
@@ -128,6 +135,17 @@ class AsyncFFmpegWorker:
|
|
|
128
135
|
self._shm_buffers: Dict[str, Any] = {}
|
|
129
136
|
self._last_shm_frame_idx: Dict[str, int] = {}
|
|
130
137
|
|
|
138
|
+
# Initialize frame optimizer for skipping similar frames
|
|
139
|
+
frame_optimizer_config = frame_optimizer_config or {}
|
|
140
|
+
self.frame_optimizer = FrameOptimizer(
|
|
141
|
+
enabled=frame_optimizer_enabled,
|
|
142
|
+
scale=frame_optimizer_config.get("scale", 0.4),
|
|
143
|
+
diff_threshold=frame_optimizer_config.get("diff_threshold", 15),
|
|
144
|
+
similarity_threshold=frame_optimizer_config.get("similarity_threshold", 0.05),
|
|
145
|
+
bg_update_interval=frame_optimizer_config.get("bg_update_interval", 10),
|
|
146
|
+
)
|
|
147
|
+
self._last_sent_frame_ids: Dict[str, str] = {} # stream_key -> last sent frame_id
|
|
148
|
+
|
|
131
149
|
# Register atexit handler for SHM cleanup
|
|
132
150
|
if use_shm:
|
|
133
151
|
import atexit
|
|
@@ -455,7 +473,47 @@ class AsyncFFmpegWorker:
|
|
|
455
473
|
"""
|
|
456
474
|
frame_start = time.time()
|
|
457
475
|
|
|
458
|
-
#
|
|
476
|
+
# Check frame similarity BEFORE encoding (saves CPU if frame is similar)
|
|
477
|
+
is_similar, similarity_score = self.frame_optimizer.is_similar(frame, stream_key)
|
|
478
|
+
reference_frame_id = self._last_sent_frame_ids.get(stream_key)
|
|
479
|
+
|
|
480
|
+
import uuid
|
|
481
|
+
|
|
482
|
+
if is_similar and reference_frame_id:
|
|
483
|
+
# Frame is similar - send message with empty content + cached_frame_id
|
|
484
|
+
message = {
|
|
485
|
+
"frame_id": str(uuid.uuid4()),
|
|
486
|
+
"input_name": stream_key,
|
|
487
|
+
"input_stream": {
|
|
488
|
+
"content": b"", # EMPTY content for cached frame
|
|
489
|
+
"metadata": {
|
|
490
|
+
"width": width,
|
|
491
|
+
"height": height,
|
|
492
|
+
"frame_count": frame_counter,
|
|
493
|
+
"camera_location": camera_location,
|
|
494
|
+
"stream_group_key": stream_group_key,
|
|
495
|
+
"encoding_type": "cached",
|
|
496
|
+
"codec": "cached",
|
|
497
|
+
"feed_type": "ffmpeg",
|
|
498
|
+
"timestamp": time.time(),
|
|
499
|
+
"similarity_score": similarity_score,
|
|
500
|
+
"cached_frame_id": reference_frame_id,
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# Send to Redis
|
|
506
|
+
write_start = time.time()
|
|
507
|
+
await self.redis_client.add_message(topic, message)
|
|
508
|
+
write_time = time.time() - write_start
|
|
509
|
+
|
|
510
|
+
# Track metrics (no encoding)
|
|
511
|
+
self._frames_per_camera[stream_key] = self._frames_per_camera.get(stream_key, 0) + 1
|
|
512
|
+
total_time = time.time() - frame_start
|
|
513
|
+
self._frame_times.append(total_time)
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
# Frame is different - encode and send full frame
|
|
459
517
|
encode_start = time.time()
|
|
460
518
|
success, jpeg_buffer = cv2.imencode(
|
|
461
519
|
'.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), quality]
|
|
@@ -473,9 +531,9 @@ class AsyncFFmpegWorker:
|
|
|
473
531
|
frame_data = bytes(jpeg_buffer)
|
|
474
532
|
|
|
475
533
|
# Build message
|
|
476
|
-
|
|
534
|
+
new_frame_id = str(uuid.uuid4())
|
|
477
535
|
message = {
|
|
478
|
-
"frame_id":
|
|
536
|
+
"frame_id": new_frame_id,
|
|
479
537
|
"input_name": stream_key,
|
|
480
538
|
"input_stream": {
|
|
481
539
|
"content": frame_data,
|
|
@@ -498,6 +556,10 @@ class AsyncFFmpegWorker:
|
|
|
498
556
|
await self.redis_client.add_message(topic, message)
|
|
499
557
|
write_time = time.time() - write_start
|
|
500
558
|
|
|
559
|
+
# Track this frame_id as the last sent for future reference frames
|
|
560
|
+
self._last_sent_frame_ids[stream_key] = new_frame_id
|
|
561
|
+
self.frame_optimizer.set_last_frame_id(stream_key, new_frame_id)
|
|
562
|
+
|
|
501
563
|
# Track metrics
|
|
502
564
|
total_time = time.time() - frame_start
|
|
503
565
|
self._frame_times.append(total_time)
|
|
@@ -529,6 +591,40 @@ class AsyncFFmpegWorker:
|
|
|
529
591
|
"""
|
|
530
592
|
frame_start = time.time()
|
|
531
593
|
|
|
594
|
+
# Check frame similarity BEFORE writing to SHM (saves SHM writes for static scenes)
|
|
595
|
+
is_similar, similarity_score = self.frame_optimizer.is_similar(frame, stream_key)
|
|
596
|
+
reference_frame_idx = self._last_shm_frame_idx.get(stream_key)
|
|
597
|
+
|
|
598
|
+
if is_similar and reference_frame_idx is not None:
|
|
599
|
+
# Frame is similar - send metadata with reference to previous frame
|
|
600
|
+
ts_ns = int(time.time() * 1e9)
|
|
601
|
+
shm_buffer = self._shm_buffers.get(stream_key)
|
|
602
|
+
|
|
603
|
+
await self.redis_client.add_shm_metadata(
|
|
604
|
+
stream_name=topic,
|
|
605
|
+
cam_id=stream_key,
|
|
606
|
+
shm_name=shm_buffer.shm_name if shm_buffer else "",
|
|
607
|
+
frame_idx=reference_frame_idx, # Reference to cached frame
|
|
608
|
+
slot=None, # No new slot written
|
|
609
|
+
ts_ns=ts_ns,
|
|
610
|
+
width=width,
|
|
611
|
+
height=height,
|
|
612
|
+
format=self.shm_frame_format,
|
|
613
|
+
is_similar=True,
|
|
614
|
+
reference_frame_idx=reference_frame_idx,
|
|
615
|
+
similarity_score=similarity_score,
|
|
616
|
+
stream_group_key=stream_group_key,
|
|
617
|
+
camera_location=camera_location,
|
|
618
|
+
frame_counter=frame_counter,
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# Track metrics (no SHM write)
|
|
622
|
+
self._frames_per_camera[stream_key] = self._frames_per_camera.get(stream_key, 0) + 1
|
|
623
|
+
total_time = time.time() - frame_start
|
|
624
|
+
self._frame_times.append(total_time)
|
|
625
|
+
return
|
|
626
|
+
|
|
627
|
+
# Frame is different - write to SHM
|
|
532
628
|
# Get or create SHM buffer
|
|
533
629
|
shm_buffer = self._get_or_create_shm_buffer(stream_key, width, height)
|
|
534
630
|
|
|
@@ -809,6 +905,8 @@ def run_ffmpeg_worker(
|
|
|
809
905
|
shm_frame_format: str = "BGR",
|
|
810
906
|
pin_cpu_affinity: bool = True,
|
|
811
907
|
total_workers: int = 1,
|
|
908
|
+
frame_optimizer_enabled: bool = True,
|
|
909
|
+
frame_optimizer_config: Optional[Dict[str, Any]] = None,
|
|
812
910
|
):
|
|
813
911
|
"""Entry point for FFmpeg worker process.
|
|
814
912
|
|
|
@@ -826,6 +924,8 @@ def run_ffmpeg_worker(
|
|
|
826
924
|
shm_frame_format: Frame format for SHM storage
|
|
827
925
|
pin_cpu_affinity: Pin worker to specific CPU cores
|
|
828
926
|
total_workers: Total number of workers
|
|
927
|
+
frame_optimizer_enabled: Enable frame optimizer
|
|
928
|
+
frame_optimizer_config: Frame optimizer configuration dict
|
|
829
929
|
"""
|
|
830
930
|
logging.basicConfig(
|
|
831
931
|
level=logging.INFO,
|
|
@@ -855,6 +955,8 @@ def run_ffmpeg_worker(
|
|
|
855
955
|
shm_frame_format=shm_frame_format,
|
|
856
956
|
pin_cpu_affinity=pin_cpu_affinity,
|
|
857
957
|
total_workers=total_workers,
|
|
958
|
+
frame_optimizer_enabled=frame_optimizer_enabled,
|
|
959
|
+
frame_optimizer_config=frame_optimizer_config,
|
|
858
960
|
)
|
|
859
961
|
|
|
860
962
|
asyncio.run(worker.run())
|
|
@@ -23,6 +23,10 @@ from collections import deque
|
|
|
23
23
|
|
|
24
24
|
import numpy as np
|
|
25
25
|
import cv2
|
|
26
|
+
import psutil
|
|
27
|
+
|
|
28
|
+
from matrice_common.optimize import FrameOptimizer
|
|
29
|
+
from matrice_common.stream.shm_ring_buffer import ShmRingBuffer
|
|
26
30
|
|
|
27
31
|
from .ffmpeg_config import FFmpegConfig, is_ffmpeg_available, detect_hwaccel
|
|
28
32
|
|
|
@@ -306,6 +310,16 @@ class FFmpegCameraStreamer:
|
|
|
306
310
|
video_codec: Optional[str] = None,
|
|
307
311
|
gateway_util=None,
|
|
308
312
|
ffmpeg_config: Optional[FFmpegConfig] = None,
|
|
313
|
+
# SHM mode options
|
|
314
|
+
use_shm: bool = False,
|
|
315
|
+
shm_slot_count: int = 300,
|
|
316
|
+
shm_frame_format: str = "BGR",
|
|
317
|
+
# Frame optimizer options
|
|
318
|
+
frame_optimizer_enabled: bool = True,
|
|
319
|
+
frame_optimizer_config: Optional[Dict[str, Any]] = None,
|
|
320
|
+
# CPU affinity options
|
|
321
|
+
pin_cpu_affinity: bool = False,
|
|
322
|
+
cpu_affinity_core: Optional[int] = None,
|
|
309
323
|
):
|
|
310
324
|
"""Initialize FFmpeg camera streamer.
|
|
311
325
|
|
|
@@ -316,6 +330,13 @@ class FFmpegCameraStreamer:
|
|
|
316
330
|
video_codec: Video codec (h264 or h265)
|
|
317
331
|
gateway_util: Gateway utility for API interactions
|
|
318
332
|
ffmpeg_config: FFmpeg configuration
|
|
333
|
+
use_shm: Enable SHM mode for raw frame sharing
|
|
334
|
+
shm_slot_count: Number of frame slots per camera ring buffer
|
|
335
|
+
shm_frame_format: Frame format for SHM storage ("BGR", "RGB", or "NV12")
|
|
336
|
+
frame_optimizer_enabled: Enable frame optimizer for skipping similar frames
|
|
337
|
+
frame_optimizer_config: Frame optimizer configuration dict
|
|
338
|
+
pin_cpu_affinity: Pin process to specific CPU core
|
|
339
|
+
cpu_affinity_core: CPU core to pin to (None = auto-assign)
|
|
319
340
|
"""
|
|
320
341
|
self.session = session
|
|
321
342
|
self.service_id = service_id
|
|
@@ -355,12 +376,136 @@ class FFmpegCameraStreamer:
|
|
|
355
376
|
# MatriceStream client
|
|
356
377
|
self.stream_client = None
|
|
357
378
|
|
|
379
|
+
# ================================================================
|
|
380
|
+
# SHM Mode Configuration
|
|
381
|
+
# ================================================================
|
|
382
|
+
self.use_shm = use_shm
|
|
383
|
+
self.shm_slot_count = shm_slot_count
|
|
384
|
+
self.shm_frame_format = shm_frame_format
|
|
385
|
+
self._shm_buffers: Dict[str, ShmRingBuffer] = {}
|
|
386
|
+
self._last_shm_frame_idx: Dict[str, int] = {}
|
|
387
|
+
|
|
388
|
+
# Register signal handlers for SHM cleanup
|
|
389
|
+
if use_shm:
|
|
390
|
+
self._setup_signal_handlers()
|
|
391
|
+
self.logger.info(
|
|
392
|
+
f"SHM mode ENABLED: format={shm_frame_format}, slots={shm_slot_count}"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# ================================================================
|
|
396
|
+
# Frame Optimizer Configuration
|
|
397
|
+
# ================================================================
|
|
398
|
+
frame_optimizer_config = frame_optimizer_config or {}
|
|
399
|
+
self.frame_optimizer = FrameOptimizer(
|
|
400
|
+
enabled=frame_optimizer_enabled,
|
|
401
|
+
scale=frame_optimizer_config.get("scale", 0.4),
|
|
402
|
+
diff_threshold=frame_optimizer_config.get("diff_threshold", 15),
|
|
403
|
+
similarity_threshold=frame_optimizer_config.get("similarity_threshold", 0.05),
|
|
404
|
+
bg_update_interval=frame_optimizer_config.get("bg_update_interval", 10),
|
|
405
|
+
)
|
|
406
|
+
self._last_sent_frame_ids: Dict[str, str] = {}
|
|
407
|
+
|
|
408
|
+
# ================================================================
|
|
409
|
+
# CPU Affinity Configuration
|
|
410
|
+
# ================================================================
|
|
411
|
+
self.pin_cpu_affinity = pin_cpu_affinity
|
|
412
|
+
self.cpu_affinity_core = cpu_affinity_core
|
|
413
|
+
self.pinned_cores: Optional[List[int]] = None
|
|
414
|
+
|
|
415
|
+
if pin_cpu_affinity:
|
|
416
|
+
self._apply_cpu_affinity()
|
|
417
|
+
|
|
358
418
|
self.logger.info(
|
|
359
419
|
f"FFmpegCameraStreamer initialized: "
|
|
360
420
|
f"hwaccel={self.ffmpeg_config.hwaccel}, "
|
|
361
|
-
f"pixel_format={self.ffmpeg_config.pixel_format}"
|
|
421
|
+
f"pixel_format={self.ffmpeg_config.pixel_format}, "
|
|
422
|
+
f"shm={use_shm}, optimizer={frame_optimizer_enabled}"
|
|
362
423
|
)
|
|
363
424
|
|
|
425
|
+
def _setup_signal_handlers(self):
|
|
426
|
+
"""Setup signal handlers for graceful SHM cleanup on termination."""
|
|
427
|
+
def cleanup_handler(signum, frame):
|
|
428
|
+
"""Handle SIGTERM/SIGINT for graceful SHM cleanup."""
|
|
429
|
+
sig_name = signal.Signals(signum).name if hasattr(signal.Signals, 'name') else str(signum)
|
|
430
|
+
self.logger.info(f"Received {sig_name}, cleaning up SHM buffers...")
|
|
431
|
+
self._cleanup_shm_buffers()
|
|
432
|
+
# Re-raise the signal to allow normal termination
|
|
433
|
+
signal.signal(signum, signal.SIG_DFL)
|
|
434
|
+
os.kill(os.getpid(), signum)
|
|
435
|
+
|
|
436
|
+
# Register signal handlers
|
|
437
|
+
signal.signal(signal.SIGINT, cleanup_handler)
|
|
438
|
+
if sys.platform != 'win32':
|
|
439
|
+
signal.signal(signal.SIGTERM, cleanup_handler)
|
|
440
|
+
|
|
441
|
+
# Also register atexit handler
|
|
442
|
+
import atexit
|
|
443
|
+
atexit.register(self._cleanup_shm_buffers)
|
|
444
|
+
|
|
445
|
+
def _cleanup_shm_buffers(self):
|
|
446
|
+
"""Cleanup all SHM buffers."""
|
|
447
|
+
for camera_id, shm_buffer in list(self._shm_buffers.items()):
|
|
448
|
+
try:
|
|
449
|
+
shm_buffer.close()
|
|
450
|
+
self.logger.info(f"Closed SHM buffer for {camera_id}")
|
|
451
|
+
except Exception as e:
|
|
452
|
+
self.logger.warning(f"Failed to cleanup SHM {camera_id}: {e}")
|
|
453
|
+
self._shm_buffers.clear()
|
|
454
|
+
|
|
455
|
+
def _apply_cpu_affinity(self):
|
|
456
|
+
"""Apply CPU affinity pinning."""
|
|
457
|
+
try:
|
|
458
|
+
p = psutil.Process()
|
|
459
|
+
if self.cpu_affinity_core is not None:
|
|
460
|
+
# Pin to specific core
|
|
461
|
+
self.pinned_cores = [self.cpu_affinity_core]
|
|
462
|
+
else:
|
|
463
|
+
# Auto-assign to first available core
|
|
464
|
+
cpu_count = psutil.cpu_count(logical=True)
|
|
465
|
+
self.pinned_cores = [0] if cpu_count > 0 else None
|
|
466
|
+
|
|
467
|
+
if self.pinned_cores:
|
|
468
|
+
p.cpu_affinity(self.pinned_cores)
|
|
469
|
+
self.logger.info(f"CPU affinity pinned to cores: {self.pinned_cores}")
|
|
470
|
+
except Exception as e:
|
|
471
|
+
self.logger.warning(f"Failed to set CPU affinity: {e}")
|
|
472
|
+
self.pinned_cores = None
|
|
473
|
+
|
|
474
|
+
def _get_or_create_shm_buffer(
|
|
475
|
+
self, stream_key: str, width: int, height: int
|
|
476
|
+
) -> ShmRingBuffer:
|
|
477
|
+
"""Get existing or create new SHM buffer for stream.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
stream_key: Stream identifier
|
|
481
|
+
width: Frame width
|
|
482
|
+
height: Frame height
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
ShmRingBuffer instance for this stream
|
|
486
|
+
"""
|
|
487
|
+
if stream_key not in self._shm_buffers:
|
|
488
|
+
format_map = {
|
|
489
|
+
"BGR": ShmRingBuffer.FORMAT_BGR,
|
|
490
|
+
"RGB": ShmRingBuffer.FORMAT_RGB,
|
|
491
|
+
"NV12": ShmRingBuffer.FORMAT_NV12,
|
|
492
|
+
}
|
|
493
|
+
frame_format = format_map.get(self.shm_frame_format, ShmRingBuffer.FORMAT_BGR)
|
|
494
|
+
|
|
495
|
+
self._shm_buffers[stream_key] = ShmRingBuffer(
|
|
496
|
+
camera_id=stream_key,
|
|
497
|
+
width=width,
|
|
498
|
+
height=height,
|
|
499
|
+
frame_format=frame_format,
|
|
500
|
+
slot_count=self.shm_slot_count,
|
|
501
|
+
create=True,
|
|
502
|
+
)
|
|
503
|
+
self.logger.info(
|
|
504
|
+
f"Created SHM buffer for {stream_key}: "
|
|
505
|
+
f"{width}x{height} {self.shm_frame_format}, {self.shm_slot_count} slots"
|
|
506
|
+
)
|
|
507
|
+
return self._shm_buffers[stream_key]
|
|
508
|
+
|
|
364
509
|
def register_stream_topic(self, stream_key: str, topic: str):
|
|
365
510
|
"""Register a topic for a stream.
|
|
366
511
|
|
|
@@ -538,39 +683,32 @@ class FFmpegCameraStreamer:
|
|
|
538
683
|
|
|
539
684
|
frame_counter += 1
|
|
540
685
|
|
|
541
|
-
#
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
# Update stats
|
|
569
|
-
self._transmission_stats["total_frames"] += 1
|
|
570
|
-
self._transmission_stats["total_bytes"] += len(frame_data)
|
|
571
|
-
|
|
572
|
-
except Exception as e:
|
|
573
|
-
self.logger.error(f"Failed to send frame: {e}")
|
|
686
|
+
# ================================================================
|
|
687
|
+
# SHM Mode vs JPEG Mode
|
|
688
|
+
# ================================================================
|
|
689
|
+
if self.use_shm:
|
|
690
|
+
self._process_frame_shm_mode(
|
|
691
|
+
frame=frame,
|
|
692
|
+
stream_key=stream_key,
|
|
693
|
+
stream_group_key=stream_group_key,
|
|
694
|
+
topic=topic,
|
|
695
|
+
width=pipeline.width,
|
|
696
|
+
height=pipeline.height,
|
|
697
|
+
frame_counter=frame_counter,
|
|
698
|
+
camera_location=camera_location,
|
|
699
|
+
)
|
|
700
|
+
else:
|
|
701
|
+
self._process_frame_jpeg_mode(
|
|
702
|
+
frame=frame,
|
|
703
|
+
stream_key=stream_key,
|
|
704
|
+
stream_group_key=stream_group_key,
|
|
705
|
+
topic=topic,
|
|
706
|
+
width=pipeline.width,
|
|
707
|
+
height=pipeline.height,
|
|
708
|
+
quality=quality,
|
|
709
|
+
frame_counter=frame_counter,
|
|
710
|
+
camera_location=camera_location,
|
|
711
|
+
)
|
|
574
712
|
|
|
575
713
|
# Maintain target FPS
|
|
576
714
|
elapsed = time.time() - loop_start
|
|
@@ -584,6 +722,206 @@ class FFmpegCameraStreamer:
|
|
|
584
722
|
|
|
585
723
|
self.logger.info(f"Stream loop stopped for {stream_key}")
|
|
586
724
|
|
|
725
|
+
def _process_frame_jpeg_mode(
|
|
726
|
+
self,
|
|
727
|
+
frame: np.ndarray,
|
|
728
|
+
stream_key: str,
|
|
729
|
+
stream_group_key: str,
|
|
730
|
+
topic: str,
|
|
731
|
+
width: int,
|
|
732
|
+
height: int,
|
|
733
|
+
quality: int,
|
|
734
|
+
frame_counter: int,
|
|
735
|
+
camera_location: str,
|
|
736
|
+
):
|
|
737
|
+
"""Process frame in JPEG mode with frame optimizer.
|
|
738
|
+
|
|
739
|
+
Args:
|
|
740
|
+
frame: Raw frame from FFmpeg
|
|
741
|
+
stream_key: Stream identifier
|
|
742
|
+
stream_group_key: Stream group identifier
|
|
743
|
+
topic: Topic name
|
|
744
|
+
width: Frame width
|
|
745
|
+
height: Frame height
|
|
746
|
+
quality: JPEG quality
|
|
747
|
+
frame_counter: Current frame number
|
|
748
|
+
camera_location: Camera location
|
|
749
|
+
"""
|
|
750
|
+
# Check frame similarity BEFORE encoding
|
|
751
|
+
is_similar, similarity_score = self.frame_optimizer.is_similar(frame, stream_key)
|
|
752
|
+
reference_frame_id = self._last_sent_frame_ids.get(stream_key)
|
|
753
|
+
|
|
754
|
+
import uuid
|
|
755
|
+
|
|
756
|
+
if is_similar and reference_frame_id:
|
|
757
|
+
# Frame is similar - send cached reference
|
|
758
|
+
message = {
|
|
759
|
+
"frame_id": str(uuid.uuid4()),
|
|
760
|
+
"input_name": stream_key,
|
|
761
|
+
"input_stream": {
|
|
762
|
+
"content": b"", # Empty for cached frame
|
|
763
|
+
"metadata": {
|
|
764
|
+
"width": width,
|
|
765
|
+
"height": height,
|
|
766
|
+
"frame_count": frame_counter,
|
|
767
|
+
"camera_location": camera_location,
|
|
768
|
+
"stream_group_key": stream_group_key,
|
|
769
|
+
"encoding_type": "cached",
|
|
770
|
+
"codec": "cached",
|
|
771
|
+
"timestamp": time.time(),
|
|
772
|
+
"similarity_score": similarity_score,
|
|
773
|
+
"cached_frame_id": reference_frame_id,
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if self.stream_client:
|
|
779
|
+
try:
|
|
780
|
+
self.stream_client.add_message(topic, message, key=stream_key)
|
|
781
|
+
self._transmission_stats["total_frames"] += 1
|
|
782
|
+
except Exception as e:
|
|
783
|
+
self.logger.error(f"Failed to send cached frame: {e}")
|
|
784
|
+
return
|
|
785
|
+
|
|
786
|
+
# Frame is different - encode and send full frame
|
|
787
|
+
success, jpeg_buffer = cv2.imencode(
|
|
788
|
+
'.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), quality]
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
if not success:
|
|
792
|
+
self.logger.warning(f"JPEG encode failed for {stream_key}")
|
|
793
|
+
return
|
|
794
|
+
|
|
795
|
+
frame_data = bytes(jpeg_buffer)
|
|
796
|
+
|
|
797
|
+
# Build and send message
|
|
798
|
+
new_frame_id = str(uuid.uuid4())
|
|
799
|
+
message = {
|
|
800
|
+
"frame_id": new_frame_id,
|
|
801
|
+
"input_name": stream_key,
|
|
802
|
+
"input_stream": {
|
|
803
|
+
"content": frame_data,
|
|
804
|
+
"metadata": {
|
|
805
|
+
"width": width,
|
|
806
|
+
"height": height,
|
|
807
|
+
"frame_count": frame_counter,
|
|
808
|
+
"camera_location": camera_location,
|
|
809
|
+
"stream_group_key": stream_group_key,
|
|
810
|
+
"encoding_type": "jpeg",
|
|
811
|
+
"codec": "h264",
|
|
812
|
+
"timestamp": time.time(),
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if self.stream_client:
|
|
818
|
+
try:
|
|
819
|
+
self.stream_client.add_message(topic, message, key=stream_key)
|
|
820
|
+
self._transmission_stats["total_frames"] += 1
|
|
821
|
+
self._transmission_stats["total_bytes"] += len(frame_data)
|
|
822
|
+
|
|
823
|
+
# Track this frame_id for future references
|
|
824
|
+
self._last_sent_frame_ids[stream_key] = new_frame_id
|
|
825
|
+
self.frame_optimizer.set_last_frame_id(stream_key, new_frame_id)
|
|
826
|
+
except Exception as e:
|
|
827
|
+
self.logger.error(f"Failed to send frame: {e}")
|
|
828
|
+
|
|
829
|
+
def _process_frame_shm_mode(
|
|
830
|
+
self,
|
|
831
|
+
frame: np.ndarray,
|
|
832
|
+
stream_key: str,
|
|
833
|
+
stream_group_key: str,
|
|
834
|
+
topic: str,
|
|
835
|
+
width: int,
|
|
836
|
+
height: int,
|
|
837
|
+
frame_counter: int,
|
|
838
|
+
camera_location: str,
|
|
839
|
+
):
|
|
840
|
+
"""Process frame in SHM mode - write raw frame to SHM, metadata to Redis.
|
|
841
|
+
|
|
842
|
+
Args:
|
|
843
|
+
frame: Raw frame from FFmpeg (BGR format)
|
|
844
|
+
stream_key: Stream identifier
|
|
845
|
+
stream_group_key: Stream group identifier
|
|
846
|
+
topic: Topic name
|
|
847
|
+
width: Frame width
|
|
848
|
+
height: Frame height
|
|
849
|
+
frame_counter: Current frame number
|
|
850
|
+
camera_location: Camera location
|
|
851
|
+
"""
|
|
852
|
+
# Check frame similarity BEFORE writing to SHM
|
|
853
|
+
is_similar, similarity_score = self.frame_optimizer.is_similar(frame, stream_key)
|
|
854
|
+
reference_frame_idx = self._last_shm_frame_idx.get(stream_key)
|
|
855
|
+
|
|
856
|
+
if is_similar and reference_frame_idx is not None:
|
|
857
|
+
# Frame is similar - send metadata with reference to previous frame
|
|
858
|
+
ts_ns = int(time.time() * 1e9)
|
|
859
|
+
shm_buffer = self._shm_buffers.get(stream_key)
|
|
860
|
+
|
|
861
|
+
if self.stream_client:
|
|
862
|
+
try:
|
|
863
|
+
self.stream_client.add_shm_metadata(
|
|
864
|
+
stream_name=topic,
|
|
865
|
+
cam_id=stream_key,
|
|
866
|
+
shm_name=shm_buffer.shm_name if shm_buffer else "",
|
|
867
|
+
frame_idx=reference_frame_idx,
|
|
868
|
+
slot=None,
|
|
869
|
+
ts_ns=ts_ns,
|
|
870
|
+
width=width,
|
|
871
|
+
height=height,
|
|
872
|
+
format=self.shm_frame_format,
|
|
873
|
+
is_similar=True,
|
|
874
|
+
reference_frame_idx=reference_frame_idx,
|
|
875
|
+
similarity_score=similarity_score,
|
|
876
|
+
stream_group_key=stream_group_key,
|
|
877
|
+
camera_location=camera_location,
|
|
878
|
+
frame_counter=frame_counter,
|
|
879
|
+
)
|
|
880
|
+
self._transmission_stats["total_frames"] += 1
|
|
881
|
+
except Exception as e:
|
|
882
|
+
self.logger.error(f"Failed to send SHM metadata: {e}")
|
|
883
|
+
return
|
|
884
|
+
|
|
885
|
+
# Frame is different - write to SHM
|
|
886
|
+
shm_buffer = self._get_or_create_shm_buffer(stream_key, width, height)
|
|
887
|
+
|
|
888
|
+
# Convert frame to target format
|
|
889
|
+
if self.shm_frame_format == "RGB":
|
|
890
|
+
raw_bytes = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB).tobytes()
|
|
891
|
+
elif self.shm_frame_format == "NV12":
|
|
892
|
+
from matrice_common.stream.shm_ring_buffer import bgr_to_nv12
|
|
893
|
+
raw_bytes = bgr_to_nv12(frame)
|
|
894
|
+
else: # BGR default
|
|
895
|
+
raw_bytes = frame.tobytes()
|
|
896
|
+
|
|
897
|
+
# Write to SHM ring buffer
|
|
898
|
+
frame_idx, slot = shm_buffer.write_frame(raw_bytes)
|
|
899
|
+
self._last_shm_frame_idx[stream_key] = frame_idx
|
|
900
|
+
|
|
901
|
+
# Send metadata to stream backend
|
|
902
|
+
ts_ns = int(time.time() * 1e9)
|
|
903
|
+
if self.stream_client:
|
|
904
|
+
try:
|
|
905
|
+
self.stream_client.add_shm_metadata(
|
|
906
|
+
stream_name=topic,
|
|
907
|
+
cam_id=stream_key,
|
|
908
|
+
shm_name=shm_buffer.shm_name,
|
|
909
|
+
frame_idx=frame_idx,
|
|
910
|
+
slot=slot,
|
|
911
|
+
ts_ns=ts_ns,
|
|
912
|
+
width=width,
|
|
913
|
+
height=height,
|
|
914
|
+
format=self.shm_frame_format,
|
|
915
|
+
is_similar=False,
|
|
916
|
+
stream_group_key=stream_group_key,
|
|
917
|
+
camera_location=camera_location,
|
|
918
|
+
frame_counter=frame_counter,
|
|
919
|
+
)
|
|
920
|
+
self._transmission_stats["total_frames"] += 1
|
|
921
|
+
self._transmission_stats["total_bytes"] += len(raw_bytes)
|
|
922
|
+
except Exception as e:
|
|
923
|
+
self.logger.error(f"Failed to send SHM metadata: {e}")
|
|
924
|
+
|
|
587
925
|
def _build_message(
|
|
588
926
|
self,
|
|
589
927
|
frame_data: bytes,
|
|
@@ -641,6 +979,15 @@ class FFmpegCameraStreamer:
|
|
|
641
979
|
self.pipelines[stream_key].close()
|
|
642
980
|
del self.pipelines[stream_key]
|
|
643
981
|
self.logger.info(f"Stopped stream: {stream_key}")
|
|
982
|
+
|
|
983
|
+
# Cleanup SHM buffer for this stream
|
|
984
|
+
if stream_key in self._shm_buffers:
|
|
985
|
+
try:
|
|
986
|
+
self._shm_buffers[stream_key].close()
|
|
987
|
+
del self._shm_buffers[stream_key]
|
|
988
|
+
self.logger.info(f"Closed SHM buffer for {stream_key}")
|
|
989
|
+
except Exception as e:
|
|
990
|
+
self.logger.warning(f"Failed to close SHM buffer {stream_key}: {e}")
|
|
644
991
|
else:
|
|
645
992
|
# Stop all streams
|
|
646
993
|
self._stop_streaming = True
|
|
@@ -657,6 +1004,11 @@ class FFmpegCameraStreamer:
|
|
|
657
1004
|
thread.join(timeout=5.0)
|
|
658
1005
|
|
|
659
1006
|
self.streaming_threads.clear()
|
|
1007
|
+
|
|
1008
|
+
# Cleanup all SHM buffers
|
|
1009
|
+
if self.use_shm:
|
|
1010
|
+
self._cleanup_shm_buffers()
|
|
1011
|
+
|
|
660
1012
|
self.logger.info("All FFmpeg streams stopped")
|
|
661
1013
|
|
|
662
1014
|
def get_transmission_stats(self) -> Dict[str, Any]:
|
|
File without changes
|
|
File without changes
|
{matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/matrice_streaming.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/matrice_streaming.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/matrice_streaming.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/client/__init__.py
RENAMED
|
File without changes
|
{matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/client/client.py
RENAMED
|
File without changes
|
{matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/client/client_utils.py
RENAMED
|
File without changes
|
{matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/deployment/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/deployment/deployment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{matrice_streaming-0.1.58 → matrice_streaming-0.1.59}/src/matrice_streaming/deployment/todo.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|