matrice-streaming 0.1.58__py3-none-any.whl → 0.1.59__py3-none-any.whl

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.
@@ -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
- # Encode to JPEG
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
- import uuid
534
+ new_frame_id = str(uuid.uuid4())
477
535
  message = {
478
- "frame_id": str(uuid.uuid4()),
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
- # Encode to JPEG
542
- encode_start = time.time()
543
- success, jpeg_buffer = cv2.imencode(
544
- '.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), quality]
545
- )
546
- encode_time = time.time() - encode_start
547
-
548
- if not success:
549
- self.logger.warning(f"JPEG encode failed for {stream_key}")
550
- continue
551
-
552
- frame_data = bytes(jpeg_buffer)
553
-
554
- # Send to stream backend
555
- if self.stream_client:
556
- try:
557
- message = self._build_message(
558
- frame_data=frame_data,
559
- stream_key=stream_key,
560
- stream_group_key=stream_group_key,
561
- width=pipeline.width,
562
- height=pipeline.height,
563
- frame_counter=frame_counter,
564
- camera_location=camera_location,
565
- )
566
- self.stream_client.add_message(topic, message, key=stream_key)
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]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrice_streaming
3
- Version: 0.1.58
3
+ Version: 0.1.59
4
4
  Summary: Common server utilities for Matrice.ai services
5
5
  Author-email: "Matrice.ai" <dipendra@matrice.ai>
6
6
  License-Expression: MIT
@@ -20,12 +20,12 @@ matrice_streaming/streaming_gateway/streaming_status_listener.py,sha256=RgbW0xYb
20
20
  matrice_streaming/streaming_gateway/camera_streamer/ARCHITECTURE.md,sha256=kngsqiS1PdNYhihBoMtoiIf3THJ4OM33E_hxExqzKqY,9980
21
21
  matrice_streaming/streaming_gateway/camera_streamer/__init__.py,sha256=_XI6CbqUxnkDuoQNYFcexhTKTBUfvH-Kj_XCUGSc_HY,1310
22
22
  matrice_streaming/streaming_gateway/camera_streamer/async_camera_worker.py,sha256=Zk65RTqZv_q0HzzdLgEY6qjaf0Lr87dBT04BYsSMekI,59247
23
- matrice_streaming/streaming_gateway/camera_streamer/async_ffmpeg_worker.py,sha256=BIJK1P_BZ0M_RMSt7MyXtH6xlWZOB6bQiFjZZkk2Pzw,32140
23
+ matrice_streaming/streaming_gateway/camera_streamer/async_ffmpeg_worker.py,sha256=2HEeK4f7yU5wM7lTZ8zGfzxY8qlokclVNyEGfioLKsg,37005
24
24
  matrice_streaming/streaming_gateway/camera_streamer/camera_streamer.py,sha256=1BzWnwfPEWPVQ6gBW7uZFvykq2zEvygd5LWE6uqVRhY,39085
25
25
  matrice_streaming/streaming_gateway/camera_streamer/device_detection.py,sha256=9F4rsbMpIexOIlX8aCj7Q6PFG01kOS1wtgAIQBG0FaM,18463
26
26
  matrice_streaming/streaming_gateway/camera_streamer/encoder_manager.py,sha256=guWqNtgGZQBVBxeDlAVnLWTP-hsleWmKYuVcnd0Hvn0,6962
27
27
  matrice_streaming/streaming_gateway/camera_streamer/encoding_pool_manager.py,sha256=kYfHu-B8bw9wTFxociAYLVvk-UOC-KqcaFdZ1TQuah4,4222
28
- matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_camera_streamer.py,sha256=QKMDWUXHz5o2kEHkJBgOCajrXiFH7czCHe9jJegN-0M,23318
28
+ matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_camera_streamer.py,sha256=i3OwGeXREtd-VDv90S70ZhsGwMFTvnwkuwxrvrZRfYM,37645
29
29
  matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_config.py,sha256=fbogEMtmqw4Fae4MS7vL1oQeFy3FkQbJlRGRF_BxIIo,6156
30
30
  matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_worker_manager.py,sha256=zqbsmicc5KddGqplpYumo_2jK6wZNXhPHSEgujdCCLE,17395
31
31
  matrice_streaming/streaming_gateway/camera_streamer/frame_processor.py,sha256=UPifhOr7wYpA1ACU4xGzC3A4lmbr2FvohcG371O6FHQ,2255
@@ -47,8 +47,8 @@ matrice_streaming/streaming_gateway/debug/debug_streaming_gateway.py,sha256=ZiDg
47
47
  matrice_streaming/streaming_gateway/debug/debug_utils.py,sha256=jWcSBgrk_YVt1QzSyw6geX17YBnTvgVdA5ubqO531a0,10477
48
48
  matrice_streaming/streaming_gateway/debug/example_debug_streaming.py,sha256=-gS8zNDswAoj6oss66QQWYZhY24usfLiMH0FFK06vV0,7994
49
49
  matrice_streaming/streaming_gateway/debug/test_videoplayback.py,sha256=s_dgWkoESiuJHlUAf_iv4d7OGmAhwocwDZmIcFUZzvo,11093
50
- matrice_streaming-0.1.58.dist-info/licenses/LICENSE.txt,sha256=_uQUZpgO0mRYL5-fPoEvLSbNnLPv6OmbeEDCHXhK6Qc,1066
51
- matrice_streaming-0.1.58.dist-info/METADATA,sha256=PbXqPZOjt7T0Rh2RuW4qVxDHdX_ZmS7cLpEHIv6dLqY,2477
52
- matrice_streaming-0.1.58.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
- matrice_streaming-0.1.58.dist-info/top_level.txt,sha256=PM_trIe8f4JLc90J871rNMYGVM3Po9Inx4As5LrCFUU,18
54
- matrice_streaming-0.1.58.dist-info/RECORD,,
50
+ matrice_streaming-0.1.59.dist-info/licenses/LICENSE.txt,sha256=_uQUZpgO0mRYL5-fPoEvLSbNnLPv6OmbeEDCHXhK6Qc,1066
51
+ matrice_streaming-0.1.59.dist-info/METADATA,sha256=JRPBDQVfq8dCsEgqqXI8adcOjnF4cpCabfnJPAv-gCE,2477
52
+ matrice_streaming-0.1.59.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ matrice_streaming-0.1.59.dist-info/top_level.txt,sha256=PM_trIe8f4JLc90J871rNMYGVM3Po9Inx4As5LrCFUU,18
54
+ matrice_streaming-0.1.59.dist-info/RECORD,,