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.
- matrice_streaming/__init__.py +44 -32
- matrice_streaming/streaming_gateway/camera_streamer/__init__.py +68 -1
- matrice_streaming/streaming_gateway/camera_streamer/async_camera_worker.py +1388 -0
- matrice_streaming/streaming_gateway/camera_streamer/async_ffmpeg_worker.py +966 -0
- matrice_streaming/streaming_gateway/camera_streamer/camera_streamer.py +188 -24
- matrice_streaming/streaming_gateway/camera_streamer/device_detection.py +507 -0
- matrice_streaming/streaming_gateway/camera_streamer/encoding_pool_manager.py +136 -0
- matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_camera_streamer.py +1048 -0
- matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_config.py +192 -0
- matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_worker_manager.py +470 -0
- matrice_streaming/streaming_gateway/camera_streamer/gstreamer_camera_streamer.py +1368 -0
- matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker.py +1063 -0
- matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker_manager.py +546 -0
- matrice_streaming/streaming_gateway/camera_streamer/message_builder.py +60 -15
- matrice_streaming/streaming_gateway/camera_streamer/nvdec.py +1330 -0
- matrice_streaming/streaming_gateway/camera_streamer/nvdec_worker_manager.py +412 -0
- matrice_streaming/streaming_gateway/camera_streamer/platform_pipelines.py +680 -0
- matrice_streaming/streaming_gateway/camera_streamer/stream_statistics.py +111 -4
- matrice_streaming/streaming_gateway/camera_streamer/video_capture_manager.py +223 -27
- matrice_streaming/streaming_gateway/camera_streamer/worker_manager.py +694 -0
- matrice_streaming/streaming_gateway/debug/__init__.py +27 -2
- matrice_streaming/streaming_gateway/debug/benchmark.py +727 -0
- matrice_streaming/streaming_gateway/debug/debug_gstreamer_gateway.py +599 -0
- matrice_streaming/streaming_gateway/debug/debug_streaming_gateway.py +245 -95
- matrice_streaming/streaming_gateway/debug/debug_utils.py +29 -0
- matrice_streaming/streaming_gateway/debug/test_videoplayback.py +318 -0
- matrice_streaming/streaming_gateway/dynamic_camera_manager.py +656 -39
- matrice_streaming/streaming_gateway/metrics_reporter.py +676 -139
- matrice_streaming/streaming_gateway/streaming_action.py +71 -20
- matrice_streaming/streaming_gateway/streaming_gateway.py +1026 -78
- matrice_streaming/streaming_gateway/streaming_gateway_utils.py +175 -20
- matrice_streaming/streaming_gateway/streaming_status_listener.py +89 -0
- {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/METADATA +1 -1
- matrice_streaming-0.1.65.dist-info/RECORD +56 -0
- matrice_streaming-0.1.14.dist-info/RECORD +0 -38
- {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/WHEEL +0 -0
- {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|
-
|
|
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
|
-
#
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
+
|