matrice-streaming 0.1.72__py3-none-any.whl → 0.1.74__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.
@@ -495,6 +495,9 @@ class AsyncCameraWorker:
495
495
  cap = None
496
496
  consecutive_failures = 0
497
497
  frame_counter = 0
498
+ # PTS-based pacing for video files
499
+ first_video_ms = None
500
+ first_wall_time = None
498
501
 
499
502
  try:
500
503
  # Prepare source (download if URL)
@@ -570,6 +573,18 @@ class AsyncCameraWorker:
570
573
  consecutive_failures = 0
571
574
  frame_counter += 1
572
575
 
576
+ # Get video file timestamp (for video files, use video PTS instead of wall clock)
577
+ # This prevents timestamp jumps when pipeline stalls
578
+ video_ts_ms = cap.get(cv2.CAP_PROP_POS_MSEC) if source_type == "video_file" else None
579
+
580
+ # Frame decimation: Read ALL frames, skip based on target FPS
581
+ # This avoids sleep-based throttling which misses frames
582
+ source_fps = video_props.get('original_fps', 30) if video_props else 30
583
+ if fps > 0 and source_fps > fps:
584
+ skip_ratio = int(source_fps / fps)
585
+ if skip_ratio > 1 and frame_counter % skip_ratio != 0:
586
+ continue # Skip this frame, read next immediately
587
+
573
588
  # Resize if needed
574
589
  if width or height:
575
590
  frame = FrameProcessor.resize_frame(frame, width, height)
@@ -581,7 +596,7 @@ class AsyncCameraWorker:
581
596
  await self._process_frame_shm_mode(
582
597
  frame, stream_key, stream_group_key, topic,
583
598
  actual_width, actual_height, frame_counter,
584
- camera_location, read_time
599
+ camera_location, read_time, video_ts_ms
585
600
  )
586
601
  else:
587
602
  # EXISTING FLOW: JPEG encode and send full frame
@@ -589,16 +604,22 @@ class AsyncCameraWorker:
589
604
  frame, stream_key, stream_group_key, topic,
590
605
  source, video_props, fps, quality,
591
606
  actual_width, actual_height, source_type,
592
- frame_counter, camera_location, read_time
607
+ frame_counter, camera_location, read_time,
608
+ video_ts_ms
593
609
  )
594
610
 
595
- # Maintain target FPS for ALL sources (video files AND live cameras)
596
- # This prevents overwhelming the encoder by reading at native camera rate (30+ FPS)
597
- frame_interval = 1.0 / fps
598
- frame_elapsed = time.time() - read_start
599
- sleep_time = max(0, frame_interval - frame_elapsed)
600
- if sleep_time > 0:
601
- await asyncio.sleep(sleep_time)
611
+ # PTS-based pacing for video files (smooth playback without drift)
612
+ # Instead of fixed-interval sleep, pace based on video timeline
613
+ if source_type == "video_file" and video_ts_ms is not None:
614
+ if first_video_ms is None:
615
+ first_video_ms = video_ts_ms
616
+ first_wall_time = time.time()
617
+ else:
618
+ # Calculate target wall time based on video PTS
619
+ target_wall = first_wall_time + (video_ts_ms - first_video_ms) / 1000.0
620
+ sleep_time = target_wall - time.time()
621
+ if sleep_time > 0:
622
+ await asyncio.sleep(sleep_time)
602
623
 
603
624
  except asyncio.CancelledError:
604
625
  self.logger.info(f"Worker {self.worker_id}: Camera {stream_key} task cancelled")
@@ -671,7 +692,8 @@ class AsyncCameraWorker:
671
692
  source_type: str,
672
693
  frame_counter: int,
673
694
  camera_location: str,
674
- read_time: float
695
+ read_time: float,
696
+ video_ts_ms: Optional[float] = None
675
697
  ):
676
698
  """Process frame and send to Redis asynchronously.
677
699
 
@@ -692,6 +714,7 @@ class AsyncCameraWorker:
692
714
  frame_counter: Current frame number
693
715
  camera_location: Camera location
694
716
  read_time: Time taken to read frame
717
+ video_ts_ms: Video file timestamp in milliseconds (None for live streams)
695
718
  """
696
719
  frame_start = time.time()
697
720
 
@@ -865,7 +888,8 @@ class AsyncCameraWorker:
865
888
  height: int,
866
889
  frame_counter: int,
867
890
  camera_location: str,
868
- read_time: float
891
+ read_time: float,
892
+ video_ts_ms: Optional[float] = None
869
893
  ):
870
894
  """SHM_MODE: Write raw frame to SHM, send metadata to Redis.
871
895
 
@@ -882,6 +906,7 @@ class AsyncCameraWorker:
882
906
  frame_counter: Current frame number
883
907
  camera_location: Camera location string
884
908
  read_time: Time taken to read frame
909
+ video_ts_ms: Video file timestamp in milliseconds (None for live streams)
885
910
  """
886
911
  frame_start = time.time()
887
912
 
@@ -895,7 +920,11 @@ class AsyncCameraWorker:
895
920
  if is_similar and reference_frame_idx is not None:
896
921
  # Frame is similar - send metadata with reference to previous frame
897
922
  # Consumer can skip reading SHM and use previous result
898
- ts_ns = int(time.time() * 1e9)
923
+ # Use video PTS for video files, wall clock for live streams
924
+ if video_ts_ms is not None:
925
+ ts_ns = int(video_ts_ms * 1e6) # Convert ms to ns
926
+ else:
927
+ ts_ns = int(time.time() * 1e9)
899
928
  shm_buffer = self._shm_buffers.get(stream_key)
900
929
 
901
930
  await self.redis_client.add_shm_metadata(
@@ -950,7 +979,11 @@ class AsyncCameraWorker:
950
979
  self._last_shm_frame_idx[stream_key] = frame_idx
951
980
 
952
981
  # Send metadata-only message to Redis
953
- ts_ns = int(time.time() * 1e9)
982
+ # Use video PTS for video files, wall clock for live streams
983
+ if video_ts_ms is not None:
984
+ ts_ns = int(video_ts_ms * 1e6) # Convert ms to ns
985
+ else:
986
+ ts_ns = int(time.time() * 1e9)
954
987
  write_start = time.time()
955
988
 
956
989
  await self.redis_client.add_shm_metadata(
@@ -393,6 +393,16 @@ class StreamConfig:
393
393
  height: int = 640
394
394
  target_fps: int = 10
395
395
  gpu_id: int = 0
396
+ stream_type: str = "file" # "file" or "rtsp"
397
+
398
+ def __post_init__(self):
399
+ """Auto-detect stream_type from video_path if not explicitly set."""
400
+ if self.video_path.startswith("rtsp://") or self.video_path.startswith("rtsps://"):
401
+ self.stream_type = "rtsp"
402
+ elif self.video_path.startswith("http://") or self.video_path.startswith("https://"):
403
+ self.stream_type = "file" # Downloaded files
404
+ else:
405
+ self.stream_type = "file"
396
406
 
397
407
 
398
408
  @dataclass
@@ -424,6 +434,8 @@ class StreamState:
424
434
  empty_packets: int = 0
425
435
  decode_errors: int = 0 # Consecutive decode errors
426
436
  MAX_DECODE_ERRORS: int = 5 # Restart demuxer after this many consecutive errors
437
+ stream_type: str = "file" # "file" or "rtsp"
438
+ source_fps: float = 30.0 # Source video FPS for timestamp calculation
427
439
 
428
440
 
429
441
  # =============================================================================
@@ -620,11 +632,20 @@ class NVDECDecoderPool:
620
632
  logger.info(f"Created NVDEC pool: {self.actual_pool_size}/{pool_size} decoders on GPU {gpu_id}")
621
633
 
622
634
  def assign_stream(self, stream_id: int, camera_id: str, video_path: str,
623
- width: int = 640, height: int = 640) -> bool:
635
+ width: int = 640, height: int = 640,
636
+ stream_type: str = "file") -> bool:
624
637
  """Assign a stream to a decoder (round-robin).
625
638
 
626
639
  Automatically downloads HTTPS URLs to local files since PyNvVideoCodec's
627
640
  bundled FFmpeg doesn't support HTTPS protocol.
641
+
642
+ Args:
643
+ stream_id: Stream identifier
644
+ camera_id: Camera identifier
645
+ video_path: Video file path or RTSP URL
646
+ width: Target width
647
+ height: Target height
648
+ stream_type: Source type ("file" or "rtsp")
628
649
  """
629
650
  if self.actual_pool_size == 0:
630
651
  return False
@@ -641,23 +662,40 @@ class NVDECDecoderPool:
641
662
  logger.error(f"Failed to create demuxer for {camera_id}: {e}")
642
663
  return False
643
664
 
665
+ # Extract source FPS from demuxer for video timestamp calculation
666
+ source_fps = 30.0 # Default fallback
667
+ try:
668
+ # PyNvVideoCodec demuxer provides frame rate info
669
+ fps_num = demuxer.FrameRate()[0]
670
+ fps_den = demuxer.FrameRate()[1]
671
+ if fps_den > 0:
672
+ source_fps = fps_num / fps_den
673
+ logger.debug(f"{camera_id}: Detected source FPS = {source_fps:.2f}")
674
+ except Exception as e:
675
+ logger.debug(f"{camera_id}: Could not get FPS from demuxer, using default: {e}")
676
+
644
677
  stream_state = StreamState(
645
678
  stream_id=stream_id,
646
679
  camera_id=camera_id,
647
680
  video_path=local_path, # Store local path for video looping
648
681
  demuxer=demuxer,
649
682
  width=width,
650
- height=height
683
+ height=height,
684
+ stream_type=stream_type,
685
+ source_fps=source_fps,
651
686
  )
652
687
  self.streams_per_decoder[decoder_idx].append(stream_state)
653
688
  return True
654
689
 
655
690
  def decode_round(self, decoder_idx: int, frames_per_stream: int = 4,
656
- target_h: int = 640, target_w: int = 640) -> Tuple[int, List[Tuple[str, cp.ndarray]]]:
691
+ target_h: int = 640, target_w: int = 640) -> Tuple[int, List[Tuple[str, cp.ndarray, int, str]]]:
657
692
  """Decode frames and convert to NV12.
658
693
 
659
694
  Returns:
660
- (total_frames, [(camera_id, nv12_tensor), ...])
695
+ (total_frames, [(camera_id, nv12_tensor, timestamp_ns, stream_type), ...])
696
+ where:
697
+ - timestamp_ns: For RTSP = UTC nanoseconds, for files = video timestamp ns (frame_num / fps * 1e9)
698
+ - stream_type: "rtsp" or "file"
661
699
  """
662
700
  if decoder_idx >= self.actual_pool_size:
663
701
  return 0, []
@@ -676,6 +714,8 @@ class NVDECDecoderPool:
676
714
  if packet is None:
677
715
  stream.demuxer = nvc.CreateDemuxer(stream.video_path)
678
716
  stream.empty_packets = 0
717
+ # Reset frame counter on loop for consistent video timestamps
718
+ stream.frames_decoded = 0
679
719
  packet = stream.demuxer.Demux()
680
720
  if packet is None:
681
721
  break
@@ -685,10 +725,21 @@ class NVDECDecoderPool:
685
725
  # Wrap decode loop in try/except to catch decode errors
686
726
  try:
687
727
  for surface in decoder.Decode(packet):
728
+ # Calculate timestamp based on stream type:
729
+ # - RTSP: UTC nanoseconds for real-time sync
730
+ # - File: Video timestamp (frame_num / fps * 1e9) for video playback sync
731
+ if stream.stream_type == "rtsp":
732
+ decode_timestamp_ns = time.time_ns()
733
+ else:
734
+ # Video file: use video timestamp based on frame number
735
+ # This allows syncing to the video timeline instead of wall clock
736
+ video_time_seconds = stream.frames_decoded / stream.source_fps
737
+ decode_timestamp_ns = int(video_time_seconds * 1_000_000_000)
738
+
688
739
  tensor = surface_to_nv12(surface, target_h, target_w)
689
740
 
690
741
  if tensor is not None:
691
- decoded_frames.append((stream.camera_id, tensor))
742
+ decoded_frames.append((stream.camera_id, tensor, decode_timestamp_ns, stream.stream_type))
692
743
  frames_this_stream += 1
693
744
  stream.frames_decoded += 1
694
745
  total_frames += 1
@@ -839,7 +890,7 @@ def nvdec_pool_worker(
839
890
  target_w=TARGET_WIDTH # Always 640
840
891
  )
841
892
 
842
- for cam_id, tensor in decoded_frames:
893
+ for cam_id, tensor, decode_timestamp_ns, stream_type in decoded_frames:
843
894
  if cam_id in ring_buffers:
844
895
  try:
845
896
  # Validate frame shape matches expected NV12 dimensions
@@ -853,7 +904,11 @@ def nvdec_pool_worker(
853
904
  )
854
905
  continue # Skip this frame
855
906
 
856
- ring_buffers[cam_id].write_frame_fast(tensor, sync=False)
907
+ # Pass decode-time timestamp for accurate frame timing
908
+ # For RTSP: UTC nanoseconds, for files: video timestamp ns
909
+ ring_buffers[cam_id].write_frame_fast(
910
+ tensor, sync=False, timestamp_ns=decode_timestamp_ns
911
+ )
857
912
  local_frames += 1
858
913
  frames_since_counter_update += 1
859
914
 
@@ -880,7 +935,7 @@ def nvdec_pool_worker(
880
935
  # Sync all buffers that received frames in this round
881
936
  # Critical for cross-container IPC - ensures GPU writes are visible
882
937
  synced_cams = set()
883
- for cam_id, _ in decoded_frames:
938
+ for cam_id, _, _, _ in decoded_frames:
884
939
  if cam_id in ring_buffers and cam_id not in synced_cams:
885
940
  ring_buffers[cam_id].sync_writes()
886
941
  synced_cams.add(cam_id)
@@ -1047,7 +1102,8 @@ def nvdec_pool_process(
1047
1102
  camera_id=config.camera_id,
1048
1103
  video_path=config.video_path,
1049
1104
  width=TARGET_WIDTH, # Use fixed width
1050
- height=TARGET_HEIGHT # Use fixed height
1105
+ height=TARGET_HEIGHT, # Use fixed height
1106
+ stream_type=config.stream_type, # Pass stream type for timestamp mode
1051
1107
  )
1052
1108
 
1053
1109
  # Write all GPU mappings at once
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrice_streaming
3
- Version: 0.1.72
3
+ Version: 0.1.74
4
4
  Summary: Common server utilities for Matrice.ai services
5
5
  Author-email: "Matrice.ai" <dipendra@matrice.ai>
6
6
  License-Expression: MIT
@@ -19,7 +19,7 @@ matrice_streaming/streaming_gateway/streaming_gateway_utils.py,sha256=6kNYgl3f7H
19
19
  matrice_streaming/streaming_gateway/streaming_status_listener.py,sha256=RgbW0xYbbpmO6ZjkVlh6fg8iqkpRaIShR2dQ3SMVFUw,3161
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=nwVY-ySnKvOedeDXakyR_6KPHSz3yzSeaO_4IFMMP4I,2219
22
- matrice_streaming/streaming_gateway/camera_streamer/async_camera_worker.py,sha256=W6hEeh0KLGXIBAixDIomAAOOoo5QectCp1__RHLS-ko,61242
22
+ matrice_streaming/streaming_gateway/camera_streamer/async_camera_worker.py,sha256=AJOqvG8ZN9JMsImNuP1Pz1CEb0xGsJCPnnU-e8TA8eU,63263
23
23
  matrice_streaming/streaming_gateway/camera_streamer/async_ffmpeg_worker.py,sha256=cD3XocWqamkBE9TlkG757OK6tl_Op45r-cMd-ZgJXaA,37063
24
24
  matrice_streaming/streaming_gateway/camera_streamer/camera_streamer.py,sha256=zv6tWUKQf2uXAxqtSlfwAVDb9N483VCRApPogUWJ9e8,39113
25
25
  matrice_streaming/streaming_gateway/camera_streamer/device_detection.py,sha256=9F4rsbMpIexOIlX8aCj7Q6PFG01kOS1wtgAIQBG0FaM,18463
@@ -33,7 +33,7 @@ matrice_streaming/streaming_gateway/camera_streamer/gstreamer_camera_streamer.py
33
33
  matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker.py,sha256=AqKNJ6q_BxFphOlJ2GaS4WpoLCHXLEu5JVvoKQNrGV0,42822
34
34
  matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker_manager.py,sha256=jlKwIWWMXpztdyKiyremGmkVyw9mf2AxEmT7154xnrc,22002
35
35
  matrice_streaming/streaming_gateway/camera_streamer/message_builder.py,sha256=W295q6cIm05ReF1ooQus3rsKgZOG3EldZplbQco-OyM,10231
36
- matrice_streaming/streaming_gateway/camera_streamer/nvdec.py,sha256=A94bIwBC9rX7AJUdNYm9KRoKgiVEgSI5z_0zMpeQZ7g,58503
36
+ matrice_streaming/streaming_gateway/camera_streamer/nvdec.py,sha256=Eumbrv0XVxuwqUiSZSxylWFph_3vsvYd-BK_zg61Wqc,61651
37
37
  matrice_streaming/streaming_gateway/camera_streamer/nvdec_worker_manager.py,sha256=KlcwKFUPVZTQ3J1VIuhPev8Xv9BNw4dj2iLGHrREQCQ,16035
38
38
  matrice_streaming/streaming_gateway/camera_streamer/platform_pipelines.py,sha256=UNjsYYWbUJteOq2tzxISFAbWMa2e9GUExSS6fWc2Aow,27303
39
39
  matrice_streaming/streaming_gateway/camera_streamer/retry_manager.py,sha256=d8tlGoWoeSlgpCgXbUHTM61ekCQZki7TO1HzL2yPVzk,3607
@@ -49,8 +49,8 @@ matrice_streaming/streaming_gateway/debug/debug_streaming_gateway.py,sha256=ZiDg
49
49
  matrice_streaming/streaming_gateway/debug/debug_utils.py,sha256=jWcSBgrk_YVt1QzSyw6geX17YBnTvgVdA5ubqO531a0,10477
50
50
  matrice_streaming/streaming_gateway/debug/example_debug_streaming.py,sha256=-gS8zNDswAoj6oss66QQWYZhY24usfLiMH0FFK06vV0,7994
51
51
  matrice_streaming/streaming_gateway/debug/test_videoplayback.py,sha256=s_dgWkoESiuJHlUAf_iv4d7OGmAhwocwDZmIcFUZzvo,11093
52
- matrice_streaming-0.1.72.dist-info/licenses/LICENSE.txt,sha256=_uQUZpgO0mRYL5-fPoEvLSbNnLPv6OmbeEDCHXhK6Qc,1066
53
- matrice_streaming-0.1.72.dist-info/METADATA,sha256=R5X_pdLFu9o3SUw0LXwB0xsbxvk0b3We7IbCpoRwgQc,2477
54
- matrice_streaming-0.1.72.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
55
- matrice_streaming-0.1.72.dist-info/top_level.txt,sha256=PM_trIe8f4JLc90J871rNMYGVM3Po9Inx4As5LrCFUU,18
56
- matrice_streaming-0.1.72.dist-info/RECORD,,
52
+ matrice_streaming-0.1.74.dist-info/licenses/LICENSE.txt,sha256=_uQUZpgO0mRYL5-fPoEvLSbNnLPv6OmbeEDCHXhK6Qc,1066
53
+ matrice_streaming-0.1.74.dist-info/METADATA,sha256=5-ineu029FfwWA0JQszQXYX8zt9uUsyS-zYhrl6ThZA,2477
54
+ matrice_streaming-0.1.74.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
55
+ matrice_streaming-0.1.74.dist-info/top_level.txt,sha256=PM_trIe8f4JLc90J871rNMYGVM3Po9Inx4As5LrCFUU,18
56
+ matrice_streaming-0.1.74.dist-info/RECORD,,