matrice-streaming 0.1.14__py3-none-any.whl → 0.1.65__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.
Files changed (38) hide show
  1. matrice_streaming/__init__.py +44 -32
  2. matrice_streaming/streaming_gateway/camera_streamer/__init__.py +68 -1
  3. matrice_streaming/streaming_gateway/camera_streamer/async_camera_worker.py +1388 -0
  4. matrice_streaming/streaming_gateway/camera_streamer/async_ffmpeg_worker.py +966 -0
  5. matrice_streaming/streaming_gateway/camera_streamer/camera_streamer.py +188 -24
  6. matrice_streaming/streaming_gateway/camera_streamer/device_detection.py +507 -0
  7. matrice_streaming/streaming_gateway/camera_streamer/encoding_pool_manager.py +136 -0
  8. matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_camera_streamer.py +1048 -0
  9. matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_config.py +192 -0
  10. matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_worker_manager.py +470 -0
  11. matrice_streaming/streaming_gateway/camera_streamer/gstreamer_camera_streamer.py +1368 -0
  12. matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker.py +1063 -0
  13. matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker_manager.py +546 -0
  14. matrice_streaming/streaming_gateway/camera_streamer/message_builder.py +60 -15
  15. matrice_streaming/streaming_gateway/camera_streamer/nvdec.py +1330 -0
  16. matrice_streaming/streaming_gateway/camera_streamer/nvdec_worker_manager.py +412 -0
  17. matrice_streaming/streaming_gateway/camera_streamer/platform_pipelines.py +680 -0
  18. matrice_streaming/streaming_gateway/camera_streamer/stream_statistics.py +111 -4
  19. matrice_streaming/streaming_gateway/camera_streamer/video_capture_manager.py +223 -27
  20. matrice_streaming/streaming_gateway/camera_streamer/worker_manager.py +694 -0
  21. matrice_streaming/streaming_gateway/debug/__init__.py +27 -2
  22. matrice_streaming/streaming_gateway/debug/benchmark.py +727 -0
  23. matrice_streaming/streaming_gateway/debug/debug_gstreamer_gateway.py +599 -0
  24. matrice_streaming/streaming_gateway/debug/debug_streaming_gateway.py +245 -95
  25. matrice_streaming/streaming_gateway/debug/debug_utils.py +29 -0
  26. matrice_streaming/streaming_gateway/debug/test_videoplayback.py +318 -0
  27. matrice_streaming/streaming_gateway/dynamic_camera_manager.py +656 -39
  28. matrice_streaming/streaming_gateway/metrics_reporter.py +676 -139
  29. matrice_streaming/streaming_gateway/streaming_action.py +71 -20
  30. matrice_streaming/streaming_gateway/streaming_gateway.py +1026 -78
  31. matrice_streaming/streaming_gateway/streaming_gateway_utils.py +175 -20
  32. matrice_streaming/streaming_gateway/streaming_status_listener.py +89 -0
  33. {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/METADATA +1 -1
  34. matrice_streaming-0.1.65.dist-info/RECORD +56 -0
  35. matrice_streaming-0.1.14.dist-info/RECORD +0 -38
  36. {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/WHEEL +0 -0
  37. {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/licenses/LICENSE.txt +0 -0
  38. {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/top_level.txt +0 -0
@@ -20,18 +20,21 @@ class DynamicCameraManager:
20
20
  self,
21
21
  camera_streamer,
22
22
  streaming_gateway_id: str,
23
- session=None
23
+ session=None,
24
+ streaming_gateway=None
24
25
  ):
25
26
  """Initialize dynamic camera manager.
26
-
27
+
27
28
  Args:
28
29
  camera_streamer: CameraStreamer instance to control streams
29
30
  streaming_gateway_id: ID of the streaming gateway
30
31
  session: Session object for API calls (optional)
32
+ streaming_gateway: StreamingGateway instance for updating mappings (optional)
31
33
  """
32
34
  self.camera_streamer = camera_streamer
33
35
  self.streaming_gateway_id = streaming_gateway_id
34
36
  self.session = session
37
+ self.streaming_gateway = streaming_gateway
35
38
 
36
39
  # Initialize gateway util for API calls
37
40
  self.gateway_util = None
@@ -113,23 +116,31 @@ class DynamicCameraManager:
113
116
  self.logger.info(f"Initialized with {len(input_streams)} cameras")
114
117
 
115
118
  def add_camera(self, camera_data: Dict[str, Any]) -> bool:
116
- """Add a new camera and start streaming.
117
-
119
+ """Add a new camera and start streaming if active.
120
+
118
121
  Args:
119
122
  camera_data: Camera configuration data from event
120
-
123
+
121
124
  Returns:
122
125
  bool: True if camera was added successfully
123
126
  """
124
127
  camera_id = camera_data.get('id')
125
128
  camera_name = camera_data.get('cameraName', 'Unknown')
126
-
129
+ is_active = camera_data.get('isActive', True) # Default to True
130
+
127
131
  with self._lock:
128
132
  # Check if camera already exists
129
133
  if camera_id in self.cameras:
130
134
  self.logger.warning(f"Camera {camera_id} already exists, updating instead")
131
135
  return self.update_camera(camera_data)
132
-
136
+
137
+ # If camera is not active (scheduled), just store data without streaming
138
+ if not is_active:
139
+ self.cameras[camera_id] = camera_data
140
+ self.logger.info(f"Camera {camera_name} added but not active (scheduled)")
141
+ self.stats['cameras_added'] += 1
142
+ return True
143
+
133
144
  try:
134
145
  # Create InputStream from camera data
135
146
  input_stream = self._create_input_stream(camera_data)
@@ -171,11 +182,16 @@ class DynamicCameraManager:
171
182
  'input': topic,
172
183
  'output': None,
173
184
  }
174
-
185
+
186
+ # Update streaming gateway mappings for metrics
187
+ if self.streaming_gateway:
188
+ self.streaming_gateway._stream_key_to_camera_id[input_stream.camera_key] = camera_id
189
+ self.streaming_gateway._my_stream_keys.add(input_stream.camera_key)
190
+
175
191
  # Update statistics
176
192
  self.stats['cameras_added'] += 1
177
193
  self.stats['active_cameras'] += 1
178
-
194
+
179
195
  self.logger.info(f"Successfully added and started streaming for camera: {camera_name}")
180
196
  return True
181
197
 
@@ -185,49 +201,69 @@ class DynamicCameraManager:
185
201
 
186
202
  def update_camera(self, camera_data: Dict[str, Any]) -> bool:
187
203
  """Update an existing camera's configuration.
188
-
189
- This will restart the stream with new settings.
190
-
204
+
205
+ Handles isActive transitions:
206
+ - If camera becomes active: Start streaming
207
+ - If camera becomes inactive: Stop streaming but keep data
208
+ - If camera stays active: Restart with new config
209
+
191
210
  Args:
192
211
  camera_data: Updated camera configuration data
193
-
212
+
194
213
  Returns:
195
214
  bool: True if camera was updated successfully
196
215
  """
197
216
  camera_id = camera_data.get('id')
198
217
  camera_name = camera_data.get('cameraName', 'Unknown')
199
-
218
+ is_active = camera_data.get('isActive', True) # Default to True
219
+
200
220
  with self._lock:
201
221
  # Check if camera exists
202
222
  if camera_id not in self.cameras:
203
223
  self.logger.warning(f"Camera {camera_id} not found, adding instead")
204
224
  return self.add_camera(camera_data)
205
-
225
+
226
+ # Check if camera is currently streaming
227
+ is_currently_streaming = camera_id in self.camera_stream_keys
228
+
206
229
  try:
207
- # Get current stream key
208
- stream_key = self.camera_stream_keys.get(camera_id)
209
-
210
- if not stream_key:
211
- self.logger.error(f"No stream key found for camera {camera_id}")
212
- return False
213
-
230
+ # Case 1: Camera should become inactive (stop streaming)
231
+ if not is_active:
232
+ if is_currently_streaming:
233
+ # Clean up streaming gateway mappings
234
+ stream_key = self.camera_stream_keys.get(camera_id)
235
+ if stream_key and self.streaming_gateway:
236
+ self.streaming_gateway._stream_key_to_camera_id.pop(stream_key, None)
237
+ self.streaming_gateway._my_stream_keys.discard(stream_key)
238
+
239
+ # Remove stream key mapping
240
+ del self.camera_stream_keys[camera_id]
241
+ self.stats['active_cameras'] -= 1
242
+ self.logger.info(f"Stopped streaming for camera {camera_name} (now inactive)")
243
+
244
+ # Update stored camera data
245
+ self.cameras[camera_id] = camera_data
246
+ self.stats['cameras_updated'] += 1
247
+ self.logger.info(f"Camera {camera_name} updated (inactive)")
248
+ return True
249
+
250
+ # Case 2: Camera should be active
214
251
  # Create new input stream with updated data
215
252
  input_stream = self._create_input_stream(camera_data)
216
-
253
+
217
254
  if not input_stream:
218
255
  self.logger.error(f"Failed to create updated input stream for camera {camera_id}")
219
256
  return False
220
-
257
+
221
258
  # Register topic (may have changed) - generate default if not provided
222
259
  topic = input_stream.camera_input_topic
223
260
  if not topic:
224
- # Generate default topic name
225
261
  topic = f"{camera_id}_input_topic"
226
262
  self.logger.warning(f"No input topic for camera {camera_name}, using default: {topic}")
227
-
263
+
228
264
  self.camera_streamer.register_stream_topic(input_stream.camera_key, topic)
229
-
230
- # Restart streaming with new configuration
265
+
266
+ # Start/restart streaming with new configuration
231
267
  success = self.camera_streamer.start_background_stream(
232
268
  input=input_stream.source,
233
269
  fps=input_stream.fps,
@@ -239,21 +275,30 @@ class DynamicCameraManager:
239
275
  simulate_video_file_stream=input_stream.simulate_video_file_stream,
240
276
  camera_location=input_stream.camera_location,
241
277
  )
242
-
278
+
243
279
  if not success:
244
- self.logger.error(f"Failed to restart streaming for camera {camera_name}")
280
+ self.logger.error(f"Failed to start/restart streaming for camera {camera_name}")
245
281
  return False
246
-
282
+
247
283
  # Update stored camera data
248
284
  self.cameras[camera_id] = camera_data
249
285
  self.camera_stream_keys[camera_id] = input_stream.camera_key
250
-
286
+
287
+ # Update streaming gateway mappings for metrics
288
+ if self.streaming_gateway:
289
+ self.streaming_gateway._stream_key_to_camera_id[input_stream.camera_key] = camera_id
290
+ self.streaming_gateway._my_stream_keys.add(input_stream.camera_key)
291
+
251
292
  # Update statistics
293
+ if not is_currently_streaming:
294
+ self.stats['active_cameras'] += 1
295
+ self.logger.info(f"Started streaming for camera {camera_name} (now active)")
296
+ else:
297
+ self.logger.info(f"Restarted streaming for camera {camera_name}")
298
+
252
299
  self.stats['cameras_updated'] += 1
253
-
254
- self.logger.info(f"Successfully updated camera: {camera_name}")
255
300
  return True
256
-
301
+
257
302
  except Exception as e:
258
303
  self.logger.error(f"Error updating camera {camera_name}: {e}", exc_info=True)
259
304
  return False
@@ -275,8 +320,14 @@ class DynamicCameraManager:
275
320
  return False
276
321
 
277
322
  camera_name = camera_data.get('cameraName', 'Unknown')
278
-
279
- try:
323
+
324
+ try:
325
+ # Clean up streaming gateway mappings for metrics
326
+ stream_key = self.camera_stream_keys.get(camera_id)
327
+ if stream_key and self.streaming_gateway:
328
+ self.streaming_gateway._stream_key_to_camera_id.pop(stream_key, None)
329
+ self.streaming_gateway._my_stream_keys.discard(stream_key)
330
+
280
331
  # Remove from storage
281
332
  del self.cameras[camera_id]
282
333
  if camera_id in self.camera_stream_keys:
@@ -493,11 +544,15 @@ class DynamicCameraManager:
493
544
  simulate_video = True
494
545
 
495
546
  # Try to get signed URL if we have gateway_util
496
- if self.gateway_util and source:
547
+ # Always fetch presigned URL for FILE cameras - the API uses camera_id,
548
+ # not the local simulationVideoPath, to look up and sign the URL
549
+ if self.gateway_util:
497
550
  try:
498
551
  stream_url_data = self.gateway_util.get_simulated_stream_url(camera_id)
499
552
  if stream_url_data and stream_url_data.get('url'):
500
553
  source = stream_url_data['url']
554
+ else:
555
+ self.logger.warning(f"No signed URL returned for FILE camera {camera_id}, using raw path")
501
556
  except Exception as e:
502
557
  self.logger.warning(f"Failed to get signed URL for camera {camera_id}: {e}")
503
558
 
@@ -524,8 +579,570 @@ class DynamicCameraManager:
524
579
  )
525
580
 
526
581
  return input_stream
527
-
582
+
528
583
  except Exception as e:
529
584
  self.logger.error(f"Error creating input stream: {e}", exc_info=True)
530
585
  return None
531
586
 
587
+
588
+ class DynamicCameraManagerForWorkers:
589
+ """Dynamic camera manager for WorkerManager-based async flow.
590
+
591
+ This class adapts the DynamicCameraManager interface to work with
592
+ the new WorkerManager + AsyncCameraWorker multiprocessing architecture.
593
+
594
+ It converts event camera_data to worker camera_config format and routes
595
+ add/update/remove operations to the WorkerManager.
596
+ """
597
+
598
+ def __init__(
599
+ self,
600
+ worker_manager,
601
+ streaming_gateway_id: str,
602
+ session=None,
603
+ streaming_gateway=None
604
+ ):
605
+ """Initialize dynamic camera manager for workers.
606
+
607
+ Args:
608
+ worker_manager: WorkerManager instance to control streams
609
+ streaming_gateway_id: ID of the streaming gateway
610
+ session: Session object for API calls (optional)
611
+ streaming_gateway: StreamingGateway instance for updating mappings (optional)
612
+ """
613
+ self.worker_manager = worker_manager
614
+ self.streaming_gateway_id = streaming_gateway_id
615
+ self.session = session
616
+ self.streaming_gateway = streaming_gateway
617
+
618
+ # Initialize gateway util for API calls
619
+ self.gateway_util = None
620
+ if session and streaming_gateway_id:
621
+ try:
622
+ self.gateway_util = StreamingGatewayUtil(session, streaming_gateway_id)
623
+ except Exception as e:
624
+ logging.warning(f"Could not initialize StreamingGatewayUtil: {e}")
625
+
626
+ # Camera storage (same as original manager)
627
+ self.cameras: Dict[str, Dict[str, Any]] = {} # camera_id -> camera_data
628
+ self.camera_groups: Dict[str, Dict[str, Any]] = {} # group_id -> group_data
629
+ self.camera_topics: Dict[str, Dict[str, str]] = {} # camera_id -> {input, output}
630
+ self.camera_stream_keys: Dict[str, str] = {} # camera_id -> stream_key
631
+
632
+ # Lock for thread-safe operations
633
+ self._lock = threading.RLock()
634
+
635
+ # Statistics
636
+ self.stats = {
637
+ 'cameras_added': 0,
638
+ 'cameras_updated': 0,
639
+ 'cameras_removed': 0,
640
+ 'active_cameras': 0,
641
+ }
642
+
643
+ self.logger = logging.getLogger(__name__)
644
+ self.logger.info(f"DynamicCameraManagerForWorkers initialized for gateway {streaming_gateway_id}")
645
+
646
+ def initialize_from_config(self, input_streams: list):
647
+ """Initialize with existing input stream configurations.
648
+
649
+ For WorkerManager flow, cameras are already started by WorkerManager,
650
+ so this just populates the local tracking data.
651
+
652
+ Args:
653
+ input_streams: List of InputStream objects
654
+ """
655
+ with self._lock:
656
+ # Fetch and cache camera groups if we have gateway_util
657
+ if self.gateway_util:
658
+ try:
659
+ all_groups = self.gateway_util.get_camera_groups()
660
+ if all_groups:
661
+ for group in all_groups:
662
+ self.camera_groups[group['id']] = group
663
+ self.logger.info(f"Cached {len(all_groups)} camera groups")
664
+ except Exception as e:
665
+ self.logger.warning(f"Failed to fetch camera groups during init: {e}")
666
+
667
+ # Initialize cameras from input streams (tracking only)
668
+ for input_stream in input_streams:
669
+ camera_id = input_stream.camera_id
670
+
671
+ # Store camera data
672
+ self.cameras[camera_id] = {
673
+ 'id': camera_id,
674
+ 'cameraName': input_stream.camera_key,
675
+ 'cameraGroupId': input_stream.camera_group_key,
676
+ 'source': input_stream.source,
677
+ 'fps': input_stream.fps,
678
+ 'quality': input_stream.quality,
679
+ 'width': input_stream.width,
680
+ 'height': input_stream.height,
681
+ 'camera_location': input_stream.camera_location,
682
+ 'simulate_video_file_stream': input_stream.simulate_video_file_stream,
683
+ }
684
+
685
+ # Store topic mapping
686
+ self.camera_topics[camera_id] = {
687
+ 'input': input_stream.camera_input_topic,
688
+ 'output': None,
689
+ }
690
+
691
+ # Store stream key mapping
692
+ self.camera_stream_keys[camera_id] = input_stream.camera_key
693
+
694
+ self.stats['active_cameras'] += 1
695
+
696
+ self.logger.info(f"Initialized with {len(input_streams)} cameras (tracking only)")
697
+
698
+ def add_camera(self, camera_data: Dict[str, Any]) -> bool:
699
+ """Add a new camera via WorkerManager.
700
+
701
+ Args:
702
+ camera_data: Camera configuration data from event
703
+
704
+ Returns:
705
+ bool: True if camera was added successfully
706
+ """
707
+ camera_id = camera_data.get('id')
708
+ camera_name = camera_data.get('cameraName', 'Unknown')
709
+ is_active = camera_data.get('isActive', True)
710
+
711
+ with self._lock:
712
+ # Check if camera already exists
713
+ if camera_id in self.cameras:
714
+ self.logger.warning(f"Camera {camera_id} already exists, updating instead")
715
+ return self.update_camera(camera_data)
716
+
717
+ # If camera is not active (scheduled), just store data without streaming
718
+ if not is_active:
719
+ self.cameras[camera_id] = camera_data
720
+ self.logger.info(f"Camera {camera_name} added but not active (scheduled)")
721
+ self.stats['cameras_added'] += 1
722
+ return True
723
+
724
+ try:
725
+ # Create worker camera config from camera data
726
+ worker_config = self._create_worker_camera_config(camera_data)
727
+
728
+ if not worker_config:
729
+ self.logger.error(f"Failed to create worker config for camera {camera_id}")
730
+ return False
731
+
732
+ # Add via WorkerManager
733
+ if not self.worker_manager.add_camera(worker_config):
734
+ self.logger.error(f"WorkerManager failed to add camera {camera_name}")
735
+ return False
736
+
737
+ # Store camera data
738
+ self.cameras[camera_id] = camera_data
739
+ self.camera_stream_keys[camera_id] = worker_config['stream_key']
740
+ self.camera_topics[camera_id] = {
741
+ 'input': worker_config['topic'],
742
+ 'output': None,
743
+ }
744
+
745
+ # Update streaming gateway mappings for metrics
746
+ if self.streaming_gateway:
747
+ self.streaming_gateway._stream_key_to_camera_id[worker_config['stream_key']] = camera_id
748
+ self.streaming_gateway._my_stream_keys.add(worker_config['stream_key'])
749
+
750
+ # Update statistics
751
+ self.stats['cameras_added'] += 1
752
+ self.stats['active_cameras'] += 1
753
+
754
+ self.logger.info(f"Successfully added camera via WorkerManager: {camera_name}")
755
+ return True
756
+
757
+ except Exception as e:
758
+ self.logger.error(f"Error adding camera {camera_name}: {e}", exc_info=True)
759
+ return False
760
+
761
+ def update_camera(self, camera_data: Dict[str, Any]) -> bool:
762
+ """Update an existing camera's configuration via WorkerManager.
763
+
764
+ Args:
765
+ camera_data: Updated camera configuration data
766
+
767
+ Returns:
768
+ bool: True if camera was updated successfully
769
+ """
770
+ camera_id = camera_data.get('id')
771
+ camera_name = camera_data.get('cameraName', 'Unknown')
772
+ is_active = camera_data.get('isActive', True)
773
+
774
+ with self._lock:
775
+ # Check if camera exists
776
+ if camera_id not in self.cameras:
777
+ self.logger.warning(f"Camera {camera_id} not found, adding instead")
778
+ return self.add_camera(camera_data)
779
+
780
+ # Check if camera is currently streaming
781
+ stream_key = self.camera_stream_keys.get(camera_id)
782
+ is_currently_streaming = stream_key is not None
783
+
784
+ try:
785
+ # Case 1: Camera should become inactive (stop streaming)
786
+ if not is_active:
787
+ if is_currently_streaming:
788
+ # Remove via WorkerManager
789
+ self.worker_manager.remove_camera(stream_key)
790
+
791
+ # Clean up streaming gateway mappings
792
+ if self.streaming_gateway:
793
+ self.streaming_gateway._stream_key_to_camera_id.pop(stream_key, None)
794
+ self.streaming_gateway._my_stream_keys.discard(stream_key)
795
+
796
+ # Remove stream key mapping
797
+ del self.camera_stream_keys[camera_id]
798
+ self.stats['active_cameras'] -= 1
799
+ self.logger.info(f"Stopped streaming for camera {camera_name} (now inactive)")
800
+
801
+ # Update stored camera data
802
+ self.cameras[camera_id] = camera_data
803
+ self.stats['cameras_updated'] += 1
804
+ self.logger.info(f"Camera {camera_name} updated (inactive)")
805
+ return True
806
+
807
+ # Case 2: Camera should be active
808
+ # Create worker config with updated data
809
+ worker_config = self._create_worker_camera_config(camera_data)
810
+
811
+ if not worker_config:
812
+ self.logger.error(f"Failed to create updated worker config for camera {camera_id}")
813
+ return False
814
+
815
+ # Update via WorkerManager
816
+ if not self.worker_manager.update_camera(worker_config):
817
+ self.logger.error(f"WorkerManager failed to update camera {camera_name}")
818
+ return False
819
+
820
+ # Update stored camera data
821
+ self.cameras[camera_id] = camera_data
822
+ self.camera_stream_keys[camera_id] = worker_config['stream_key']
823
+ self.camera_topics[camera_id] = {
824
+ 'input': worker_config['topic'],
825
+ 'output': None,
826
+ }
827
+
828
+ # Update streaming gateway mappings for metrics
829
+ if self.streaming_gateway:
830
+ self.streaming_gateway._stream_key_to_camera_id[worker_config['stream_key']] = camera_id
831
+ self.streaming_gateway._my_stream_keys.add(worker_config['stream_key'])
832
+
833
+ # Update statistics
834
+ if not is_currently_streaming:
835
+ self.stats['active_cameras'] += 1
836
+ self.logger.info(f"Started streaming for camera {camera_name} (now active)")
837
+ else:
838
+ self.logger.info(f"Restarted streaming for camera {camera_name}")
839
+
840
+ self.stats['cameras_updated'] += 1
841
+ return True
842
+
843
+ except Exception as e:
844
+ self.logger.error(f"Error updating camera {camera_name}: {e}", exc_info=True)
845
+ return False
846
+
847
+ def remove_camera(self, camera_id: str) -> bool:
848
+ """Remove a camera via WorkerManager.
849
+
850
+ Args:
851
+ camera_id: ID of camera to remove
852
+
853
+ Returns:
854
+ bool: True if camera was removed successfully
855
+ """
856
+ with self._lock:
857
+ camera_data = self.cameras.get(camera_id)
858
+
859
+ if not camera_data:
860
+ self.logger.warning(f"Camera {camera_id} not found")
861
+ return False
862
+
863
+ camera_name = camera_data.get('cameraName', 'Unknown')
864
+
865
+ try:
866
+ # Get stream key
867
+ stream_key = self.camera_stream_keys.get(camera_id)
868
+
869
+ if stream_key:
870
+ # Remove via WorkerManager
871
+ if not self.worker_manager.remove_camera(stream_key):
872
+ self.logger.error(f"WorkerManager failed to remove camera {camera_name}")
873
+ return False
874
+
875
+ # Clean up streaming gateway mappings
876
+ if self.streaming_gateway:
877
+ self.streaming_gateway._stream_key_to_camera_id.pop(stream_key, None)
878
+ self.streaming_gateway._my_stream_keys.discard(stream_key)
879
+
880
+ # Remove from storage
881
+ del self.cameras[camera_id]
882
+ if camera_id in self.camera_stream_keys:
883
+ del self.camera_stream_keys[camera_id]
884
+ if camera_id in self.camera_topics:
885
+ del self.camera_topics[camera_id]
886
+
887
+ # Update statistics
888
+ self.stats['cameras_removed'] += 1
889
+ self.stats['active_cameras'] -= 1
890
+
891
+ self.logger.info(f"Successfully removed camera via WorkerManager: {camera_name}")
892
+ return True
893
+
894
+ except Exception as e:
895
+ self.logger.error(f"Error removing camera {camera_name}: {e}", exc_info=True)
896
+ return False
897
+
898
+ def update_camera_group(self, group_data: Dict[str, Any]):
899
+ """Update camera group information.
900
+
901
+ Args:
902
+ group_data: Camera group data
903
+ """
904
+ group_id = group_data.get('id')
905
+
906
+ with self._lock:
907
+ self.camera_groups[group_id] = group_data
908
+ self.logger.info(f"Updated camera group: {group_data.get('cameraGroupName')}")
909
+
910
+ def remove_camera_group(self, group_id: str):
911
+ """Remove camera group information.
912
+
913
+ Args:
914
+ group_id: ID of camera group to remove
915
+ """
916
+ with self._lock:
917
+ if group_id in self.camera_groups:
918
+ del self.camera_groups[group_id]
919
+ self.logger.info(f"Removed camera group: {group_id}")
920
+
921
+ def update_cameras_in_group(self, group_id: str, group_data: Dict[str, Any]):
922
+ """Update all cameras in a group with new default settings.
923
+
924
+ For WorkerManager flow, this updates cameras via the WorkerManager.
925
+
926
+ Args:
927
+ group_id: Camera group ID
928
+ group_data: Updated group data with new default settings
929
+ """
930
+ default_settings = group_data.get('defaultStreamSettings', {})
931
+
932
+ with self._lock:
933
+ # Find all cameras in this group
934
+ cameras_to_update = [
935
+ camera_id for camera_id, camera_data in self.cameras.items()
936
+ if camera_data.get('cameraGroupId') == group_id
937
+ ]
938
+
939
+ self.logger.info(
940
+ f"Updating {len(cameras_to_update)} cameras in group {group_id} "
941
+ f"with new default settings"
942
+ )
943
+
944
+ # Update each camera
945
+ for camera_id in cameras_to_update:
946
+ camera_data = self.cameras[camera_id].copy()
947
+
948
+ # Merge default settings (only if camera doesn't have custom settings)
949
+ custom_settings = camera_data.get('customStreamSettings', {})
950
+ for key, value in default_settings.items():
951
+ if key not in custom_settings:
952
+ camera_data[key] = value
953
+
954
+ # Update camera via worker manager
955
+ self.update_camera(camera_data)
956
+
957
+ def update_camera_input_topic(self, camera_id: str, topic_name: Optional[str]):
958
+ """Update input topic for a camera.
959
+
960
+ For WorkerManager flow, topics are managed within the worker config,
961
+ so this triggers a camera update.
962
+
963
+ Args:
964
+ camera_id: Camera ID
965
+ topic_name: New topic name (None to remove)
966
+ """
967
+ with self._lock:
968
+ if camera_id not in self.camera_topics:
969
+ self.camera_topics[camera_id] = {'input': None, 'output': None}
970
+
971
+ self.camera_topics[camera_id]['input'] = topic_name
972
+ self.logger.info(f"Updated input topic for camera {camera_id}: {topic_name}")
973
+
974
+ # If camera exists and is streaming, update it with new topic
975
+ if camera_id in self.cameras and camera_id in self.camera_stream_keys:
976
+ camera_data = self.cameras[camera_id].copy()
977
+ self.update_camera(camera_data)
978
+
979
+ def update_camera_output_topic(self, camera_id: str, topic_name: Optional[str]):
980
+ """Update output topic for a camera.
981
+
982
+ Args:
983
+ camera_id: Camera ID
984
+ topic_name: New topic name (None to remove)
985
+ """
986
+ with self._lock:
987
+ if camera_id not in self.camera_topics:
988
+ self.camera_topics[camera_id] = {'input': None, 'output': None}
989
+
990
+ self.camera_topics[camera_id]['output'] = topic_name
991
+
992
+ self.logger.info(f"Updated output topic for camera {camera_id}: {topic_name}")
993
+
994
+ def get_statistics(self) -> Dict[str, Any]:
995
+ """Get camera manager statistics.
996
+
997
+ Returns:
998
+ Dict with statistics
999
+ """
1000
+ with self._lock:
1001
+ stats = {
1002
+ **self.stats,
1003
+ 'camera_ids': list(self.cameras.keys()),
1004
+ 'camera_groups': len(self.camera_groups),
1005
+ }
1006
+
1007
+ # Add worker statistics if available
1008
+ if self.worker_manager:
1009
+ try:
1010
+ stats['worker_stats'] = self.worker_manager.get_worker_statistics()
1011
+ except Exception as e:
1012
+ self.logger.warning(f"Failed to get worker statistics: {e}")
1013
+
1014
+ return stats
1015
+
1016
+ def _fetch_camera_group(self, camera_group_id: str) -> Optional[Dict[str, Any]]:
1017
+ """Fetch camera group from API if not in cache.
1018
+
1019
+ Args:
1020
+ camera_group_id: Camera group ID
1021
+
1022
+ Returns:
1023
+ Camera group data or None
1024
+ """
1025
+ # Check cache first
1026
+ if camera_group_id in self.camera_groups:
1027
+ return self.camera_groups[camera_group_id]
1028
+
1029
+ # Fetch from API if we have gateway_util
1030
+ if self.gateway_util:
1031
+ try:
1032
+ all_groups = self.gateway_util.get_camera_groups()
1033
+ if all_groups:
1034
+ # Cache all groups
1035
+ for group in all_groups:
1036
+ self.camera_groups[group['id']] = group
1037
+
1038
+ # Return requested group
1039
+ return self.camera_groups.get(camera_group_id)
1040
+ except Exception as e:
1041
+ self.logger.warning(f"Failed to fetch camera groups: {e}")
1042
+
1043
+ return None
1044
+
1045
+ def _fetch_input_topic(self, camera_id: str) -> Optional[str]:
1046
+ """Fetch input topic for camera from API if not in cache.
1047
+
1048
+ Args:
1049
+ camera_id: Camera ID
1050
+
1051
+ Returns:
1052
+ Input topic name or None
1053
+ """
1054
+ # Check cache first
1055
+ if camera_id in self.camera_topics:
1056
+ return self.camera_topics[camera_id].get('input')
1057
+
1058
+ # Fetch from API if we have gateway_util
1059
+ if self.gateway_util:
1060
+ try:
1061
+ all_topics = self.gateway_util.get_streaming_input_topics()
1062
+ if all_topics:
1063
+ # Cache all topics
1064
+ for topic in all_topics:
1065
+ topic_camera_id = topic.get('cameraId')
1066
+ if topic_camera_id:
1067
+ if topic_camera_id not in self.camera_topics:
1068
+ self.camera_topics[topic_camera_id] = {'input': None, 'output': None}
1069
+ self.camera_topics[topic_camera_id]['input'] = topic.get('topicName')
1070
+
1071
+ # Return requested topic
1072
+ return self.camera_topics.get(camera_id, {}).get('input')
1073
+ except Exception as e:
1074
+ self.logger.warning(f"Failed to fetch input topics: {e}")
1075
+
1076
+ return None
1077
+
1078
+ def _create_worker_camera_config(self, camera_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
1079
+ """Create camera config dict for WorkerManager from camera data.
1080
+
1081
+ Converts the event camera_data format to the format expected by
1082
+ AsyncCameraWorker.
1083
+
1084
+ Args:
1085
+ camera_data: Camera configuration data from event
1086
+
1087
+ Returns:
1088
+ Dict compatible with WorkerManager/AsyncCameraWorker or None if failed
1089
+ """
1090
+ try:
1091
+ camera_id = camera_data.get('id')
1092
+ camera_group_id = camera_data.get('cameraGroupId')
1093
+
1094
+ # Get camera group for default settings (fetch if needed)
1095
+ camera_group = self._fetch_camera_group(camera_group_id) or {}
1096
+ default_settings = camera_group.get('defaultStreamSettings', {})
1097
+ custom_settings = camera_data.get('customStreamSettings', {})
1098
+
1099
+ # Merge settings (custom overrides default)
1100
+ settings = {**default_settings, **custom_settings}
1101
+
1102
+ # Determine source
1103
+ source = camera_data.get('cameraFeedPath', '')
1104
+ simulate_video = False
1105
+
1106
+ if camera_data.get('protocolType') == 'FILE':
1107
+ # Use simulation video path for file type
1108
+ source = camera_data.get('simulationVideoPath', '')
1109
+ simulate_video = True
1110
+
1111
+ # Try to get signed URL if we have gateway_util
1112
+ # Always fetch presigned URL for FILE cameras - the API uses camera_id,
1113
+ # not the local simulationVideoPath, to look up and sign the URL
1114
+ if self.gateway_util:
1115
+ try:
1116
+ stream_url_data = self.gateway_util.get_simulated_stream_url(camera_id)
1117
+ if stream_url_data and stream_url_data.get('url'):
1118
+ source = stream_url_data['url']
1119
+ else:
1120
+ self.logger.warning(f"No signed URL returned for FILE camera {camera_id}, using raw path")
1121
+ except Exception as e:
1122
+ self.logger.warning(f"Failed to get signed URL for camera {camera_id}: {e}")
1123
+
1124
+ # Get input topic (fetch if needed)
1125
+ input_topic = self._fetch_input_topic(camera_id)
1126
+ if not input_topic:
1127
+ input_topic = f"{camera_id}_input_topic"
1128
+ self.logger.warning(f"No input topic found for camera {camera_id}, using default: {input_topic}")
1129
+
1130
+ # Build worker config
1131
+ return {
1132
+ 'stream_key': camera_data.get('cameraName', f'Camera_{camera_id}'),
1133
+ 'stream_group_key': camera_group.get('cameraGroupName', 'Unknown Group'),
1134
+ 'camera_id': camera_id,
1135
+ 'source': source,
1136
+ 'topic': input_topic,
1137
+ 'fps': settings.get('streamingFPS', 10),
1138
+ 'quality': settings.get('videoQuality', 100),
1139
+ 'width': settings.get('width', 640),
1140
+ 'height': settings.get('height', 480),
1141
+ 'camera_location': camera_group.get('locationId', 'Unknown Location'),
1142
+ 'simulate_video_file_stream': simulate_video,
1143
+ }
1144
+
1145
+ except Exception as e:
1146
+ self.logger.error(f"Error creating worker camera config: {e}", exc_info=True)
1147
+ return None
1148
+