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.
Files changed (44) hide show
  1. nedo_vision_worker/__init__.py +1 -1
  2. nedo_vision_worker/cli.py +196 -167
  3. nedo_vision_worker/database/DatabaseManager.py +3 -3
  4. nedo_vision_worker/doctor.py +1066 -386
  5. nedo_vision_worker/models/ai_model.py +35 -2
  6. nedo_vision_worker/protos/AIModelService_pb2.py +12 -10
  7. nedo_vision_worker/protos/AIModelService_pb2_grpc.py +1 -1
  8. nedo_vision_worker/protos/DatasetSourceService_pb2.py +2 -2
  9. nedo_vision_worker/protos/DatasetSourceService_pb2_grpc.py +1 -1
  10. nedo_vision_worker/protos/HumanDetectionService_pb2.py +2 -2
  11. nedo_vision_worker/protos/HumanDetectionService_pb2_grpc.py +1 -1
  12. nedo_vision_worker/protos/PPEDetectionService_pb2.py +2 -2
  13. nedo_vision_worker/protos/PPEDetectionService_pb2_grpc.py +1 -1
  14. nedo_vision_worker/protos/VisionWorkerService_pb2.py +2 -2
  15. nedo_vision_worker/protos/VisionWorkerService_pb2_grpc.py +1 -1
  16. nedo_vision_worker/protos/WorkerSourcePipelineService_pb2.py +2 -2
  17. nedo_vision_worker/protos/WorkerSourcePipelineService_pb2_grpc.py +1 -1
  18. nedo_vision_worker/protos/WorkerSourceService_pb2.py +2 -2
  19. nedo_vision_worker/protos/WorkerSourceService_pb2_grpc.py +1 -1
  20. nedo_vision_worker/services/AIModelClient.py +184 -160
  21. nedo_vision_worker/services/DirectDeviceToRTMPStreamer.py +534 -0
  22. nedo_vision_worker/services/GrpcClientBase.py +142 -108
  23. nedo_vision_worker/services/PPEDetectionClient.py +0 -7
  24. nedo_vision_worker/services/RestrictedAreaClient.py +0 -5
  25. nedo_vision_worker/services/SharedDirectDeviceClient.py +278 -0
  26. nedo_vision_worker/services/SharedVideoStreamServer.py +315 -0
  27. nedo_vision_worker/services/SystemWideDeviceCoordinator.py +236 -0
  28. nedo_vision_worker/services/VideoSharingDaemon.py +832 -0
  29. nedo_vision_worker/services/VideoStreamClient.py +43 -20
  30. nedo_vision_worker/services/WorkerSourceClient.py +1 -1
  31. nedo_vision_worker/services/WorkerSourcePipelineClient.py +35 -12
  32. nedo_vision_worker/services/WorkerSourceUpdater.py +30 -3
  33. nedo_vision_worker/util/FFmpegUtil.py +124 -0
  34. nedo_vision_worker/util/VideoProbeUtil.py +227 -17
  35. nedo_vision_worker/worker/DataSyncWorker.py +1 -0
  36. nedo_vision_worker/worker/PipelineImageWorker.py +1 -1
  37. nedo_vision_worker/worker/VideoStreamWorker.py +27 -3
  38. nedo_vision_worker/worker/WorkerManager.py +2 -29
  39. nedo_vision_worker/worker_service.py +22 -5
  40. {nedo_vision_worker-1.1.3.dist-info → nedo_vision_worker-1.2.1.dist-info}/METADATA +1 -3
  41. {nedo_vision_worker-1.1.3.dist-info → nedo_vision_worker-1.2.1.dist-info}/RECORD +44 -38
  42. {nedo_vision_worker-1.1.3.dist-info → nedo_vision_worker-1.2.1.dist-info}/WHEEL +0 -0
  43. {nedo_vision_worker-1.1.3.dist-info → nedo_vision_worker-1.2.1.dist-info}/entry_points.txt +0 -0
  44. {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
- # metadata = VideoProbeUtil._get_metadata_opencv(video_url)
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
- # Build ffprobe command based on stream type
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
- cmd.insert(1, "-rtsp_transport")
80
- cmd.insert(2, "tcp")
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 # Placeholder if you need a timestamp later
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 == "live" else worker_source.file_path,
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
- if not url or (not url.startswith("rtsp://") and not url.startswith("worker-source/")):
112
- logger.error(f"⚠️ [APP] Invalid URL: {url}")
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 RTSP-to-RTMP streaming in a separate thread."""
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 # Ensure all models are registered before DB init
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
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