nedo-vision-worker 1.1.3__py3-none-any.whl → 1.2.1__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.
- nedo_vision_worker/__init__.py +1 -1
- nedo_vision_worker/cli.py +196 -167
- nedo_vision_worker/database/DatabaseManager.py +3 -3
- nedo_vision_worker/doctor.py +1066 -386
- nedo_vision_worker/models/ai_model.py +35 -2
- nedo_vision_worker/protos/AIModelService_pb2.py +12 -10
- nedo_vision_worker/protos/AIModelService_pb2_grpc.py +1 -1
- nedo_vision_worker/protos/DatasetSourceService_pb2.py +2 -2
- nedo_vision_worker/protos/DatasetSourceService_pb2_grpc.py +1 -1
- nedo_vision_worker/protos/HumanDetectionService_pb2.py +2 -2
- nedo_vision_worker/protos/HumanDetectionService_pb2_grpc.py +1 -1
- nedo_vision_worker/protos/PPEDetectionService_pb2.py +2 -2
- nedo_vision_worker/protos/PPEDetectionService_pb2_grpc.py +1 -1
- nedo_vision_worker/protos/VisionWorkerService_pb2.py +2 -2
- nedo_vision_worker/protos/VisionWorkerService_pb2_grpc.py +1 -1
- nedo_vision_worker/protos/WorkerSourcePipelineService_pb2.py +2 -2
- nedo_vision_worker/protos/WorkerSourcePipelineService_pb2_grpc.py +1 -1
- nedo_vision_worker/protos/WorkerSourceService_pb2.py +2 -2
- nedo_vision_worker/protos/WorkerSourceService_pb2_grpc.py +1 -1
- nedo_vision_worker/services/AIModelClient.py +184 -160
- nedo_vision_worker/services/DirectDeviceToRTMPStreamer.py +534 -0
- nedo_vision_worker/services/GrpcClientBase.py +142 -108
- nedo_vision_worker/services/PPEDetectionClient.py +0 -7
- nedo_vision_worker/services/RestrictedAreaClient.py +0 -5
- nedo_vision_worker/services/SharedDirectDeviceClient.py +278 -0
- nedo_vision_worker/services/SharedVideoStreamServer.py +315 -0
- nedo_vision_worker/services/SystemWideDeviceCoordinator.py +236 -0
- nedo_vision_worker/services/VideoSharingDaemon.py +832 -0
- nedo_vision_worker/services/VideoStreamClient.py +43 -20
- nedo_vision_worker/services/WorkerSourceClient.py +1 -1
- nedo_vision_worker/services/WorkerSourcePipelineClient.py +35 -12
- nedo_vision_worker/services/WorkerSourceUpdater.py +30 -3
- nedo_vision_worker/util/FFmpegUtil.py +124 -0
- nedo_vision_worker/util/VideoProbeUtil.py +227 -17
- nedo_vision_worker/worker/DataSyncWorker.py +1 -0
- nedo_vision_worker/worker/PipelineImageWorker.py +1 -1
- nedo_vision_worker/worker/VideoStreamWorker.py +27 -3
- nedo_vision_worker/worker/WorkerManager.py +2 -29
- nedo_vision_worker/worker_service.py +22 -5
- {nedo_vision_worker-1.1.3.dist-info → nedo_vision_worker-1.2.1.dist-info}/METADATA +1 -3
- {nedo_vision_worker-1.1.3.dist-info → nedo_vision_worker-1.2.1.dist-info}/RECORD +44 -38
- {nedo_vision_worker-1.1.3.dist-info → nedo_vision_worker-1.2.1.dist-info}/WHEEL +0 -0
- {nedo_vision_worker-1.1.3.dist-info → nedo_vision_worker-1.2.1.dist-info}/entry_points.txt +0 -0
- {nedo_vision_worker-1.1.3.dist-info → nedo_vision_worker-1.2.1.dist-info}/top_level.txt +0 -0
|
@@ -4,16 +4,36 @@ import logging
|
|
|
4
4
|
import json
|
|
5
5
|
import fractions
|
|
6
6
|
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
import platform
|
|
9
|
+
from pathlib import Path
|
|
7
10
|
from urllib.parse import urlparse
|
|
11
|
+
from .FFmpegUtil import get_rtsp_probe_options
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from nedo_vision_worker.services.VideoSharingDaemon import VideoSharingClient
|
|
16
|
+
except ImportError:
|
|
17
|
+
logging.warning("VideoSharingDaemon not available")
|
|
18
|
+
VideoSharingClient = None
|
|
19
|
+
|
|
20
|
+
# Import DatabaseManager for storage path
|
|
21
|
+
try:
|
|
22
|
+
from nedo_vision_worker.database.DatabaseManager import get_storage_path
|
|
23
|
+
except ImportError:
|
|
24
|
+
get_storage_path = None
|
|
8
25
|
|
|
9
26
|
class VideoProbeUtil:
|
|
10
27
|
"""Utility to extract metadata from video URLs using OpenCV and ffmpeg."""
|
|
11
28
|
|
|
12
29
|
@staticmethod
|
|
13
30
|
def get_video_metadata(video_url: str) -> dict:
|
|
14
|
-
"""Extracts resolution and frame rate from a video URL using OpenCV or ffmpeg."""
|
|
15
31
|
try:
|
|
16
|
-
|
|
32
|
+
if isinstance(video_url, str) and video_url.isdigit():
|
|
33
|
+
metadata = VideoProbeUtil._get_metadata_direct_device(video_url)
|
|
34
|
+
if metadata:
|
|
35
|
+
return metadata
|
|
36
|
+
|
|
17
37
|
metadata = VideoProbeUtil._get_metadata_ffmpeg(video_url)
|
|
18
38
|
return metadata
|
|
19
39
|
|
|
@@ -45,13 +65,208 @@ class VideoProbeUtil:
|
|
|
45
65
|
"timestamp": None
|
|
46
66
|
}
|
|
47
67
|
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _get_metadata_opencv_direct_device(device_idx: int) -> dict:
|
|
70
|
+
"""Fallback method to get metadata directly from camera device using OpenCV."""
|
|
71
|
+
try:
|
|
72
|
+
logging.debug(f"Attempting direct OpenCV access to device {device_idx}")
|
|
73
|
+
cap = cv2.VideoCapture(device_idx)
|
|
74
|
+
|
|
75
|
+
if not cap.isOpened():
|
|
76
|
+
logging.warning(f"⚠️ [APP] OpenCV failed to open device {device_idx}")
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
# Set a reasonable timeout and try to read a frame
|
|
80
|
+
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Reduce buffer to get fresh frames
|
|
81
|
+
|
|
82
|
+
# Try multiple attempts to read a frame (some cameras need warm-up)
|
|
83
|
+
ret, frame = False, None
|
|
84
|
+
for attempt in range(3):
|
|
85
|
+
ret, frame = cap.read()
|
|
86
|
+
if ret and frame is not None:
|
|
87
|
+
break
|
|
88
|
+
logging.debug(f"Attempt {attempt + 1} failed to read frame from device {device_idx}")
|
|
89
|
+
|
|
90
|
+
if not ret or frame is None:
|
|
91
|
+
logging.warning(f"⚠️ [APP] OpenCV failed to read frame from device {device_idx} after 3 attempts")
|
|
92
|
+
cap.release()
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
# Get video properties
|
|
96
|
+
height, width = frame.shape[:2]
|
|
97
|
+
frame_rate = cap.get(cv2.CAP_PROP_FPS)
|
|
98
|
+
|
|
99
|
+
# Some cameras report 0 FPS, use a reasonable default
|
|
100
|
+
if frame_rate <= 0:
|
|
101
|
+
frame_rate = 30.0
|
|
102
|
+
else:
|
|
103
|
+
frame_rate = round(frame_rate, 2)
|
|
104
|
+
|
|
105
|
+
cap.release()
|
|
106
|
+
|
|
107
|
+
logging.info(f"✅ [APP] Successfully probed device {device_idx} directly: {width}x{height} @ {frame_rate}fps")
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
"resolution": f"{width}x{height}",
|
|
111
|
+
"frame_rate": frame_rate,
|
|
112
|
+
"timestamp": None
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logging.error(f"❌ [APP] Error probing device {device_idx} with OpenCV: {e}")
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _get_metadata_ffmpeg_direct_device(device_idx: int) -> dict:
|
|
121
|
+
"""Fallback method to get metadata from camera device using FFmpeg."""
|
|
122
|
+
if not shutil.which("ffprobe"):
|
|
123
|
+
logging.warning("⚠️ [APP] ffprobe not available for device probing")
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
system = platform.system().lower()
|
|
128
|
+
|
|
129
|
+
# Determine the device input format based on OS
|
|
130
|
+
if system == "linux":
|
|
131
|
+
device_path = f"/dev/video{device_idx}"
|
|
132
|
+
input_format = "v4l2"
|
|
133
|
+
cmd = ["ffprobe", "-f", input_format, "-i", device_path]
|
|
134
|
+
elif system == "windows":
|
|
135
|
+
# Windows DirectShow device
|
|
136
|
+
input_format = "dshow"
|
|
137
|
+
device_name = f"video={device_idx}" # This might need adjustment based on actual device names
|
|
138
|
+
cmd = ["ffprobe", "-f", input_format, "-i", device_name]
|
|
139
|
+
elif system == "darwin": # macOS
|
|
140
|
+
input_format = "avfoundation"
|
|
141
|
+
cmd = ["ffprobe", "-f", input_format, "-i", str(device_idx)]
|
|
142
|
+
else:
|
|
143
|
+
logging.warning(f"⚠️ [APP] Unsupported platform for FFmpeg device access: {system}")
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
# Add common ffprobe arguments
|
|
147
|
+
cmd.extend([
|
|
148
|
+
"-v", "error",
|
|
149
|
+
"-select_streams", "v:0",
|
|
150
|
+
"-show_entries", "stream=width,height,avg_frame_rate",
|
|
151
|
+
"-of", "json"
|
|
152
|
+
])
|
|
153
|
+
|
|
154
|
+
logging.debug(f"Running FFmpeg command: {' '.join(cmd)}")
|
|
155
|
+
|
|
156
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
|
157
|
+
|
|
158
|
+
if result.returncode != 0:
|
|
159
|
+
logging.warning(f"⚠️ [APP] FFmpeg failed for device {device_idx}: {result.stderr.strip()}")
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
if not result.stdout.strip():
|
|
163
|
+
logging.warning(f"⚠️ [APP] No output from FFmpeg for device {device_idx}")
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
metadata = json.loads(result.stdout)
|
|
167
|
+
streams = metadata.get("streams", [])
|
|
168
|
+
|
|
169
|
+
if not streams:
|
|
170
|
+
logging.warning(f"⚠️ [APP] No video streams found for device {device_idx}")
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
stream = streams[0]
|
|
174
|
+
width = stream.get("width")
|
|
175
|
+
height = stream.get("height")
|
|
176
|
+
avg_fps = stream.get("avg_frame_rate", "30/1")
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
frame_rate = round(float(fractions.Fraction(avg_fps)), 2)
|
|
180
|
+
except (ValueError, ZeroDivisionError):
|
|
181
|
+
frame_rate = 30.0
|
|
182
|
+
|
|
183
|
+
if not width or not height:
|
|
184
|
+
logging.warning(f"⚠️ [APP] Invalid resolution from FFmpeg for device {device_idx}")
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
logging.info(f"✅ [APP] Successfully probed device {device_idx} with FFmpeg: {width}x{height} @ {frame_rate}fps")
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
"resolution": f"{width}x{height}",
|
|
191
|
+
"frame_rate": frame_rate,
|
|
192
|
+
"timestamp": None
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
except subprocess.TimeoutExpired:
|
|
196
|
+
logging.warning(f"⚠️ [APP] FFmpeg timeout for device {device_idx}")
|
|
197
|
+
except json.JSONDecodeError:
|
|
198
|
+
logging.error(f"❌ [APP] Failed to parse FFmpeg output for device {device_idx}")
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logging.error(f"❌ [APP] Error probing device {device_idx} with FFmpeg: {e}")
|
|
201
|
+
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def _get_metadata_direct_device(device_index: str) -> dict:
|
|
206
|
+
try:
|
|
207
|
+
device_idx = int(device_index)
|
|
208
|
+
|
|
209
|
+
logging.debug(f"VideoSharingClient available: {VideoSharingClient is not None}")
|
|
210
|
+
|
|
211
|
+
# Try to use VideoSharingClient first for cross-process access
|
|
212
|
+
if VideoSharingClient:
|
|
213
|
+
try:
|
|
214
|
+
logging.debug("Attempting to use VideoSharingClient...")
|
|
215
|
+
|
|
216
|
+
# Get storage path from DatabaseManager
|
|
217
|
+
storage_path = None
|
|
218
|
+
if get_storage_path:
|
|
219
|
+
try:
|
|
220
|
+
storage_path = str(get_storage_path())
|
|
221
|
+
logging.debug(f"Got storage path: {storage_path}")
|
|
222
|
+
except Exception as e:
|
|
223
|
+
logging.debug(f"Could not get storage path: {e}")
|
|
224
|
+
|
|
225
|
+
# Create temporary client to get device properties
|
|
226
|
+
video_client = VideoSharingClient(device_idx, storage_path=storage_path)
|
|
227
|
+
logging.debug(f"Created VideoSharingClient, info_file: {video_client.info_file}")
|
|
228
|
+
|
|
229
|
+
# Load daemon info to get properties without connecting
|
|
230
|
+
if video_client._load_daemon_info():
|
|
231
|
+
width = video_client.width
|
|
232
|
+
height = video_client.height
|
|
233
|
+
fps = video_client.fps
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
"resolution": f"{width}x{height}",
|
|
237
|
+
"frame_rate": round(fps, 2) if fps > 0 else 30.0,
|
|
238
|
+
"timestamp": None
|
|
239
|
+
}
|
|
240
|
+
else:
|
|
241
|
+
logging.debug(f"Video sharing daemon not available for device {device_idx}")
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logging.debug(f"Video sharing not available for device {device_idx}: {e}")
|
|
245
|
+
import traceback
|
|
246
|
+
logging.debug(traceback.format_exc())
|
|
247
|
+
|
|
248
|
+
# Fallback 1: Try direct OpenCV access
|
|
249
|
+
logging.info(f"🔄 [APP] Daemon not available for device {device_idx}, falling back to direct OpenCV access")
|
|
250
|
+
metadata = VideoProbeUtil._get_metadata_opencv_direct_device(device_idx)
|
|
251
|
+
if metadata:
|
|
252
|
+
return metadata
|
|
253
|
+
|
|
254
|
+
# Fallback 2: Try FFmpeg for device access (Linux v4l2, Windows dshow)
|
|
255
|
+
logging.info(f"🔄 [APP] OpenCV failed for device {device_idx}, trying FFmpeg device access")
|
|
256
|
+
return VideoProbeUtil._get_metadata_ffmpeg_direct_device(device_idx)
|
|
257
|
+
|
|
258
|
+
except Exception as e:
|
|
259
|
+
logging.error(f"❌ [APP] Error getting metadata from direct device {device_index}: {e}")
|
|
260
|
+
return None
|
|
261
|
+
|
|
48
262
|
@staticmethod
|
|
49
263
|
def _detect_stream_type(video_url: str) -> str:
|
|
50
|
-
"""Detect whether the URL is RTSP, local file, or other type."""
|
|
51
|
-
# Convert PosixPath to string if needed
|
|
52
264
|
if hasattr(video_url, '__str__'):
|
|
53
265
|
video_url = str(video_url)
|
|
54
266
|
|
|
267
|
+
if isinstance(video_url, str) and video_url.isdigit():
|
|
268
|
+
return "direct"
|
|
269
|
+
|
|
55
270
|
parsed_url = urlparse(video_url)
|
|
56
271
|
if parsed_url.scheme == "rtsp":
|
|
57
272
|
return "rtsp"
|
|
@@ -62,45 +277,40 @@ class VideoProbeUtil:
|
|
|
62
277
|
|
|
63
278
|
@staticmethod
|
|
64
279
|
def _get_metadata_ffmpeg(video_url: str) -> dict:
|
|
65
|
-
# Check if ffprobe is available
|
|
66
280
|
if not shutil.which("ffprobe"):
|
|
67
281
|
logging.error("⚠️ [APP] ffprobe is not installed or not found in PATH.")
|
|
68
282
|
return None
|
|
69
283
|
|
|
70
|
-
# Detect stream type
|
|
71
284
|
stream_type = VideoProbeUtil._detect_stream_type(video_url)
|
|
72
285
|
|
|
73
|
-
|
|
286
|
+
if stream_type == "direct":
|
|
287
|
+
return VideoProbeUtil._get_metadata_direct_device(video_url)
|
|
288
|
+
|
|
74
289
|
cmd = ["ffprobe", "-v", "error", "-select_streams", "v:0",
|
|
75
290
|
"-show_entries", "stream=width,height,avg_frame_rate", "-of", "json"]
|
|
76
291
|
|
|
77
|
-
# Add RTSP transport option only for RTSP streams
|
|
78
292
|
if stream_type == "rtsp":
|
|
79
|
-
|
|
80
|
-
|
|
293
|
+
probe_options = get_rtsp_probe_options()
|
|
294
|
+
# Insert options at the beginning (after ffprobe)
|
|
295
|
+
for i, option in enumerate(probe_options):
|
|
296
|
+
cmd.insert(1 + i, option)
|
|
81
297
|
|
|
82
|
-
# Add the video URL
|
|
83
298
|
cmd.append(video_url)
|
|
84
299
|
|
|
85
300
|
try:
|
|
86
|
-
# Run ffprobe command
|
|
87
301
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
|
88
302
|
|
|
89
|
-
# Check for errors
|
|
90
303
|
if result.returncode != 0 or not result.stdout.strip():
|
|
91
304
|
logging.warning(f"⚠️ [APP] ffprobe failed for {video_url}: {result.stderr.strip()}")
|
|
92
305
|
return None
|
|
93
306
|
|
|
94
|
-
# Parse JSON output
|
|
95
307
|
metadata = json.loads(result.stdout)
|
|
96
308
|
streams = metadata.get("streams", [{}])[0]
|
|
97
309
|
|
|
98
|
-
# Extract metadata
|
|
99
310
|
width = streams.get("width")
|
|
100
311
|
height = streams.get("height")
|
|
101
312
|
avg_fps = streams.get("avg_frame_rate", "0/1")
|
|
102
313
|
|
|
103
|
-
# Convert FPS safely
|
|
104
314
|
try:
|
|
105
315
|
frame_rate = round(float(fractions.Fraction(avg_fps)), 2)
|
|
106
316
|
except (ValueError, ZeroDivisionError):
|
|
@@ -109,7 +319,7 @@ class VideoProbeUtil:
|
|
|
109
319
|
return {
|
|
110
320
|
"resolution": f"{width}x{height}" if width and height else None,
|
|
111
321
|
"frame_rate": frame_rate,
|
|
112
|
-
"timestamp": None
|
|
322
|
+
"timestamp": None
|
|
113
323
|
}
|
|
114
324
|
|
|
115
325
|
except subprocess.TimeoutExpired:
|
|
@@ -64,6 +64,7 @@ class DataSyncWorker:
|
|
|
64
64
|
"""Stop the data sync worker."""
|
|
65
65
|
logging.info("🛑 [DATA SYNC] Stopping DataSyncWorker.")
|
|
66
66
|
self.running = False
|
|
67
|
+
self.ai_model_client.cleanup_downloads()
|
|
67
68
|
safe_join_thread(self.thread)
|
|
68
69
|
logging.info("🛑 [DATA SYNC] DataSyncWorker stopped.")
|
|
69
70
|
|
|
@@ -112,7 +112,7 @@ class PipelineImageWorker:
|
|
|
112
112
|
response = self.worker_source_pipeline_client.send_pipeline_image(
|
|
113
113
|
worker_source_pipeline_id=worker_source_pipeline_id,
|
|
114
114
|
uuid=uuid,
|
|
115
|
-
url=worker_source.url if worker_source.type_code
|
|
115
|
+
url=worker_source.url if worker_source.type_code in ["live", "direct"] else worker_source.file_path,
|
|
116
116
|
token=self.token
|
|
117
117
|
)
|
|
118
118
|
|
|
@@ -4,6 +4,8 @@ import logging
|
|
|
4
4
|
import json
|
|
5
5
|
from ..database.DatabaseManager import get_storage_path
|
|
6
6
|
from ..services.FileToRTMPServer import FileToRTMPStreamer
|
|
7
|
+
from ..services.DirectDeviceToRTMPStreamer import DirectDeviceToRTMPStreamer
|
|
8
|
+
from ..services.SharedDirectDeviceClient import SharedDirectDeviceClient
|
|
7
9
|
from .RabbitMQListener import RabbitMQListener
|
|
8
10
|
from ..services.RTSPtoRTMPStreamer import RTSPtoRTMPStreamer
|
|
9
11
|
|
|
@@ -44,6 +46,9 @@ class VideoStreamWorker:
|
|
|
44
46
|
self.thread = None
|
|
45
47
|
self.stop_event = threading.Event()
|
|
46
48
|
self.lock = threading.Lock()
|
|
49
|
+
|
|
50
|
+
# Initialize shared device client for direct video devices
|
|
51
|
+
self.shared_device_client = SharedDirectDeviceClient()
|
|
47
52
|
|
|
48
53
|
# Initialize RabbitMQ listener
|
|
49
54
|
self.listener = RabbitMQListener(
|
|
@@ -75,6 +80,11 @@ class VideoStreamWorker:
|
|
|
75
80
|
safe_join_thread(self.thread) # Ensures the thread stops gracefully
|
|
76
81
|
self.thread = None
|
|
77
82
|
logger.info(f"🛑 [APP] Stream Worker stopped (Device: {self.worker_id}).")
|
|
83
|
+
|
|
84
|
+
def _is_direct_device(self, url) -> bool:
|
|
85
|
+
"""Check if the URL is a direct video device."""
|
|
86
|
+
is_device, _ = self.shared_device_client._is_direct_device(url)
|
|
87
|
+
return is_device
|
|
78
88
|
|
|
79
89
|
def _run(self):
|
|
80
90
|
"""Main loop to manage RabbitMQ listener."""
|
|
@@ -108,8 +118,19 @@ class VideoStreamWorker:
|
|
|
108
118
|
|
|
109
119
|
logger.info(f"📡 [APP] Received video preview message ({data})")
|
|
110
120
|
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
# Validate URL - support RTSP, worker-source files, and direct devices
|
|
122
|
+
if not url:
|
|
123
|
+
logger.error(f"⚠️ [APP] Missing URL in message")
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
is_valid_url = (
|
|
127
|
+
url.startswith("rtsp://") or
|
|
128
|
+
url.startswith("worker-source/") or
|
|
129
|
+
self._is_direct_device(url)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if not is_valid_url:
|
|
133
|
+
logger.error(f"⚠️ [APP] Invalid URL: {url} (must be RTSP, worker-source file, or direct device)")
|
|
113
134
|
return
|
|
114
135
|
|
|
115
136
|
if stream_duration <= 0:
|
|
@@ -131,13 +152,16 @@ class VideoStreamWorker:
|
|
|
131
152
|
logger.error("🚨 [APP] Error processing video preview message.", exc_info=True)
|
|
132
153
|
|
|
133
154
|
def _start_stream(self, url, rtmp_server, stream_key, stream_duration, worker_id):
|
|
134
|
-
"""Runs
|
|
155
|
+
"""Runs streaming to RTMP in a separate thread."""
|
|
135
156
|
try:
|
|
136
157
|
logger.info(f"🎥 [APP] Starting RTMP stream (Worker: {worker_id})")
|
|
137
158
|
|
|
138
159
|
if url.startswith("worker-source/"):
|
|
139
160
|
streamer = FileToRTMPStreamer(self.source_file_path / os.path.basename(url), rtmp_server, stream_key, stream_duration)
|
|
161
|
+
elif self._is_direct_device(url):
|
|
162
|
+
streamer = DirectDeviceToRTMPStreamer(url, rtmp_server, stream_key, stream_duration)
|
|
140
163
|
else:
|
|
164
|
+
# Assume RTSP or other supported protocols
|
|
141
165
|
streamer = RTSPtoRTMPStreamer(url, rtmp_server, stream_key, stream_duration)
|
|
142
166
|
|
|
143
167
|
streamer.start_stream()
|
|
@@ -18,6 +18,7 @@ class WorkerManager:
|
|
|
18
18
|
self.config = config
|
|
19
19
|
self.worker_id = self.config.get("worker_id")
|
|
20
20
|
self.server_host = self.config.get("server_host")
|
|
21
|
+
self.server_port = self.config.get("server_port", 50051)
|
|
21
22
|
self.token = self.config.get("token")
|
|
22
23
|
|
|
23
24
|
if not self.worker_id:
|
|
@@ -29,7 +30,7 @@ class WorkerManager:
|
|
|
29
30
|
|
|
30
31
|
# Configure the centralized gRPC client manager
|
|
31
32
|
self.client_manager = GrpcClientManager.get_instance()
|
|
32
|
-
self.client_manager.configure(self.server_host)
|
|
33
|
+
self.client_manager.configure(self.server_host, self.server_port)
|
|
33
34
|
|
|
34
35
|
# Get shared client instance
|
|
35
36
|
self.status_client = self.client_manager.get_client(WorkerStatusClient)
|
|
@@ -46,16 +47,9 @@ class WorkerManager:
|
|
|
46
47
|
"""Start processing workers while keeping monitoring workers running."""
|
|
47
48
|
try:
|
|
48
49
|
self.video_stream_worker.start()
|
|
49
|
-
logger.info("🚀 [APP] Video Stream Worker started.")
|
|
50
|
-
|
|
51
50
|
self.pipeline_image_worker.start()
|
|
52
|
-
logger.info("🚀 [APP] Pipeline Image Worker started.")
|
|
53
|
-
|
|
54
51
|
self.data_sender_worker.start_updating()
|
|
55
|
-
logger.info("🚀 [APP] Data Sender Worker started updating.")
|
|
56
|
-
|
|
57
52
|
self.dataset_frame_worker.start()
|
|
58
|
-
logger.info("🚀 [APP] Dataset Frame Worker started.")
|
|
59
53
|
|
|
60
54
|
self._update_status("run")
|
|
61
55
|
|
|
@@ -66,16 +60,9 @@ class WorkerManager:
|
|
|
66
60
|
"""Stop processing workers while keeping monitoring workers running."""
|
|
67
61
|
try:
|
|
68
62
|
self.video_stream_worker.stop()
|
|
69
|
-
logger.info("🛑 [APP] Video Stream Worker stopped.")
|
|
70
|
-
|
|
71
63
|
self.pipeline_image_worker.stop()
|
|
72
|
-
logger.info("🛑 [APP] Pipeline Image Worker stopped.")
|
|
73
|
-
|
|
74
64
|
self.data_sender_worker.stop_updating()
|
|
75
|
-
logger.info("🛑 [APP] Data Sender Worker stopped updating.")
|
|
76
|
-
|
|
77
65
|
self.dataset_frame_worker.stop()
|
|
78
|
-
logger.info("🛑 [APP] Dataset Frame Worker stopped.")
|
|
79
66
|
|
|
80
67
|
self._update_status("stop")
|
|
81
68
|
|
|
@@ -87,16 +74,9 @@ class WorkerManager:
|
|
|
87
74
|
try:
|
|
88
75
|
# Start monitoring workers first
|
|
89
76
|
self.core_action_worker.start()
|
|
90
|
-
logging.info("🚀 [APP] Core Action Worker started and listening for commands.")
|
|
91
|
-
|
|
92
77
|
self.data_sync_worker.start()
|
|
93
|
-
logger.info("🚀 [APP] Data Sync Worker started.")
|
|
94
|
-
|
|
95
78
|
self.data_sender_worker.start()
|
|
96
|
-
logger.info("🚀 [APP] Data Sender Worker started.")
|
|
97
|
-
|
|
98
79
|
self.pipeline_action_worker.start()
|
|
99
|
-
logger.info("🚀 [APP] Pipeline Action Worker started.")
|
|
100
80
|
|
|
101
81
|
self._start_workers()
|
|
102
82
|
|
|
@@ -109,16 +89,9 @@ class WorkerManager:
|
|
|
109
89
|
"""Stop all workers including monitoring workers."""
|
|
110
90
|
try:
|
|
111
91
|
self.core_action_worker.stop()
|
|
112
|
-
logger.info("🛑 [APP] Core Action Worker stopped.")
|
|
113
|
-
|
|
114
92
|
self.data_sync_worker.stop()
|
|
115
|
-
logger.info("🛑 [APP] Data Sync Worker stopped.")
|
|
116
|
-
|
|
117
93
|
self.data_sender_worker.stop()
|
|
118
|
-
logger.info("🛑 [APP] Data Sender Worker stopped.")
|
|
119
|
-
|
|
120
94
|
self.pipeline_action_worker.stop()
|
|
121
|
-
logger.info("🛑 [APP] Pipeline Action Worker stopped.")
|
|
122
95
|
|
|
123
96
|
self._stop_workers()
|
|
124
97
|
|
|
@@ -4,11 +4,9 @@ import signal
|
|
|
4
4
|
import sys
|
|
5
5
|
import logging
|
|
6
6
|
|
|
7
|
-
# Set multiprocessing start method to 'spawn' for CUDA compatibility
|
|
8
7
|
try:
|
|
9
8
|
multiprocessing.set_start_method('spawn', force=True)
|
|
10
9
|
except RuntimeError:
|
|
11
|
-
# Method already set, ignore
|
|
12
10
|
pass
|
|
13
11
|
|
|
14
12
|
from .initializer.AppInitializer import AppInitializer
|
|
@@ -17,7 +15,7 @@ from .config.ConfigurationManager import ConfigurationManager
|
|
|
17
15
|
from .util.HardwareID import HardwareID
|
|
18
16
|
from .services.GrpcClientBase import set_auth_failure_callback
|
|
19
17
|
from .database.DatabaseManager import set_storage_path
|
|
20
|
-
from . import models
|
|
18
|
+
from . import models
|
|
21
19
|
|
|
22
20
|
|
|
23
21
|
class WorkerService:
|
|
@@ -33,6 +31,7 @@ class WorkerService:
|
|
|
33
31
|
system_usage_interval: int = 30,
|
|
34
32
|
rtmp_server: str = "rtmp://live.vision.sindika.co.id:1935/live",
|
|
35
33
|
storage_path: str = "data",
|
|
34
|
+
server_port: int = 50051,
|
|
36
35
|
):
|
|
37
36
|
"""
|
|
38
37
|
Initialize the worker service.
|
|
@@ -43,6 +42,7 @@ class WorkerService:
|
|
|
43
42
|
system_usage_interval: Interval for system usage reporting (default: 30)
|
|
44
43
|
rtmp_server: RTMP server URL for video streaming
|
|
45
44
|
storage_path: Storage path for databases and files (default: 'data')
|
|
45
|
+
server_port: gRPC server port (default: 50051)
|
|
46
46
|
"""
|
|
47
47
|
# Set the global storage path before any database operations
|
|
48
48
|
set_storage_path(storage_path)
|
|
@@ -55,6 +55,7 @@ class WorkerService:
|
|
|
55
55
|
self.system_usage_interval = system_usage_interval
|
|
56
56
|
self.rtmp_server = rtmp_server
|
|
57
57
|
self.storage_path = storage_path
|
|
58
|
+
self.server_port = server_port
|
|
58
59
|
self.config = None
|
|
59
60
|
self.auth_failure_detected = False
|
|
60
61
|
|
|
@@ -119,10 +120,13 @@ class WorkerService:
|
|
|
119
120
|
# Initialize with token
|
|
120
121
|
AppInitializer.initialize_configuration(hardware_id, server_host, self.token)
|
|
121
122
|
|
|
123
|
+
# Set server_port in config for first-time setup
|
|
124
|
+
ConfigurationManager.set_config("server_port", str(self.server_port))
|
|
125
|
+
|
|
122
126
|
# Get configuration
|
|
123
127
|
config = ConfigurationManager.get_all_configs()
|
|
124
128
|
else:
|
|
125
|
-
# Check if server_host or token has changed and update if needed
|
|
129
|
+
# Check if server_host, server_port, or token has changed and update if needed
|
|
126
130
|
config_updated = False
|
|
127
131
|
|
|
128
132
|
if config['server_host'] != server_host:
|
|
@@ -130,6 +134,12 @@ class WorkerService:
|
|
|
130
134
|
config_updated = True
|
|
131
135
|
self.logger.info(f"✅ [APP] Updated server host to: {server_host}")
|
|
132
136
|
|
|
137
|
+
# Check if server_port has changed and update if needed
|
|
138
|
+
if str(config.get('server_port')) != str(self.server_port):
|
|
139
|
+
ConfigurationManager.set_config("server_port", str(self.server_port))
|
|
140
|
+
config_updated = True
|
|
141
|
+
self.logger.info(f"✅ [APP] Updated server port to: {self.server_port}")
|
|
142
|
+
|
|
133
143
|
# Check if token has changed and update if needed
|
|
134
144
|
if self.token and config.get('token') != self.token:
|
|
135
145
|
ConfigurationManager.set_config("token", self.token)
|
|
@@ -240,6 +250,12 @@ def main():
|
|
|
240
250
|
default="be.vision.sindika.co.id",
|
|
241
251
|
help="Manager server host (default: be.vision.sindika.co.id)"
|
|
242
252
|
)
|
|
253
|
+
parser.add_argument(
|
|
254
|
+
"--server-port",
|
|
255
|
+
type=int,
|
|
256
|
+
default=50051,
|
|
257
|
+
help="gRPC server port (default: 50051)"
|
|
258
|
+
)
|
|
243
259
|
parser.add_argument(
|
|
244
260
|
"--system-usage-interval",
|
|
245
261
|
type=int,
|
|
@@ -251,10 +267,11 @@ def main():
|
|
|
251
267
|
# Create and run worker service
|
|
252
268
|
service = WorkerService(
|
|
253
269
|
server_host=args.server_host,
|
|
270
|
+
server_port=args.server_port,
|
|
254
271
|
system_usage_interval=args.system_usage_interval
|
|
255
272
|
)
|
|
256
273
|
service.run()
|
|
257
274
|
|
|
258
275
|
|
|
259
276
|
if __name__ == "__main__":
|
|
260
|
-
main()
|
|
277
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nedo-vision-worker
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: Nedo Vision Worker Service Library for AI Vision Processing
|
|
5
5
|
Author-email: Willy Achmat Fauzi <willy.achmat@gmail.com>
|
|
6
6
|
Maintainer-email: Willy Achmat Fauzi <willy.achmat@gmail.com>
|
|
@@ -179,10 +179,8 @@ This will check:
|
|
|
179
179
|
- ✅ Python version and dependencies
|
|
180
180
|
- ✅ FFmpeg installation and functionality
|
|
181
181
|
- ✅ OpenCV installation and optimizations
|
|
182
|
-
- ✅ gRPC connectivity
|
|
183
182
|
- ✅ NVIDIA GPU support and capabilities
|
|
184
183
|
- ✅ Storage permissions
|
|
185
|
-
- ✅ Network connectivity
|
|
186
184
|
|
|
187
185
|
## 📖 Quick Start
|
|
188
186
|
|