nedo-vision-worker-core 0.2.0__py3-none-any.whl → 0.3.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.
- nedo_vision_worker_core/__init__.py +47 -12
- nedo_vision_worker_core/callbacks/DetectionCallbackManager.py +306 -0
- nedo_vision_worker_core/callbacks/DetectionCallbackTypes.py +150 -0
- nedo_vision_worker_core/callbacks/__init__.py +27 -0
- nedo_vision_worker_core/cli.py +47 -5
- nedo_vision_worker_core/core_service.py +121 -55
- nedo_vision_worker_core/database/DatabaseManager.py +2 -2
- nedo_vision_worker_core/detection/BaseDetector.py +2 -1
- nedo_vision_worker_core/detection/DetectionManager.py +2 -2
- nedo_vision_worker_core/detection/RFDETRDetector.py +23 -5
- nedo_vision_worker_core/detection/YOLODetector.py +18 -5
- nedo_vision_worker_core/detection/detection_processing/DetectionProcessor.py +1 -1
- nedo_vision_worker_core/detection/detection_processing/HumanDetectionProcessor.py +57 -3
- nedo_vision_worker_core/detection/detection_processing/PPEDetectionProcessor.py +173 -10
- nedo_vision_worker_core/models/ai_model.py +23 -2
- nedo_vision_worker_core/pipeline/PipelineProcessor.py +51 -8
- nedo_vision_worker_core/pipeline/PipelineSyncThread.py +32 -0
- nedo_vision_worker_core/repositories/PPEDetectionRepository.py +18 -15
- nedo_vision_worker_core/repositories/RestrictedAreaRepository.py +17 -13
- nedo_vision_worker_core/services/SharedVideoStreamServer.py +276 -0
- nedo_vision_worker_core/services/VideoSharingDaemon.py +808 -0
- nedo_vision_worker_core/services/VideoSharingDaemonManager.py +257 -0
- nedo_vision_worker_core/streams/SharedVideoDeviceManager.py +383 -0
- nedo_vision_worker_core/streams/StreamSyncThread.py +16 -2
- nedo_vision_worker_core/streams/VideoStream.py +208 -246
- nedo_vision_worker_core/streams/VideoStreamManager.py +158 -6
- nedo_vision_worker_core/tracker/TrackerManager.py +25 -31
- nedo_vision_worker_core-0.3.0.dist-info/METADATA +444 -0
- {nedo_vision_worker_core-0.2.0.dist-info → nedo_vision_worker_core-0.3.0.dist-info}/RECORD +32 -25
- nedo_vision_worker_core-0.2.0.dist-info/METADATA +0 -347
- {nedo_vision_worker_core-0.2.0.dist-info → nedo_vision_worker_core-0.3.0.dist-info}/WHEEL +0 -0
- {nedo_vision_worker_core-0.2.0.dist-info → nedo_vision_worker_core-0.3.0.dist-info}/entry_points.txt +0 -0
- {nedo_vision_worker_core-0.2.0.dist-info → nedo_vision_worker_core-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Video Sharing Daemon Manager for automatic daemon lifecycle management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import tempfile
|
|
12
|
+
from typing import Dict, Optional, Set
|
|
13
|
+
from .VideoSharingDaemon import VideoSharingDaemon
|
|
14
|
+
from ..database.DatabaseManager import DatabaseManager
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VideoSharingDaemonManager:
|
|
18
|
+
"""
|
|
19
|
+
Manages video sharing daemons for multiple devices automatically.
|
|
20
|
+
Integrates with CoreService to start/stop daemons as needed.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_instance = None
|
|
24
|
+
_lock = threading.Lock()
|
|
25
|
+
|
|
26
|
+
def __new__(cls):
|
|
27
|
+
if cls._instance is None:
|
|
28
|
+
with cls._lock:
|
|
29
|
+
if cls._instance is None:
|
|
30
|
+
cls._instance = super().__new__(cls)
|
|
31
|
+
return cls._instance
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
if hasattr(self, '_initialized'):
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
self._initialized = True
|
|
38
|
+
self.daemons = {} # Dict[int, VideoSharingDaemon]
|
|
39
|
+
self.daemon_threads: Dict[int, threading.Thread] = {}
|
|
40
|
+
self.managed_devices: Set[int] = set()
|
|
41
|
+
self.running = False
|
|
42
|
+
self.auto_start_enabled = True
|
|
43
|
+
|
|
44
|
+
logging.info("🔗 VideoSharingDaemonManager initialized")
|
|
45
|
+
|
|
46
|
+
def enable_auto_start(self, enabled: bool = True):
|
|
47
|
+
"""Enable or disable automatic daemon starting."""
|
|
48
|
+
self.auto_start_enabled = enabled
|
|
49
|
+
logging.info(f"📋 Auto-start video sharing daemons: {'enabled' if enabled else 'disabled'}")
|
|
50
|
+
|
|
51
|
+
def is_daemon_running(self, device_index: int) -> bool:
|
|
52
|
+
"""Check if daemon is running for a specific device."""
|
|
53
|
+
if not VideoSharingDaemon:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# Get storage path from DatabaseManager
|
|
58
|
+
storage_path = None
|
|
59
|
+
if DatabaseManager and hasattr(DatabaseManager, 'STORAGE_PATH') and DatabaseManager.STORAGE_PATH:
|
|
60
|
+
storage_path = str(DatabaseManager.STORAGE_PATH)
|
|
61
|
+
|
|
62
|
+
# Use the same socket path logic as the daemon
|
|
63
|
+
from nedo_vision_worker_core.services.VideoSharingDaemon import get_storage_socket_path
|
|
64
|
+
socket_path = get_storage_socket_path(device_index, storage_path)
|
|
65
|
+
info_file = socket_path.parent / f"vd{device_index}_info.json"
|
|
66
|
+
|
|
67
|
+
if info_file.exists():
|
|
68
|
+
with open(info_file, 'r') as f:
|
|
69
|
+
info = json.load(f)
|
|
70
|
+
|
|
71
|
+
pid = info.get('pid')
|
|
72
|
+
if pid:
|
|
73
|
+
try:
|
|
74
|
+
# Cross-platform process check
|
|
75
|
+
import platform
|
|
76
|
+
if platform.system() == 'Windows':
|
|
77
|
+
import subprocess
|
|
78
|
+
result = subprocess.run(['tasklist', '/FI', f'PID eq {pid}'],
|
|
79
|
+
capture_output=True, text=True)
|
|
80
|
+
return str(pid) in result.stdout
|
|
81
|
+
else:
|
|
82
|
+
os.kill(pid, 0) # Check if process exists
|
|
83
|
+
return True
|
|
84
|
+
except (OSError, ProcessLookupError, subprocess.SubprocessError):
|
|
85
|
+
# Process is dead, clean up
|
|
86
|
+
info_file.unlink()
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def start_daemon_for_device(self, device_index: int) -> bool:
|
|
92
|
+
"""Start a video sharing daemon for a specific device."""
|
|
93
|
+
if not VideoSharingDaemon:
|
|
94
|
+
logging.warning(f"Video sharing daemon not available, cannot start daemon for device {device_index}")
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
if device_index in self.daemons:
|
|
98
|
+
logging.info(f"📹 Daemon for device {device_index} already managed")
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
# Check if external daemon is already running
|
|
102
|
+
if self.is_daemon_running(device_index):
|
|
103
|
+
logging.info(f"📹 External daemon for device {device_index} already running")
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
# Get storage path from DatabaseManager
|
|
108
|
+
storage_path = None
|
|
109
|
+
if DatabaseManager and hasattr(DatabaseManager, 'STORAGE_PATH') and DatabaseManager.STORAGE_PATH:
|
|
110
|
+
storage_path = str(DatabaseManager.STORAGE_PATH)
|
|
111
|
+
|
|
112
|
+
# Create daemon instance
|
|
113
|
+
daemon = VideoSharingDaemon(device_index, storage_path=storage_path)
|
|
114
|
+
|
|
115
|
+
# Start daemon in separate thread
|
|
116
|
+
def daemon_runner():
|
|
117
|
+
try:
|
|
118
|
+
logging.info(f"🚀 Starting video sharing daemon for device {device_index}")
|
|
119
|
+
daemon.start_daemon()
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logging.error(f"❌ Error running daemon for device {device_index}: {e}")
|
|
122
|
+
finally:
|
|
123
|
+
# Clean up when daemon stops
|
|
124
|
+
with self._lock:
|
|
125
|
+
if device_index in self.daemons:
|
|
126
|
+
del self.daemons[device_index]
|
|
127
|
+
if device_index in self.daemon_threads:
|
|
128
|
+
del self.daemon_threads[device_index]
|
|
129
|
+
if device_index in self.managed_devices:
|
|
130
|
+
self.managed_devices.remove(device_index)
|
|
131
|
+
|
|
132
|
+
daemon_thread = threading.Thread(
|
|
133
|
+
target=daemon_runner,
|
|
134
|
+
name=f"VideoSharingDaemon-{device_index}",
|
|
135
|
+
daemon=True
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Store references
|
|
139
|
+
self.daemons[device_index] = daemon
|
|
140
|
+
self.daemon_threads[device_index] = daemon_thread
|
|
141
|
+
self.managed_devices.add(device_index)
|
|
142
|
+
|
|
143
|
+
# Start the daemon
|
|
144
|
+
daemon_thread.start()
|
|
145
|
+
|
|
146
|
+
# Give daemon time to start
|
|
147
|
+
time.sleep(1)
|
|
148
|
+
|
|
149
|
+
# Verify daemon started successfully
|
|
150
|
+
if self.is_daemon_running(device_index):
|
|
151
|
+
logging.info(f"✅ Video sharing daemon started for device {device_index}")
|
|
152
|
+
return True
|
|
153
|
+
else:
|
|
154
|
+
logging.error(f"❌ Failed to start video sharing daemon for device {device_index}")
|
|
155
|
+
self.stop_daemon_for_device(device_index)
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logging.error(f"❌ Error starting daemon for device {device_index}: {e}")
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
def stop_daemon_for_device(self, device_index: int):
|
|
163
|
+
"""Stop the video sharing daemon for a specific device."""
|
|
164
|
+
try:
|
|
165
|
+
if device_index in self.daemons:
|
|
166
|
+
daemon = self.daemons[device_index]
|
|
167
|
+
logging.info(f"🛑 Stopping video sharing daemon for device {device_index}")
|
|
168
|
+
daemon.stop_daemon()
|
|
169
|
+
|
|
170
|
+
# Wait for thread to finish
|
|
171
|
+
if device_index in self.daemon_threads:
|
|
172
|
+
thread = self.daemon_threads[device_index]
|
|
173
|
+
thread.join(timeout=5)
|
|
174
|
+
|
|
175
|
+
# Clean up references
|
|
176
|
+
if device_index in self.daemons:
|
|
177
|
+
del self.daemons[device_index]
|
|
178
|
+
if device_index in self.daemon_threads:
|
|
179
|
+
del self.daemon_threads[device_index]
|
|
180
|
+
if device_index in self.managed_devices:
|
|
181
|
+
self.managed_devices.remove(device_index)
|
|
182
|
+
|
|
183
|
+
logging.info(f"✅ Video sharing daemon stopped for device {device_index}")
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logging.error(f"❌ Error stopping daemon for device {device_index}: {e}")
|
|
187
|
+
|
|
188
|
+
def ensure_daemon_for_device(self, device_index: int) -> bool:
|
|
189
|
+
"""
|
|
190
|
+
Ensure a daemon is running for the device.
|
|
191
|
+
Start one if auto-start is enabled and none is running.
|
|
192
|
+
"""
|
|
193
|
+
if not self.auto_start_enabled:
|
|
194
|
+
return self.is_daemon_running(device_index)
|
|
195
|
+
|
|
196
|
+
# Check if daemon is already running (managed or external)
|
|
197
|
+
if self.is_daemon_running(device_index):
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
# Try to start our own daemon
|
|
201
|
+
return self.start_daemon_for_device(device_index)
|
|
202
|
+
|
|
203
|
+
def start_all_managed_daemons(self):
|
|
204
|
+
"""Start daemons for all currently managed devices."""
|
|
205
|
+
self.running = True
|
|
206
|
+
for device_index in list(self.managed_devices):
|
|
207
|
+
if not self.is_daemon_running(device_index):
|
|
208
|
+
self.start_daemon_for_device(device_index)
|
|
209
|
+
|
|
210
|
+
def stop_all_daemons(self):
|
|
211
|
+
"""Stop all managed video sharing daemons."""
|
|
212
|
+
logging.info("🛑 Stopping all video sharing daemons...")
|
|
213
|
+
self.running = False
|
|
214
|
+
|
|
215
|
+
# Stop all managed daemons
|
|
216
|
+
device_indices = list(self.managed_devices)
|
|
217
|
+
for device_index in device_indices:
|
|
218
|
+
self.stop_daemon_for_device(device_index)
|
|
219
|
+
|
|
220
|
+
logging.info("✅ All video sharing daemons stopped")
|
|
221
|
+
|
|
222
|
+
def get_daemon_status(self) -> Dict[int, Dict]:
|
|
223
|
+
"""Get status of all managed daemons."""
|
|
224
|
+
status = {}
|
|
225
|
+
|
|
226
|
+
for device_index in self.managed_devices:
|
|
227
|
+
is_running = self.is_daemon_running(device_index)
|
|
228
|
+
thread_alive = device_index in self.daemon_threads and self.daemon_threads[device_index].is_alive()
|
|
229
|
+
|
|
230
|
+
status[device_index] = {
|
|
231
|
+
'managed': True,
|
|
232
|
+
'daemon_running': is_running,
|
|
233
|
+
'thread_alive': thread_alive,
|
|
234
|
+
'auto_start': self.auto_start_enabled
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return status
|
|
238
|
+
|
|
239
|
+
def get_device_info(self, device_index: int) -> Optional[Dict]:
|
|
240
|
+
"""Get device information from daemon if available."""
|
|
241
|
+
try:
|
|
242
|
+
info_file = Path(tempfile.gettempdir()) / f"video_device_{device_index}_info.json"
|
|
243
|
+
if info_file.exists():
|
|
244
|
+
with open(info_file, 'r') as f:
|
|
245
|
+
return json.load(f)
|
|
246
|
+
except Exception:
|
|
247
|
+
pass
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# Singleton instance
|
|
252
|
+
daemon_manager = VideoSharingDaemonManager()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_daemon_manager() -> VideoSharingDaemonManager:
|
|
256
|
+
"""Get the singleton daemon manager instance."""
|
|
257
|
+
return daemon_manager
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
import cv2
|
|
5
|
+
from typing import Dict, Optional, Callable
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from .VideoStream import VideoStream
|
|
8
|
+
from ..services.SharedVideoStreamServer import get_shared_stream_server
|
|
9
|
+
from ..services.VideoSharingDaemonManager import get_daemon_manager
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from nedo_vision_worker_core.services.VideoSharingDaemon import VideoSharingClient
|
|
13
|
+
except ImportError:
|
|
14
|
+
logging.warning("Video sharing daemon not available, falling back to SharedVideoStreamServer")
|
|
15
|
+
VideoSharingClient = None
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from ..database.DatabaseManager import DatabaseManager
|
|
19
|
+
except ImportError:
|
|
20
|
+
DatabaseManager = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DeviceAccessMode(Enum):
|
|
24
|
+
EXCLUSIVE = "exclusive"
|
|
25
|
+
SHARED = "shared"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SharedVideoDeviceManager:
|
|
29
|
+
"""
|
|
30
|
+
Manages shared access to direct video devices across multiple services.
|
|
31
|
+
Prevents 'device busy' errors by implementing a device sharing mechanism.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
_instance = None
|
|
35
|
+
_lock = threading.Lock()
|
|
36
|
+
|
|
37
|
+
def __new__(cls):
|
|
38
|
+
if cls._instance is None:
|
|
39
|
+
with cls._lock:
|
|
40
|
+
if cls._instance is None:
|
|
41
|
+
cls._instance = super().__new__(cls)
|
|
42
|
+
return cls._instance
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
if hasattr(self, '_initialized'):
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
self._initialized = True
|
|
49
|
+
self.device_streams: Dict[int, VideoStream] = {} # device_index -> VideoStream (legacy)
|
|
50
|
+
self.device_subscribers: Dict[int, Dict[str, Callable]] = {} # device_index -> {subscriber_id: callback}
|
|
51
|
+
self.device_locks: Dict[int, threading.Lock] = {}
|
|
52
|
+
self.device_access_counts: Dict[int, int] = {}
|
|
53
|
+
self.main_lock = threading.Lock()
|
|
54
|
+
|
|
55
|
+
# Video sharing clients for cross-process access
|
|
56
|
+
self.video_clients = {} # device_index -> Video sharing client
|
|
57
|
+
self._use_video_sharing = VideoSharingClient is not None
|
|
58
|
+
|
|
59
|
+
if self._use_video_sharing:
|
|
60
|
+
logging.info("🔗 SharedVideoDeviceManager initialized with cross-process video sharing support")
|
|
61
|
+
else:
|
|
62
|
+
logging.info("⚠️ SharedVideoDeviceManager initialized with SharedVideoStreamServer fallback")
|
|
63
|
+
|
|
64
|
+
logging.info("SharedVideoDeviceManager initialized")
|
|
65
|
+
|
|
66
|
+
def _is_direct_device(self, source) -> tuple:
|
|
67
|
+
"""Check if source is a direct video device and return device index."""
|
|
68
|
+
if isinstance(source, int):
|
|
69
|
+
return True, source
|
|
70
|
+
elif isinstance(source, str) and source.isdigit():
|
|
71
|
+
return True, int(source)
|
|
72
|
+
elif isinstance(source, str) and source.startswith('/dev/video'):
|
|
73
|
+
try:
|
|
74
|
+
device_index = int(source.replace('/dev/video', ''))
|
|
75
|
+
return True, device_index
|
|
76
|
+
except ValueError:
|
|
77
|
+
pass
|
|
78
|
+
return False, None
|
|
79
|
+
|
|
80
|
+
def subscribe_to_device(self, source, subscriber_id: str, callback: Callable[[cv2.Mat], None]) -> bool:
|
|
81
|
+
"""
|
|
82
|
+
Subscribe to a direct video device.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
source: Video source (device index or path)
|
|
86
|
+
subscriber_id: Unique identifier for the subscriber
|
|
87
|
+
callback: Function to call with new frames
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
bool: True if subscription successful, False otherwise
|
|
91
|
+
"""
|
|
92
|
+
is_device, device_index = self._is_direct_device(source)
|
|
93
|
+
|
|
94
|
+
if not is_device:
|
|
95
|
+
logging.warning(f"Source {source} is not a direct video device")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
with self.main_lock:
|
|
99
|
+
# Initialize device if not exists
|
|
100
|
+
if device_index not in self.device_streams:
|
|
101
|
+
if not self._initialize_device(device_index):
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
# Add subscriber
|
|
105
|
+
if device_index not in self.device_subscribers:
|
|
106
|
+
self.device_subscribers[device_index] = {}
|
|
107
|
+
|
|
108
|
+
self.device_subscribers[device_index][subscriber_id] = callback
|
|
109
|
+
self.device_access_counts[device_index] = self.device_access_counts.get(device_index, 0) + 1
|
|
110
|
+
|
|
111
|
+
logging.info(f"Subscriber {subscriber_id} added to device {device_index}. "
|
|
112
|
+
f"Total subscribers: {self.device_access_counts[device_index]}")
|
|
113
|
+
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
def unsubscribe_from_device(self, source, subscriber_id: str) -> bool:
|
|
117
|
+
"""
|
|
118
|
+
Unsubscribe from a direct video device.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
source: Video source (device index or path)
|
|
122
|
+
subscriber_id: Unique identifier for the subscriber
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
bool: True if unsubscription successful, False otherwise
|
|
126
|
+
"""
|
|
127
|
+
is_device, device_index = self._is_direct_device(source)
|
|
128
|
+
|
|
129
|
+
if not is_device:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
with self.main_lock:
|
|
133
|
+
if device_index not in self.device_subscribers:
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
if subscriber_id in self.device_subscribers[device_index]:
|
|
137
|
+
del self.device_subscribers[device_index][subscriber_id]
|
|
138
|
+
self.device_access_counts[device_index] -= 1
|
|
139
|
+
|
|
140
|
+
logging.info(f"Subscriber {subscriber_id} removed from device {device_index}. "
|
|
141
|
+
f"Remaining subscribers: {self.device_access_counts[device_index]}")
|
|
142
|
+
|
|
143
|
+
# Clean up device if no more subscribers
|
|
144
|
+
if self.device_access_counts[device_index] <= 0:
|
|
145
|
+
self._cleanup_device(device_index)
|
|
146
|
+
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
def _initialize_device(self, device_index: int) -> bool:
|
|
152
|
+
"""Initialize a direct video device with cross-process video sharing."""
|
|
153
|
+
try:
|
|
154
|
+
if self._use_video_sharing:
|
|
155
|
+
return self._initialize_device_with_video_sharing(device_index)
|
|
156
|
+
else:
|
|
157
|
+
return self._initialize_device_with_shared_server(device_index)
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logging.error(f"❌ Failed to initialize device {device_index}: {e}")
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
def _initialize_device_with_video_sharing(self, device_index: int) -> bool:
|
|
163
|
+
"""Initialize device using VideoSharingClient for true cross-process access."""
|
|
164
|
+
try:
|
|
165
|
+
logging.info(f"Initializing cross-process access to video device {device_index}")
|
|
166
|
+
|
|
167
|
+
# Ensure daemon is running for this device
|
|
168
|
+
daemon_manager = get_daemon_manager()
|
|
169
|
+
if not daemon_manager.ensure_daemon_for_device(device_index):
|
|
170
|
+
logging.error(f"❌ Failed to ensure video sharing daemon for device {device_index}")
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
# Get storage path from DatabaseManager
|
|
174
|
+
storage_path = None
|
|
175
|
+
if DatabaseManager and hasattr(DatabaseManager, 'STORAGE_PATH') and DatabaseManager.STORAGE_PATH:
|
|
176
|
+
storage_path = str(DatabaseManager.STORAGE_PATH)
|
|
177
|
+
|
|
178
|
+
# Create video sharing client
|
|
179
|
+
video_client = VideoSharingClient(device_index, storage_path=storage_path)
|
|
180
|
+
|
|
181
|
+
# Connect to the video sharing daemon
|
|
182
|
+
if not video_client.connect(lambda frame, timestamp: self._on_frame_received(device_index, frame, timestamp)):
|
|
183
|
+
logging.error(f"❌ Failed to connect to video sharing daemon for device {device_index}")
|
|
184
|
+
logging.info("💡 Daemon should be auto-started, but connection failed")
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
# Store video client
|
|
188
|
+
self.video_clients[device_index] = video_client
|
|
189
|
+
self.device_locks[device_index] = threading.Lock()
|
|
190
|
+
|
|
191
|
+
# Store device info
|
|
192
|
+
self.device_streams[device_index] = {
|
|
193
|
+
'video_client': video_client,
|
|
194
|
+
'type': 'video_sharing'
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
logging.info(f"✅ Successfully initialized cross-process access to device {device_index}")
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logging.error(f"❌ Failed to initialize cross-process access for device {device_index}: {e}")
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
def _initialize_device_with_shared_server(self, device_index: int) -> bool:
|
|
205
|
+
"""Initialize device using SharedVideoStreamServer (fallback method)."""
|
|
206
|
+
try:
|
|
207
|
+
logging.info(f"Initializing shared access to video device {device_index}")
|
|
208
|
+
|
|
209
|
+
# Use shared stream server instead of exclusive device access
|
|
210
|
+
shared_server = get_shared_stream_server(device_index)
|
|
211
|
+
|
|
212
|
+
# Add this manager as a consumer
|
|
213
|
+
consumer_id = shared_server.add_consumer(
|
|
214
|
+
lambda frame, timestamp: self._on_frame_received(device_index, frame, timestamp),
|
|
215
|
+
f"worker-core-{device_index}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Store server reference and consumer ID
|
|
219
|
+
self.device_streams[device_index] = {
|
|
220
|
+
'shared_server': shared_server,
|
|
221
|
+
'consumer_id': consumer_id,
|
|
222
|
+
'type': 'shared'
|
|
223
|
+
}
|
|
224
|
+
self.device_locks[device_index] = threading.Lock()
|
|
225
|
+
|
|
226
|
+
logging.info(f"✅ Successfully initialized shared access to device {device_index}")
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logging.error(f"❌ Failed to initialize shared access for device {device_index}: {e}")
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
def _on_frame_received(self, device_index: int, frame, timestamp):
|
|
234
|
+
"""Handle frame received from shared stream server."""
|
|
235
|
+
try:
|
|
236
|
+
# Distribute frame to all subscribers
|
|
237
|
+
with self.device_locks[device_index]:
|
|
238
|
+
subscribers = self.device_subscribers.get(device_index, {}).copy()
|
|
239
|
+
|
|
240
|
+
for subscriber_id, callback in subscribers.items():
|
|
241
|
+
try:
|
|
242
|
+
callback(frame)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logging.warning(f"Error delivering frame to subscriber {subscriber_id}: {e}")
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logging.error(f"Error processing frame for device {device_index}: {e}")
|
|
248
|
+
|
|
249
|
+
logging.info(f"Successfully initialized shared access to device {device_index}")
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logging.error(f"Error initializing device {device_index}: {e}")
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def _cleanup_device(self, device_index: int):
|
|
257
|
+
"""Clean up resources for a device and release shared stream access."""
|
|
258
|
+
logging.info(f"Cleaning up device {device_index}")
|
|
259
|
+
|
|
260
|
+
if device_index in self.device_streams:
|
|
261
|
+
device_info = self.device_streams.pop(device_index)
|
|
262
|
+
|
|
263
|
+
if isinstance(device_info, dict):
|
|
264
|
+
device_type = device_info.get('type')
|
|
265
|
+
|
|
266
|
+
if device_type == 'video_sharing':
|
|
267
|
+
# Disconnect from video sharing client
|
|
268
|
+
video_client = device_info.get('video_client')
|
|
269
|
+
if video_client:
|
|
270
|
+
video_client.disconnect()
|
|
271
|
+
logging.info(f"Disconnected from video sharing daemon for device {device_index}")
|
|
272
|
+
|
|
273
|
+
elif device_type == 'shared':
|
|
274
|
+
# Remove from shared stream server
|
|
275
|
+
shared_server = device_info.get('shared_server')
|
|
276
|
+
consumer_id = device_info.get('consumer_id')
|
|
277
|
+
|
|
278
|
+
if shared_server and consumer_id:
|
|
279
|
+
shared_server.remove_consumer(consumer_id)
|
|
280
|
+
logging.info(f"Removed consumer from shared stream server for device {device_index}")
|
|
281
|
+
else:
|
|
282
|
+
# Legacy cleanup for old-style streams
|
|
283
|
+
if hasattr(device_info, 'stop'):
|
|
284
|
+
device_info.stop()
|
|
285
|
+
|
|
286
|
+
# Clean up video client if exists
|
|
287
|
+
if device_index in self.video_clients:
|
|
288
|
+
video_client = self.video_clients.pop(device_index)
|
|
289
|
+
video_client.disconnect()
|
|
290
|
+
logging.info(f"Cleaned up video sharing client for device {device_index}")
|
|
291
|
+
|
|
292
|
+
if device_index in self.device_locks:
|
|
293
|
+
del self.device_locks[device_index]
|
|
294
|
+
|
|
295
|
+
if device_index in self.device_subscribers:
|
|
296
|
+
del self.device_subscribers[device_index]
|
|
297
|
+
|
|
298
|
+
if device_index in self.device_access_counts:
|
|
299
|
+
del self.device_access_counts[device_index]
|
|
300
|
+
|
|
301
|
+
def _start_frame_distributor(self, device_index: int):
|
|
302
|
+
"""Start a thread to distribute frames to all subscribers."""
|
|
303
|
+
def distribute_frames():
|
|
304
|
+
stream = self.device_streams[device_index]
|
|
305
|
+
|
|
306
|
+
while stream.running and device_index in self.device_streams:
|
|
307
|
+
try:
|
|
308
|
+
frame = stream.get_frame()
|
|
309
|
+
|
|
310
|
+
if frame is not None:
|
|
311
|
+
# Distribute frame to all subscribers
|
|
312
|
+
with self.device_locks[device_index]:
|
|
313
|
+
subscribers = self.device_subscribers.get(device_index, {}).copy()
|
|
314
|
+
|
|
315
|
+
for subscriber_id, callback in subscribers.items():
|
|
316
|
+
try:
|
|
317
|
+
callback(frame.copy())
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logging.error(f"Error calling callback for subscriber {subscriber_id}: {e}")
|
|
320
|
+
|
|
321
|
+
time.sleep(1.0 / 30.0) # 30 FPS distribution rate
|
|
322
|
+
|
|
323
|
+
except Exception as e:
|
|
324
|
+
logging.error(f"Error in frame distributor for device {device_index}: {e}")
|
|
325
|
+
time.sleep(0.1)
|
|
326
|
+
|
|
327
|
+
logging.info(f"Frame distributor for device {device_index} stopped")
|
|
328
|
+
|
|
329
|
+
distributor_thread = threading.Thread(
|
|
330
|
+
target=distribute_frames,
|
|
331
|
+
name=f"FrameDistributor-{device_index}",
|
|
332
|
+
daemon=True
|
|
333
|
+
)
|
|
334
|
+
distributor_thread.start()
|
|
335
|
+
|
|
336
|
+
def get_device_info(self, device_index: int) -> Optional[Dict]:
|
|
337
|
+
"""Get information about a device."""
|
|
338
|
+
with self.main_lock:
|
|
339
|
+
if device_index not in self.device_streams:
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
stream = self.device_streams[device_index]
|
|
343
|
+
return {
|
|
344
|
+
'device_index': device_index,
|
|
345
|
+
'is_connected': stream.is_connected(),
|
|
346
|
+
'state': stream.get_state().value,
|
|
347
|
+
'subscriber_count': self.device_access_counts.get(device_index, 0),
|
|
348
|
+
'subscribers': list(self.device_subscribers.get(device_index, {}).keys())
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
def get_all_devices_info(self) -> Dict[int, Dict]:
|
|
352
|
+
"""Get information about all managed devices."""
|
|
353
|
+
with self.main_lock:
|
|
354
|
+
return {
|
|
355
|
+
device_index: self.get_device_info(device_index)
|
|
356
|
+
for device_index in self.device_streams.keys()
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
def is_device_available(self, source) -> bool:
|
|
360
|
+
"""Check if a direct video device is available."""
|
|
361
|
+
is_device, device_index = self._is_direct_device(source)
|
|
362
|
+
|
|
363
|
+
if not is_device:
|
|
364
|
+
return False
|
|
365
|
+
|
|
366
|
+
# Test if device can be opened
|
|
367
|
+
try:
|
|
368
|
+
test_cap = cv2.VideoCapture(device_index)
|
|
369
|
+
available = test_cap.isOpened()
|
|
370
|
+
test_cap.release()
|
|
371
|
+
return available
|
|
372
|
+
except Exception:
|
|
373
|
+
return False
|
|
374
|
+
|
|
375
|
+
def shutdown(self):
|
|
376
|
+
"""Shutdown the manager and clean up all devices."""
|
|
377
|
+
logging.info("Shutting down SharedVideoDeviceManager")
|
|
378
|
+
|
|
379
|
+
with self.main_lock:
|
|
380
|
+
device_indices = list(self.device_streams.keys())
|
|
381
|
+
|
|
382
|
+
for device_index in device_indices:
|
|
383
|
+
self._cleanup_device(device_index)
|
|
@@ -29,8 +29,13 @@ class StreamSyncThread(threading.Thread):
|
|
|
29
29
|
try:
|
|
30
30
|
sources = self.worker_source_repo.get_worker_sources()
|
|
31
31
|
db_sources = {
|
|
32
|
-
source.id: (
|
|
33
|
-
|
|
32
|
+
source.id: (
|
|
33
|
+
source.url if source.type_code == "live"
|
|
34
|
+
else source.url if source.type_code == "direct"
|
|
35
|
+
else self._get_source_file_path(source.file_path),
|
|
36
|
+
source.status_code
|
|
37
|
+
) for source in sources
|
|
38
|
+
} # Store latest sources
|
|
34
39
|
active_stream_ids = set(self.manager.get_active_stream_ids())
|
|
35
40
|
|
|
36
41
|
# **1️⃣ Add new streams**
|
|
@@ -54,6 +59,9 @@ class StreamSyncThread(threading.Thread):
|
|
|
54
59
|
if existing_url != url:
|
|
55
60
|
logging.info(f"🟡 Updating stream {source_id}: New URL {url}")
|
|
56
61
|
self.manager.remove_stream(source_id)
|
|
62
|
+
# Add a small delay for device cleanup
|
|
63
|
+
if self._is_direct_device(url) or self._is_direct_device(existing_url):
|
|
64
|
+
time.sleep(0.5) # Allow device to be properly released
|
|
57
65
|
self.manager.add_stream(source_id, url)
|
|
58
66
|
|
|
59
67
|
except Exception as e:
|
|
@@ -61,6 +69,12 @@ class StreamSyncThread(threading.Thread):
|
|
|
61
69
|
|
|
62
70
|
time.sleep(self.polling_interval) # Poll every X seconds
|
|
63
71
|
|
|
72
|
+
def _is_direct_device(self, url) -> bool:
|
|
73
|
+
"""Check if URL represents a direct video device."""
|
|
74
|
+
if isinstance(url, str):
|
|
75
|
+
return url.isdigit() or url.startswith('/dev/video')
|
|
76
|
+
return isinstance(url, int)
|
|
77
|
+
|
|
64
78
|
def stop(self):
|
|
65
79
|
"""Stops the synchronization thread."""
|
|
66
80
|
self.running = False
|