nedo-vision-worker-core 0.2.0__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.

Potentially problematic release.


This version of nedo-vision-worker-core might be problematic. Click here for more details.

Files changed (95) hide show
  1. nedo_vision_worker_core/__init__.py +23 -0
  2. nedo_vision_worker_core/ai/FrameDrawer.py +144 -0
  3. nedo_vision_worker_core/ai/ImageDebugger.py +126 -0
  4. nedo_vision_worker_core/ai/VideoDebugger.py +69 -0
  5. nedo_vision_worker_core/ai/__init__.py +1 -0
  6. nedo_vision_worker_core/cli.py +197 -0
  7. nedo_vision_worker_core/config/ConfigurationManager.py +173 -0
  8. nedo_vision_worker_core/config/__init__.py +1 -0
  9. nedo_vision_worker_core/core_service.py +237 -0
  10. nedo_vision_worker_core/database/DatabaseManager.py +236 -0
  11. nedo_vision_worker_core/database/__init__.py +1 -0
  12. nedo_vision_worker_core/detection/BaseDetector.py +22 -0
  13. nedo_vision_worker_core/detection/DetectionManager.py +83 -0
  14. nedo_vision_worker_core/detection/RFDETRDetector.py +62 -0
  15. nedo_vision_worker_core/detection/YOLODetector.py +57 -0
  16. nedo_vision_worker_core/detection/__init__.py +1 -0
  17. nedo_vision_worker_core/detection/detection_processing/DetectionProcessor.py +29 -0
  18. nedo_vision_worker_core/detection/detection_processing/HumanDetectionProcessor.py +47 -0
  19. nedo_vision_worker_core/detection/detection_processing/PPEDetectionProcessor.py +44 -0
  20. nedo_vision_worker_core/detection/detection_processing/__init__.py +1 -0
  21. nedo_vision_worker_core/doctor.py +342 -0
  22. nedo_vision_worker_core/drawing_assets/blue/inner_corner.png +0 -0
  23. nedo_vision_worker_core/drawing_assets/blue/inner_frame.png +0 -0
  24. nedo_vision_worker_core/drawing_assets/blue/line.png +0 -0
  25. nedo_vision_worker_core/drawing_assets/blue/top_left.png +0 -0
  26. nedo_vision_worker_core/drawing_assets/blue/top_right.png +0 -0
  27. nedo_vision_worker_core/drawing_assets/red/inner_corner.png +0 -0
  28. nedo_vision_worker_core/drawing_assets/red/inner_frame.png +0 -0
  29. nedo_vision_worker_core/drawing_assets/red/line.png +0 -0
  30. nedo_vision_worker_core/drawing_assets/red/top_left.png +0 -0
  31. nedo_vision_worker_core/drawing_assets/red/top_right.png +0 -0
  32. nedo_vision_worker_core/icons/boots-green.png +0 -0
  33. nedo_vision_worker_core/icons/boots-red.png +0 -0
  34. nedo_vision_worker_core/icons/gloves-green.png +0 -0
  35. nedo_vision_worker_core/icons/gloves-red.png +0 -0
  36. nedo_vision_worker_core/icons/goggles-green.png +0 -0
  37. nedo_vision_worker_core/icons/goggles-red.png +0 -0
  38. nedo_vision_worker_core/icons/helmet-green.png +0 -0
  39. nedo_vision_worker_core/icons/helmet-red.png +0 -0
  40. nedo_vision_worker_core/icons/mask-red.png +0 -0
  41. nedo_vision_worker_core/icons/vest-green.png +0 -0
  42. nedo_vision_worker_core/icons/vest-red.png +0 -0
  43. nedo_vision_worker_core/models/__init__.py +20 -0
  44. nedo_vision_worker_core/models/ai_model.py +41 -0
  45. nedo_vision_worker_core/models/auth.py +14 -0
  46. nedo_vision_worker_core/models/config.py +9 -0
  47. nedo_vision_worker_core/models/dataset_source.py +30 -0
  48. nedo_vision_worker_core/models/logs.py +9 -0
  49. nedo_vision_worker_core/models/ppe_detection.py +39 -0
  50. nedo_vision_worker_core/models/ppe_detection_label.py +20 -0
  51. nedo_vision_worker_core/models/restricted_area_violation.py +20 -0
  52. nedo_vision_worker_core/models/user.py +10 -0
  53. nedo_vision_worker_core/models/worker_source.py +19 -0
  54. nedo_vision_worker_core/models/worker_source_pipeline.py +21 -0
  55. nedo_vision_worker_core/models/worker_source_pipeline_config.py +24 -0
  56. nedo_vision_worker_core/models/worker_source_pipeline_debug.py +15 -0
  57. nedo_vision_worker_core/models/worker_source_pipeline_detection.py +14 -0
  58. nedo_vision_worker_core/pipeline/PipelineConfigManager.py +32 -0
  59. nedo_vision_worker_core/pipeline/PipelineManager.py +133 -0
  60. nedo_vision_worker_core/pipeline/PipelinePrepocessor.py +40 -0
  61. nedo_vision_worker_core/pipeline/PipelineProcessor.py +338 -0
  62. nedo_vision_worker_core/pipeline/PipelineSyncThread.py +202 -0
  63. nedo_vision_worker_core/pipeline/__init__.py +1 -0
  64. nedo_vision_worker_core/preprocessing/ImageResizer.py +42 -0
  65. nedo_vision_worker_core/preprocessing/ImageRoi.py +61 -0
  66. nedo_vision_worker_core/preprocessing/Preprocessor.py +16 -0
  67. nedo_vision_worker_core/preprocessing/__init__.py +1 -0
  68. nedo_vision_worker_core/repositories/AIModelRepository.py +31 -0
  69. nedo_vision_worker_core/repositories/PPEDetectionRepository.py +146 -0
  70. nedo_vision_worker_core/repositories/RestrictedAreaRepository.py +90 -0
  71. nedo_vision_worker_core/repositories/WorkerSourcePipelineDebugRepository.py +81 -0
  72. nedo_vision_worker_core/repositories/WorkerSourcePipelineDetectionRepository.py +71 -0
  73. nedo_vision_worker_core/repositories/WorkerSourcePipelineRepository.py +79 -0
  74. nedo_vision_worker_core/repositories/WorkerSourceRepository.py +19 -0
  75. nedo_vision_worker_core/repositories/__init__.py +1 -0
  76. nedo_vision_worker_core/streams/RTMPStreamer.py +146 -0
  77. nedo_vision_worker_core/streams/StreamSyncThread.py +66 -0
  78. nedo_vision_worker_core/streams/VideoStream.py +324 -0
  79. nedo_vision_worker_core/streams/VideoStreamManager.py +121 -0
  80. nedo_vision_worker_core/streams/__init__.py +1 -0
  81. nedo_vision_worker_core/tracker/SFSORT.py +325 -0
  82. nedo_vision_worker_core/tracker/TrackerManager.py +163 -0
  83. nedo_vision_worker_core/tracker/__init__.py +1 -0
  84. nedo_vision_worker_core/util/BoundingBoxMetrics.py +53 -0
  85. nedo_vision_worker_core/util/DrawingUtils.py +354 -0
  86. nedo_vision_worker_core/util/ModelReadinessChecker.py +188 -0
  87. nedo_vision_worker_core/util/PersonAttributeMatcher.py +70 -0
  88. nedo_vision_worker_core/util/PersonRestrictedAreaMatcher.py +45 -0
  89. nedo_vision_worker_core/util/TablePrinter.py +28 -0
  90. nedo_vision_worker_core/util/__init__.py +1 -0
  91. nedo_vision_worker_core-0.2.0.dist-info/METADATA +347 -0
  92. nedo_vision_worker_core-0.2.0.dist-info/RECORD +95 -0
  93. nedo_vision_worker_core-0.2.0.dist-info/WHEEL +5 -0
  94. nedo_vision_worker_core-0.2.0.dist-info/entry_points.txt +2 -0
  95. nedo_vision_worker_core-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,66 @@
1
+ import logging
2
+ import os
3
+ import time
4
+ import threading
5
+ from ..database.DatabaseManager import DatabaseManager
6
+ from ..repositories.WorkerSourceRepository import WorkerSourceRepository
7
+ from .VideoStreamManager import VideoStreamManager
8
+
9
+ class StreamSyncThread(threading.Thread):
10
+ """Thread responsible for synchronizing video streams from the database in real-time."""
11
+
12
+ def __init__(self, manager: VideoStreamManager, polling_interval=5):
13
+ super().__init__() # Set as a daemon so it stops with the main process
14
+
15
+ self.source_file_path = DatabaseManager.STORAGE_PATHS["files"] / "source_files"
16
+
17
+ self.manager = manager
18
+ self.polling_interval = polling_interval
19
+ self.worker_source_repo = WorkerSourceRepository()
20
+ self.running = True
21
+
22
+ def _get_source_file_path(self, file):
23
+ """Returns the file path for a given source file."""
24
+ return self.source_file_path / os.path.basename(file)
25
+
26
+ def run(self):
27
+ """Continuously updates the VideoStreamManager with database changes."""
28
+ while self.running:
29
+ try:
30
+ sources = self.worker_source_repo.get_worker_sources()
31
+ db_sources = {
32
+ source.id: (source.url if source.type_code == "live" else self._get_source_file_path(source.file_path), source.status_code) for source in sources
33
+ } # Store latest sources
34
+ active_stream_ids = set(self.manager.get_active_stream_ids())
35
+
36
+ # **1️⃣ Add new streams**
37
+ for source_id, (url, status_code) in db_sources.items():
38
+ if source_id not in active_stream_ids and status_code == "connected":
39
+ logging.info(f"🟢 Adding new stream: {source_id} ({url})")
40
+ self.manager.add_stream(source_id, url)
41
+
42
+ # **2️⃣ Remove deleted streams**
43
+ for stream_id in active_stream_ids:
44
+ if stream_id not in db_sources or db_sources[stream_id][1] != "connected":
45
+ logging.info(f"🔴 Removing deleted stream: {stream_id}")
46
+ self.manager.remove_stream(stream_id)
47
+
48
+ active_stream_ids = set(self.manager.get_active_stream_ids())
49
+
50
+ # **3️⃣ Update streams if URL has changed**
51
+ for source_id, (url, status_code) in db_sources.items():
52
+ if source_id in active_stream_ids:
53
+ existing_url = self.manager.get_stream_url(source_id)
54
+ if existing_url != url:
55
+ logging.info(f"🟡 Updating stream {source_id}: New URL {url}")
56
+ self.manager.remove_stream(source_id)
57
+ self.manager.add_stream(source_id, url)
58
+
59
+ except Exception as e:
60
+ logging.error(f"⚠️ Error syncing streams from database: {e}")
61
+
62
+ time.sleep(self.polling_interval) # Poll every X seconds
63
+
64
+ def stop(self):
65
+ """Stops the synchronization thread."""
66
+ self.running = False
@@ -0,0 +1,324 @@
1
+ import os
2
+ import cv2
3
+ import threading
4
+ import time
5
+ import logging
6
+ from typing import Optional
7
+
8
+
9
+ class VideoStream(threading.Thread):
10
+ """Threaded class for capturing video from a source, with automatic reconnection.
11
+
12
+ This class provides a thread-safe way to capture video frames from various sources
13
+ (cameras, files, network streams) with automatic reconnection on failure.
14
+
15
+ Attributes:
16
+ source: The video source (int for webcams, string for files/URLs)
17
+ reconnect_interval: Time in seconds to wait before reconnecting on failure
18
+ retry_limit: Maximum number of consecutive retries before giving up
19
+ """
20
+ def __init__(
21
+ self,
22
+ source: str,
23
+ reconnect_interval: int = 5,
24
+ retry_limit: int = 5,
25
+ max_reconnect_attempts: int = 10,
26
+ reconnect_backoff_factor: float = 1.5
27
+ ):
28
+ super().__init__()
29
+
30
+ # Stream configuration
31
+ self.source = source
32
+ self.reconnect_interval = reconnect_interval
33
+ self.retry_limit = retry_limit
34
+
35
+ # Stream state
36
+ self.capture = None
37
+ self.running = True
38
+ self.connected = False
39
+ self.start_time = time.time()
40
+ self.is_file = isinstance(source, (int, str, bytes, os.PathLike)) and os.path.isfile(source)
41
+ self.fps = 30 # Default FPS until determined
42
+ self.frame_count = 0
43
+
44
+ # Reconnection control
45
+ self.max_reconnect_attempts = max_reconnect_attempts
46
+ self.reconnect_attempts = 0
47
+ self.reconnect_backoff_factor = reconnect_backoff_factor
48
+ self.current_reconnect_interval = reconnect_interval
49
+
50
+ # Thread synchronization
51
+ self.lock = threading.Lock()
52
+ self._latest_frame = None
53
+
54
+ # Start the capture thread
55
+ self.start()
56
+
57
+ def _initialize_capture(self) -> bool:
58
+ """Initialize or reinitialize the capture device.
59
+
60
+ Returns:
61
+ True if successful, False otherwise
62
+ """
63
+ try:
64
+ logging.info(f"🔄 Attempting to connect to stream: {self.source} (attempt {self.reconnect_attempts + 1}/{self.max_reconnect_attempts})")
65
+
66
+ # Clean up existing capture if needed
67
+ if self.capture:
68
+ self.capture.release()
69
+
70
+ # Create new capture object
71
+ self.capture = cv2.VideoCapture(self.source)
72
+
73
+ if not self.capture.isOpened():
74
+ logging.error(f"❌ Failed to open video source: {self.source}")
75
+ return False
76
+
77
+ # Get FPS if available or estimate it
78
+ self.fps = self.capture.get(cv2.CAP_PROP_FPS)
79
+
80
+ # Check FPS validity and estimate if needed
81
+ if not self.fps or self.fps <= 0 or self.fps > 240:
82
+ if self.reconnect_attempts:
83
+ return False
84
+
85
+ logging.warning(f"⚠️ Invalid FPS reported ({self.fps}). Using default 30 FPS.")
86
+ self.fps = 30
87
+
88
+ if self.is_file:
89
+ total_frames = self.capture.get(cv2.CAP_PROP_FRAME_COUNT)
90
+ duration = (total_frames / self.fps) if self.fps > 0 and total_frames > 0 else 0
91
+ logging.info(f"✅ Connected to video file {self.source} at {self.fps:.2f} FPS, {total_frames} frames, {duration:.2f}s duration")
92
+ else:
93
+ logging.info(f"✅ Connected to stream {self.source} at {self.fps:.2f} FPS")
94
+
95
+ self.connected = True
96
+ return True
97
+
98
+ except Exception as e:
99
+ logging.error(f"❌ Error initializing capture for {self.source}: {e}")
100
+ self.connected = False
101
+ if self.capture:
102
+ try:
103
+ self.capture.release()
104
+ except:
105
+ pass
106
+ self.capture = None
107
+ return False
108
+
109
+ def run(self):
110
+ """Main thread loop that continuously captures frames with automatic reconnection."""
111
+ retry_count = 0
112
+ frame_interval = 0 # Will be calculated once we know the FPS
113
+
114
+ while self.running:
115
+ try:
116
+ # Check if we should exit
117
+ if not self.running:
118
+ break
119
+
120
+ # (Re)connect if needed
121
+ if self.capture is None or not self.capture.isOpened():
122
+ # Check if we've exceeded the maximum reconnection attempts
123
+ if self.reconnect_attempts >= self.max_reconnect_attempts:
124
+ logging.error(f"❌ Exceeded maximum reconnection attempts ({self.max_reconnect_attempts}) for {self.source}. Giving up.")
125
+ self.running = False
126
+ break
127
+
128
+ if retry_count:
129
+ self.reconnect_attempts += 1
130
+
131
+ if not self._initialize_capture():
132
+ # Apply exponential backoff to reconnection interval
133
+ self.current_reconnect_interval *= self.reconnect_backoff_factor
134
+
135
+ logging.warning(f"⚠️ Reconnection attempt {self.reconnect_attempts}/{self.max_reconnect_attempts} "
136
+ f"failed. Next attempt in {self.current_reconnect_interval:.1f}s...")
137
+
138
+ # Check for thread stop before sleeping
139
+ if not self.running:
140
+ break
141
+
142
+ time.sleep(self.current_reconnect_interval)
143
+ continue
144
+
145
+ # Reset counters on successful connection
146
+ retry_count = 0
147
+ # Only reset reconnect_attempts if we've been connected for a meaningful duration
148
+ # This prevents rapid success/fail cycles from resetting the counter
149
+ if self.connected:
150
+ self.reconnect_attempts = 0
151
+ self.current_reconnect_interval = self.reconnect_interval
152
+
153
+ frame_interval = 1.0 / self.fps if self.fps > 0 else 0.033
154
+
155
+ # Check if we should exit before reading frame
156
+ if not self.running:
157
+ break
158
+
159
+ # Read the next frame
160
+ read_start = time.time()
161
+ ret, frame = self.capture.read()
162
+
163
+ # Handle frame read failure
164
+ if not ret or frame is None or frame.size == 0:
165
+ if self.is_file:
166
+ # For video files, check if we've reached the end
167
+ current_pos = self.capture.get(cv2.CAP_PROP_POS_FRAMES)
168
+ total_frames = self.capture.get(cv2.CAP_PROP_FRAME_COUNT)
169
+
170
+ # If we're at or beyond the end of the video, restart from beginning
171
+ if current_pos >= total_frames - 1:
172
+ logging.info(f"🔄 Video file {self.source} reached end. Restarting from beginning...")
173
+ self.capture.set(cv2.CAP_PROP_POS_FRAMES, 0)
174
+ # Reset retry count for video files when restarting
175
+ retry_count = 0
176
+ continue
177
+ else:
178
+ # We're not at the end, this might be a real error
179
+ retry_count += 1
180
+ logging.warning(f"⚠️ Failed to read frame from {self.source} at position {current_pos}/{total_frames} (attempt {retry_count}/{self.retry_limit})")
181
+ else:
182
+ # For non-file sources (cameras, streams), increment retry count
183
+ retry_count += 1
184
+ logging.warning(f"⚠️ Failed to read frame from {self.source} (attempt {retry_count}/{self.retry_limit})")
185
+
186
+ if retry_count > self.retry_limit:
187
+ logging.error(f"❌ Too many consecutive frame failures. Reconnecting...")
188
+ self.connected = False
189
+ if self.capture and self.running: # Only release if we're still running
190
+ try:
191
+ self.capture.release()
192
+ self.capture = None
193
+ except Exception as e:
194
+ logging.error(f"Error releasing capture during failure: {e}")
195
+ continue
196
+
197
+ if not self.running:
198
+ break
199
+
200
+ time.sleep(0.1)
201
+ continue
202
+
203
+ # Reset retry count on successful frame
204
+ retry_count = 0
205
+ self.frame_count += 1
206
+
207
+ # Store the frame - but make sure we're still running first
208
+ if not self.running:
209
+ break
210
+
211
+ with self.lock:
212
+ self._latest_frame = frame.copy()
213
+
214
+ # Regulate frame rate to match source FPS for efficiency
215
+ # This helps prevent CPU overuse when reading from fast sources
216
+ elapsed = time.time() - read_start
217
+ sleep_time = max(0, frame_interval - elapsed)
218
+ if sleep_time > 0 and self.running: # Check running before sleep
219
+ time.sleep(sleep_time)
220
+
221
+ except cv2.error as cv_err:
222
+ logging.error(f"❌ OpenCV error in frame processing: {cv_err}")
223
+ self.connected = False
224
+ if not self.running:
225
+ break
226
+ time.sleep(1) # Brief pause before retry
227
+
228
+ except Exception as e:
229
+ logging.error(f"❌ Error in VideoStream {self.source}: {e}", exc_info=True)
230
+ self.connected = False
231
+ if not self.running:
232
+ break
233
+ time.sleep(self.reconnect_interval)
234
+
235
+ # Final cleanup
236
+ self._cleanup()
237
+
238
+ def _cleanup(self):
239
+ """Release resources and perform cleanup."""
240
+ try:
241
+ if self.capture:
242
+ self.capture.release()
243
+ self.capture = None
244
+
245
+ with self.lock:
246
+ self._latest_frame = None
247
+
248
+ self.connected = False
249
+ logging.info(f"🔴 Stream {self.source} stopped and cleaned up.")
250
+
251
+ except Exception as e:
252
+ logging.error(f"Error during final VideoStream cleanup: {e}")
253
+
254
+ def get_frame(self) -> Optional[cv2.Mat]:
255
+ """Returns the latest available frame (thread-safe).
256
+
257
+ Returns:
258
+ A copy of the latest frame or None if no frames are available
259
+ """
260
+ if not self.running:
261
+ return None
262
+
263
+ with self.lock:
264
+ if self._latest_frame is not None:
265
+ return self._latest_frame.copy()
266
+ return None
267
+
268
+ def is_video_ended(self) -> bool:
269
+ """Check if a video file has reached its end (only for video files).
270
+
271
+ Returns:
272
+ True if video file has ended, False otherwise
273
+ """
274
+ if not self.is_file or not self.capture or not self.capture.isOpened():
275
+ return False
276
+
277
+ try:
278
+ current_pos = self.capture.get(cv2.CAP_PROP_POS_FRAMES)
279
+ total_frames = self.capture.get(cv2.CAP_PROP_FRAME_COUNT)
280
+ return current_pos >= total_frames - 1
281
+ except Exception:
282
+ return False
283
+
284
+ def stop(self):
285
+ """Stops the video stream and releases resources safely."""
286
+ if not self.running: # Prevent multiple stops
287
+ return
288
+
289
+ # Step 1: Signal the thread to stop
290
+ self.running = False
291
+
292
+ # Step 2: Wait for any ongoing OpenCV operations to complete
293
+ time.sleep(0.2) # Short delay to let operations finish
294
+
295
+ # Step 3: Explicitly release capture device before joining thread
296
+ # This is critical to avoid segmentation faults in some OpenCV implementations
297
+ if self.capture:
298
+ try:
299
+ logging.debug(f"Releasing capture device for {self.source}")
300
+ with self.lock: # Use lock to ensure thread isn't accessing capture during release
301
+ if self.capture:
302
+ self.capture.release()
303
+ self.capture = None
304
+ except Exception as e:
305
+ logging.error(f"Error releasing capture device: {e}")
306
+
307
+ # Step 4: Clear any references to frames to help garbage collection
308
+ with self.lock:
309
+ self._latest_frame = None
310
+
311
+ # Step 5: Wait for thread to exit
312
+ try:
313
+ if self.is_alive():
314
+ logging.debug(f"Waiting for thread to exit for stream {self.source}")
315
+ self.join(timeout=30) # Reduced timeout since we already released resources
316
+
317
+ # Check if thread is still alive
318
+ if self.is_alive():
319
+ logging.warning(f"⚠️ Stream {self.source} thread did not exit cleanly within timeout")
320
+ except Exception as e:
321
+ logging.error(f"Error joining thread: {e}")
322
+
323
+ # Final status update
324
+ self.connected = False
@@ -0,0 +1,121 @@
1
+ import logging
2
+ import time
3
+ from .VideoStream import VideoStream
4
+ import threading
5
+
6
+ class VideoStreamManager:
7
+ """Manages multiple video streams dynamically using VideoStream threads."""
8
+
9
+ def __init__(self):
10
+ self.streams = {} # Store streams as {worker_source_id: VideoStream}
11
+ self.running = False
12
+ self.lock = threading.Lock() # Add thread lock
13
+
14
+ def add_stream(self, worker_source_id, url):
15
+ """Adds a new video stream if it's not already active."""
16
+ if worker_source_id not in self.streams:
17
+ self.streams[worker_source_id] = VideoStream(url) # Create and start the VideoStream thread
18
+
19
+ else:
20
+ logging.warning(f"⚠️ Stream {worker_source_id} is already active.")
21
+
22
+ def remove_stream(self, worker_source_id):
23
+ """Removes and stops a video stream."""
24
+ if not worker_source_id:
25
+ return
26
+
27
+ with self.lock:
28
+ if worker_source_id not in self.streams:
29
+ logging.warning(f"⚠️ Stream {worker_source_id} not found in manager.")
30
+ return
31
+
32
+ logging.info(f"🛑 Removing video stream: {worker_source_id}")
33
+
34
+ # Get reference before removing from dict
35
+ stream = self.streams.pop(worker_source_id, None)
36
+
37
+ if stream:
38
+ try:
39
+ stream.stop()
40
+
41
+ except Exception as e:
42
+ logging.error(f"❌ Error stopping stream {worker_source_id}: {e}")
43
+ finally:
44
+ stream = None # Ensure cleanup
45
+
46
+ logging.info(f"✅ Stream {worker_source_id} removed successfully.")
47
+
48
+ def start_all(self):
49
+ """Starts all video streams."""
50
+ logging.info("🔄 Starting all video streams...")
51
+ for stream in self.streams.values():
52
+ if not stream.is_alive():
53
+ stream.start() # Start thread if not already running
54
+ self.running = True
55
+ def stop_all(self):
56
+ """Stops all video streams."""
57
+ logging.info("🛑 Stopping all video streams...")
58
+
59
+ with self.lock:
60
+ # Get a list of IDs to avoid modification during iteration
61
+ stream_ids = list(self.streams.keys())
62
+
63
+ # Stop each stream
64
+ for worker_source_id in stream_ids:
65
+ try:
66
+ self.remove_stream(worker_source_id)
67
+ except Exception as e:
68
+ logging.error(f"Error stopping stream {worker_source_id}: {e}")
69
+
70
+ self.running = False
71
+
72
+ def get_frame(self, worker_source_id):
73
+ """Retrieves the latest frame for a specific stream."""
74
+ with self.lock: # Add lock protection for stream access
75
+ stream = self.streams.get(worker_source_id)
76
+ if stream is None:
77
+ return None
78
+
79
+ # Check if stream is still running
80
+ if not stream.running:
81
+ return None
82
+
83
+ try:
84
+ # **Ignore warnings for the first 5 seconds**
85
+ elapsed_time = time.time() - stream.start_time
86
+ if elapsed_time < 5:
87
+ return None
88
+
89
+ # Check if video file has ended
90
+ if stream.is_file and stream.is_video_ended():
91
+ logging.debug(f"Video file {worker_source_id} has ended, waiting for restart...")
92
+ # Small delay to allow the video to restart
93
+ time.sleep(0.1)
94
+ return None
95
+
96
+ return stream.get_frame() # Already returns a copy
97
+ except Exception as e:
98
+ logging.error(f"Error getting frame from stream {worker_source_id}: {e}")
99
+ return None
100
+
101
+ def get_active_stream_ids(self):
102
+ """Returns a list of active stream IDs."""
103
+ return list(self.streams.keys())
104
+
105
+ def get_stream_url(self, worker_source_id):
106
+ """Returns the URL of a specific stream."""
107
+ stream = self.streams.get(worker_source_id)
108
+ return stream.source if stream else None
109
+
110
+ def has_stream(self, worker_source_id):
111
+ """Checks if a stream is active."""
112
+ return worker_source_id in self.streams
113
+
114
+ def is_running(self):
115
+ """Checks if the manager is running."""
116
+ return self.running
117
+
118
+ def is_video_file(self, worker_source_id):
119
+ """Check if a stream is a video file."""
120
+ stream = self.streams.get(worker_source_id)
121
+ return stream.is_file if stream else False