nedo-vision-worker 1.2.0__py3-none-any.whl → 1.2.2__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.
@@ -6,5 +6,5 @@ A library for running worker agents in the Nedo Vision platform.
6
6
 
7
7
  from .worker_service import WorkerService
8
8
 
9
- __version__ = "1.2.0"
9
+ __version__ = "1.2.2"
10
10
  __all__ = ["WorkerService"]
nedo_vision_worker/cli.py CHANGED
@@ -49,7 +49,7 @@ Examples:
49
49
  parser.add_argument(
50
50
  "--version",
51
51
  action="version",
52
- version="nedo-vision-worker 1.2.0"
52
+ version="nedo-vision-worker 1.2.1"
53
53
  )
54
54
 
55
55
  subparsers = parser.add_subparsers(
@@ -87,8 +87,8 @@ class WorkerServiceDoctor:
87
87
  def check_python_environment(self) -> None:
88
88
  """Comprehensive Python environment validation."""
89
89
  version = sys.version_info
90
- min_version = (3, 8)
91
- recommended_version = (3, 9)
90
+ min_version = (3, 10)
91
+ recommended_version = (3, 10)
92
92
 
93
93
  details = [
94
94
  f"Python {version.major}.{version.minor}.{version.micro}",
@@ -891,6 +891,36 @@ class WorkerServiceDoctor:
891
891
  performance_impact=performance_impact
892
892
  ))
893
893
 
894
+ def check_psutil_installation(self) -> None:
895
+ """Check if psutil is installed for system monitoring."""
896
+ details = []
897
+ recommendations = []
898
+ try:
899
+ import psutil
900
+ version = getattr(psutil, '__version__', 'N/A')
901
+ details.append(f"psutil version: {version}")
902
+ status = HealthStatus.GOOD
903
+ message = "psutil is installed"
904
+ is_blocking = False
905
+ performance_impact = "None"
906
+ except ImportError:
907
+ status = HealthStatus.WARNING
908
+ message = "psutil not installed"
909
+ details.append("System resource monitoring will be disabled.")
910
+ recommendations.append("Install psutil for system monitoring: pip install psutil")
911
+ is_blocking = False
912
+ performance_impact = "Low"
913
+
914
+ self._add_result(HealthCheck(
915
+ name="System Monitoring (psutil)",
916
+ status=status,
917
+ message=message,
918
+ details=details,
919
+ recommendations=recommendations if recommendations else None,
920
+ is_blocking=is_blocking,
921
+ performance_impact=performance_impact
922
+ ))
923
+
894
924
  def run_comprehensive_health_check(self) -> List[HealthCheck]:
895
925
  """Execute all health checks with progress indication."""
896
926
  print("🏥 Nedo Vision Worker Service - Comprehensive Health Check")
@@ -17,6 +17,15 @@ class WorkerSourceRepository:
17
17
  except Exception as e:
18
18
  logger.error(f"🚨 [APP] Database error while fetching worker sources: {e}", exc_info=True)
19
19
  return []
20
+
21
+
22
+ def get_worker_sources_by_worker_id(self, worker_id: str):
23
+ """Retrieve all worker sources from the database."""
24
+ try:
25
+ return self.session.query(WorkerSourceEntity).filter_by(worker_id=worker_id).all()
26
+ except Exception as e:
27
+ logger.error(f"🚨 [APP] Database error while fetching worker sources: {e}", exc_info=True)
28
+ return []
20
29
 
21
30
  def bulk_update_worker_sources(self, updated_records):
22
31
  """Batch update worker sources in the database."""
@@ -43,4 +52,4 @@ class WorkerSourceRepository:
43
52
  return None
44
53
  except Exception as e:
45
54
  logger.error(f"🚨 [APP] Database error while fetching worker source by ID {worker_source_id}: {e}", exc_info=True)
46
- return None
55
+ return None
@@ -68,7 +68,7 @@ class GrpcClientBase:
68
68
  self.connected = False
69
69
  error_msg = str(e)
70
70
 
71
- logger.error(f"⚠️ Connection failed ({attempts}/{self.max_retries}): {error_msg}")
71
+ logger.error(f"⚠️ Connection failed ({attempts}/{self.max_retries}): {error_msg}", exc_info=True)
72
72
 
73
73
  if attempts < self.max_retries:
74
74
  sleep_time = retry_interval * (2 ** (attempts - 1))
@@ -8,6 +8,7 @@ import fractions
8
8
  from urllib.parse import urlparse
9
9
  from .GrpcClientBase import GrpcClientBase
10
10
  from .SharedDirectDeviceClient import SharedDirectDeviceClient
11
+ from ..util.FFmpegUtil import get_rtsp_ffmpeg_options, get_rtsp_probe_options
11
12
  from ..protos.VisionWorkerService_pb2_grpc import VideoStreamServiceStub
12
13
  from ..protos.VisionWorkerService_pb2 import VideoFrame
13
14
 
@@ -47,8 +48,10 @@ class VideoStreamClient(GrpcClientBase):
47
48
  ]
48
49
 
49
50
  if stream_type == "rtsp":
50
- probe_cmd.insert(1, "-rtsp_transport")
51
- probe_cmd.insert(2, "tcp")
51
+ probe_options = get_rtsp_probe_options()
52
+ # Insert options at the beginning (after ffprobe)
53
+ for i, option in enumerate(probe_options):
54
+ probe_cmd.insert(1 + i, option)
52
55
 
53
56
  result = subprocess.run(probe_cmd, capture_output=True, text=True)
54
57
  probe_data = json.loads(result.stdout)
@@ -97,10 +100,8 @@ class VideoStreamClient(GrpcClientBase):
97
100
  logging.error(f"Failed to create ffmpeg input for direct device: {e}")
98
101
  return
99
102
  elif stream_type == "rtsp":
100
- ffmpeg_input = (
101
- ffmpeg
102
- .input(url, rtsp_transport="tcp", fflags="nobuffer", timeout="5000000")
103
- )
103
+ rtsp_options = get_rtsp_ffmpeg_options()
104
+ ffmpeg_input = ffmpeg.input(url, **rtsp_options)
104
105
  elif stream_type == "hls":
105
106
  ffmpeg_input = (
106
107
  ffmpeg
@@ -148,8 +149,13 @@ class VideoStreamClient(GrpcClientBase):
148
149
  if stream_type == "direct":
149
150
  self.shared_device_client.release_device_access(url)
150
151
 
151
- stderr_output = process.stderr.read().decode()
152
- logging.error(f"FFmpeg stderr: {stderr_output}")
152
+ try:
153
+ stderr_output = process.stderr.read().decode()
154
+ if stderr_output.strip(): # Only log if there's actual error content
155
+ logging.error(f"FFmpeg stderr for {stream_type} stream: {stderr_output}")
156
+ except Exception as e:
157
+ logging.warning(f"Could not read FFmpeg stderr: {e}")
158
+
153
159
  process.terminate()
154
160
  process.wait()
155
161
 
@@ -1,17 +1,28 @@
1
1
  import logging
2
2
  import os
3
3
  import time
4
- import ffmpeg
4
+ import subprocess
5
5
  from urllib.parse import urlparse
6
6
  from pathlib import Path
7
7
 
8
8
  from ..database.DatabaseManager import _get_storage_paths
9
9
  from ..repositories.WorkerSourcePipelineDebugRepository import WorkerSourcePipelineDebugRepository
10
10
  from ..repositories.WorkerSourcePipelineDetectionRepository import WorkerSourcePipelineDetectionRepository
11
+ from ..util.FFmpegUtil import (
12
+ get_rtsp_ffmpeg_options,
13
+ get_stream_timeout_duration,
14
+ get_ffmpeg_version,
15
+ )
11
16
  from .GrpcClientBase import GrpcClientBase
12
17
  from .SharedDirectDeviceClient import SharedDirectDeviceClient
13
18
  from ..protos.WorkerSourcePipelineService_pb2_grpc import WorkerSourcePipelineServiceStub
14
- from ..protos.WorkerSourcePipelineService_pb2 import GetListByWorkerIdRequest, SendPipelineImageRequest, UpdatePipelineStatusRequest, SendPipelineDebugRequest, SendPipelineDetectionDataRequest
19
+ from ..protos.WorkerSourcePipelineService_pb2 import (
20
+ GetListByWorkerIdRequest,
21
+ SendPipelineImageRequest,
22
+ UpdatePipelineStatusRequest,
23
+ SendPipelineDebugRequest,
24
+ SendPipelineDetectionDataRequest,
25
+ )
15
26
  from ..repositories.WorkerSourcePipelineRepository import WorkerSourcePipelineRepository
16
27
 
17
28
 
@@ -22,12 +33,11 @@ class WorkerSourcePipelineClient(GrpcClientBase):
22
33
  self.debug_repo = WorkerSourcePipelineDebugRepository()
23
34
  self.detection_repo = WorkerSourcePipelineDetectionRepository()
24
35
  storage_paths = _get_storage_paths()
25
- self.source_file_path = storage_paths["files"] / "source_files"
36
+ self.source_file_path: Path = storage_paths["files"] / "source_files"
26
37
  self.shared_device_client = SharedDirectDeviceClient()
27
-
28
- # Track video playback positions and last fetch times
29
- self.video_positions = {} # {video_path: current_position_in_seconds}
30
- self.last_fetch_times = {} # {video_path: last_fetch_timestamp}
38
+
39
+ self.video_positions = {}
40
+ self.last_fetch_times = {}
31
41
 
32
42
  try:
33
43
  self.connect(WorkerSourcePipelineServiceStub)
@@ -35,132 +45,127 @@ class WorkerSourcePipelineClient(GrpcClientBase):
35
45
  logging.error(f"Failed to connect to gRPC server: {e}")
36
46
  self.stub = None
37
47
 
48
+
49
+ # ---------- small helpers ----------
50
+
51
+ @staticmethod
52
+ def _opts_dict_to_cli(opts: dict) -> list:
53
+ out = []
54
+ for k, v in opts.items():
55
+ out += [f"-{k}", str(v)]
56
+ return out
57
+
58
+ @staticmethod
59
+ def _strip_timeout_keys(d: dict) -> dict:
60
+ o = dict(d)
61
+ o.pop("rw_timeout", None)
62
+ o.pop("stimeout", None)
63
+ o.pop("timeout", None)
64
+ return o
65
+
66
+ @staticmethod
67
+ def _rtsp_timeout_flag_by_version() -> str:
68
+ major, minor, patch = get_ffmpeg_version()
69
+ return "-timeout" if major >= 5 else "-stimeout"
70
+
71
+
72
+ # ---------- stream detection & video position ----------
73
+
38
74
  def _detect_stream_type(self, url):
39
75
  if isinstance(url, str) and url.isdigit():
40
76
  return "direct"
41
-
77
+
42
78
  parsed_url = urlparse(url)
43
79
  if parsed_url.scheme == "rtsp":
44
80
  return "rtsp"
45
- elif parsed_url.scheme in ["http", "https"] and url.endswith(".m3u8"):
81
+ if parsed_url.scheme in ["http", "https"] and url.endswith(".m3u8"):
46
82
  return "hls"
47
- elif url.startswith("worker-source/"):
83
+ if url.startswith("worker-source/"):
48
84
  file_path = self.source_file_path / os.path.basename(url)
49
85
  if file_path.exists():
50
- video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v']
51
- if file_path.suffix.lower() in video_extensions:
86
+ if file_path.suffix.lower() in (".mp4", ".avi", ".mov", ".mkv", ".wmv", ".flv", ".webm", ".m4v"):
52
87
  return "video_file"
53
88
  return "image_file"
54
- else:
55
- return "unknown"
56
-
89
+ return "unknown"
90
+
57
91
  def _get_video_duration(self, file_path):
58
- """Get the duration of a video file in seconds."""
59
92
  try:
60
93
  file_path_str = str(file_path)
61
94
  if not os.path.exists(file_path_str):
62
95
  logging.error(f"Video file does not exist: {file_path_str}")
63
96
  return None
64
- import subprocess
97
+
65
98
  import json
66
- cmd = [
67
- 'ffprobe',
68
- '-v', 'quiet',
69
- '-print_format', 'json',
70
- '-show_format',
71
- file_path_str
72
- ]
99
+ cmd = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", file_path_str]
73
100
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
74
101
  if result.returncode != 0:
75
102
  logging.error(f"FFprobe failed for {file_path_str}: {result.stderr}")
76
103
  return None
104
+
77
105
  try:
78
106
  probe_data = json.loads(result.stdout)
79
107
  except json.JSONDecodeError as e:
80
108
  logging.error(f"Failed to parse ffprobe output for {file_path_str}: {e}")
81
109
  return None
82
- if 'format' not in probe_data or 'duration' not in probe_data['format']:
110
+
111
+ if "format" not in probe_data or "duration" not in probe_data["format"]:
83
112
  logging.error(f"No duration found in probe result for {file_path_str}")
84
113
  return None
85
- duration = probe_data['format']['duration']
86
- # Defensive: ensure duration is a float or convertible to float
114
+
87
115
  try:
88
- duration_val = float(duration)
116
+ duration_val = float(probe_data["format"]["duration"])
89
117
  except Exception as e:
90
- logging.error(f"Duration value not convertible to float: {duration} ({type(duration)}) - {e}", exc_info=True)
118
+ logging.error(f"Duration value not convertible to float: {e}", exc_info=True)
91
119
  return None
120
+
92
121
  if isinstance(duration_val, bool):
93
- logging.error(f"Duration value is boolean, which is invalid: {duration_val}")
122
+ logging.error("Duration value is boolean, which is invalid")
94
123
  return None
124
+
95
125
  return duration_val
126
+
96
127
  except Exception as e:
97
128
  logging.error(f"Error getting video duration for {file_path}: {e}", exc_info=True)
98
129
  return None
99
-
130
+
100
131
  def _get_current_video_position(self, video_path):
101
- """Get or advance the current playback position for a video file based on real time elapsed."""
102
- current_time = time.time()
103
-
132
+ now = time.time()
133
+
104
134
  if video_path not in self.video_positions:
105
135
  self.video_positions[video_path] = 0.0
106
- self.last_fetch_times[video_path] = current_time
136
+ self.last_fetch_times[video_path] = now
107
137
  return 0.0
108
-
138
+
109
139
  current_pos = self.video_positions[video_path]
110
140
  last_fetch_time = self.last_fetch_times[video_path]
111
-
112
- # Calculate time elapsed since last fetch
113
- time_elapsed = current_time - last_fetch_time
114
-
115
- # Advance position by the actual time elapsed
116
- current_pos += time_elapsed
117
-
118
- # Get video duration to handle looping
141
+ current_pos += (now - last_fetch_time)
142
+
119
143
  duration = self._get_video_duration(video_path)
120
144
  if duration is not None and isinstance(duration, (int, float)):
121
- # Loop back to beginning if we've reached the end
122
145
  if current_pos >= duration:
123
146
  current_pos = 0.0
124
147
  else:
125
- # Default to 120 seconds if we can't get duration
126
148
  if current_pos >= 120.0:
127
149
  current_pos = 0.0
128
-
129
- # Update the stored position and fetch time
150
+
130
151
  self.video_positions[video_path] = current_pos
131
- self.last_fetch_times[video_path] = current_time
132
-
152
+ self.last_fetch_times[video_path] = now
133
153
  return current_pos
134
-
135
- def reset_video_position(self, video_path):
136
- """Reset the playback position for a specific video file."""
137
- if video_path in self.video_positions:
138
- self.video_positions[video_path] = 0.0
139
- self.last_fetch_times[video_path] = time.time()
140
- logging.info(f"Reset video position for {video_path}")
141
-
142
- def reset_all_video_positions(self):
143
- """Reset all video playback positions."""
144
- self.video_positions.clear()
145
- self.last_fetch_times.clear()
146
- logging.info("Reset all video positions")
147
-
154
+
148
155
  def get_video_positions_status(self):
149
- """Get the current status of all video positions for debugging."""
150
156
  status = {}
151
157
  for video_path, position in self.video_positions.items():
152
158
  duration = self._get_video_duration(video_path)
153
159
  last_fetch_time = self.last_fetch_times.get(video_path, None)
154
160
  time_since_last_fetch = time.time() - last_fetch_time if last_fetch_time else None
155
-
161
+
156
162
  if duration:
157
- progress = (position / duration) * 100
158
163
  status[video_path] = {
159
164
  "current_position": position,
160
165
  "duration": duration,
161
- "progress_percent": progress,
166
+ "progress_percent": (position / duration) * 100,
162
167
  "last_fetch_time": last_fetch_time,
163
- "time_since_last_fetch": time_since_last_fetch
168
+ "time_since_last_fetch": time_since_last_fetch,
164
169
  }
165
170
  else:
166
171
  status[video_path] = {
@@ -168,108 +173,126 @@ class WorkerSourcePipelineClient(GrpcClientBase):
168
173
  "duration": None,
169
174
  "progress_percent": None,
170
175
  "last_fetch_time": last_fetch_time,
171
- "time_since_last_fetch": time_since_last_fetch
176
+ "time_since_last_fetch": time_since_last_fetch,
172
177
  }
173
178
  return status
174
-
179
+
180
+
181
+ # ---------- ffmpeg cmd builders ----------
182
+
183
+ def _build_ffmpeg_cmd_rtsp(self, url: str) -> list:
184
+ base_opts = self._strip_timeout_keys(get_rtsp_ffmpeg_options())
185
+ timeout_flag = self._rtsp_timeout_flag_by_version()
186
+ in_args = self._opts_dict_to_cli(base_opts) + ["-rtsp_transport", "tcp", timeout_flag, "5000000", "-i", url]
187
+ return ["ffmpeg", "-hide_banner", "-loglevel", "error"] + in_args + [
188
+ "-vframes", "1", "-q:v", "2", "-f", "mjpeg", "pipe:1"
189
+ ]
190
+
191
+ def _build_ffmpeg_cmd_hls(self, url: str) -> list:
192
+ in_args = ["-f", "hls", "-analyzeduration", "10000000", "-probesize", "10000000", "-i", url]
193
+ return ["ffmpeg", "-hide_banner", "-loglevel", "error"] + in_args + [
194
+ "-vframes", "1", "-q:v", "2", "-f", "mjpeg", "pipe:1"
195
+ ]
196
+
197
+ def _build_ffmpeg_cmd_video_file(self, file_path: str, pos: float) -> list:
198
+ in_args = ["-ss", f"{pos:.3f}", "-i", file_path]
199
+ return ["ffmpeg", "-hide_banner", "-loglevel", "error"] + in_args + [
200
+ "-vframes", "1", "-q:v", "2", "-f", "mjpeg", "pipe:1"
201
+ ]
202
+
203
+ def _build_ffmpeg_cmd_image_file(self, file_path: str) -> list:
204
+ in_args = ["-i", file_path]
205
+ return ["ffmpeg", "-hide_banner", "-loglevel", "error"] + in_args + [
206
+ "-vframes", "1", "-q:v", "2", "-f", "mjpeg", "pipe:1"
207
+ ]
208
+
209
+
210
+ # ---------- frame capture ----------
211
+
175
212
  def _get_single_frame_bytes(self, url):
176
213
  stream_type = self._detect_stream_type(url)
177
-
178
- if stream_type == "direct":
179
- device_index = int(url)
180
- logging.info(f"📹 [APP] Capturing frame from direct video device: {device_index}")
181
-
182
- # Use the shared device client for direct devices
183
- try:
184
- # Get device properties first
214
+ proc = None
215
+
216
+ try:
217
+ if stream_type == "direct":
218
+ device_index = int(url)
219
+ logging.info(f"📹 [APP] Capturing frame from direct device: {device_index}")
220
+
185
221
  width, height, fps, pixel_format = self.shared_device_client.get_video_properties(url)
186
222
  if not width or not height:
187
223
  logging.error(f"Failed to get properties for device {device_index}")
188
224
  return None
189
-
190
- # Create ffmpeg input using shared device client
191
- ffmpeg_input = self.shared_device_client.create_ffmpeg_input(url, width, height, fps)
192
-
193
- except Exception as e:
194
- logging.error(f"Error setting up direct device {device_index}: {e}")
225
+
226
+ cmd = self.shared_device_client.create_ffmpeg_cli(url, width, height, fps)
227
+ cmd += ["-vframes", "1", "-q:v", "2", "-f", "mjpeg", "pipe:1"]
228
+
229
+ elif stream_type == "rtsp":
230
+ cmd = self._build_ffmpeg_cmd_rtsp(url)
231
+
232
+ elif stream_type == "hls":
233
+ cmd = self._build_ffmpeg_cmd_hls(url)
234
+
235
+ elif stream_type == "video_file":
236
+ file_path = self.source_file_path / os.path.basename(url)
237
+ if not file_path.exists():
238
+ logging.error(f"Video file does not exist: {file_path}")
239
+ return None
240
+ pos = self._get_current_video_position(str(file_path))
241
+ logging.info(f"🎬 [APP] Capturing video frame at {pos:.2f}s from {file_path}")
242
+ cmd = self._build_ffmpeg_cmd_video_file(str(file_path), pos)
243
+
244
+ elif stream_type == "image_file":
245
+ file_path = self.source_file_path / os.path.basename(url)
246
+ logging.info(f"🖼️ [APP] Capturing image frame from {file_path}")
247
+ cmd = self._build_ffmpeg_cmd_image_file(str(file_path))
248
+
249
+ else:
250
+ logging.error(f"Unsupported stream type: {url}")
195
251
  return None
196
- elif stream_type == "rtsp":
197
- ffmpeg_input = (
198
- ffmpeg
199
- .input(url, rtsp_transport="tcp", fflags="nobuffer", timeout="5000000")
200
- )
201
- elif stream_type == "hls":
202
- ffmpeg_input = (
203
- ffmpeg
204
- .input(url, format="hls", analyzeduration="10000000", probesize="10000000")
205
- )
206
- elif stream_type == "video_file":
207
- file_path = self.source_file_path / os.path.basename(url)
208
- file_path_str = str(file_path)
209
-
210
- if not os.path.exists(file_path_str):
211
- logging.error(f"Video file does not exist: {file_path_str}")
252
+
253
+ timeout_s = get_stream_timeout_duration(stream_type)
254
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
255
+
256
+ try:
257
+ stdout, stderr = proc.communicate(timeout=timeout_s)
258
+ except subprocess.TimeoutExpired:
259
+ proc.kill()
260
+ stdout, stderr = proc.communicate()
261
+ logging.error(f"FFmpeg timed out after {timeout_s}s for {stream_type} stream")
212
262
  return None
213
-
214
- current_position = self._get_current_video_position(file_path_str)
215
- logging.info(f"🎬 [APP] Capturing video frame at position {current_position:.2f}s from {file_path_str}")
216
-
217
- ffmpeg_input = (
218
- ffmpeg
219
- .input(file_path_str, ss=current_position)
220
- )
221
- elif stream_type == "image_file":
222
- file_path = self.source_file_path / os.path.basename(url)
223
- logging.info(f"🖼️ [APP] Capturing image frame from {file_path}")
224
-
225
- ffmpeg_input = (
226
- ffmpeg
227
- .input(str(file_path))
228
- )
229
- else:
230
- logging.error(f"Unsupported stream type: {url}")
231
- return None
232
263
 
233
- if stream_type == "video_file":
234
- process = (
235
- ffmpeg_input
236
- .output('pipe:', format='mjpeg', vframes=1, q=2)
237
- .overwrite_output()
238
- .run_async(pipe_stdout=True, pipe_stderr=True)
239
- )
240
- else:
241
- process = (
242
- ffmpeg_input
243
- .output('pipe:', format='mjpeg', vframes=1, q=2)
244
- .overwrite_output()
245
- .run_async(pipe_stdout=True, pipe_stderr=True)
246
- )
247
-
248
- try:
249
- stdout, stderr = process.communicate(timeout=15)
250
-
251
- if process.returncode != 0:
252
- error_msg = stderr.decode('utf-8', errors='ignore')
253
- logging.error(f"FFmpeg error: {error_msg}")
264
+ if proc.returncode != 0:
265
+ logging.error(f"FFmpeg error for {stream_type} stream: {(stderr or b'').decode('utf-8', 'ignore')}")
254
266
  return None
255
-
267
+
256
268
  if not stdout:
257
269
  logging.error("No data received from FFmpeg")
258
270
  return None
259
-
271
+
260
272
  return stdout
261
-
273
+
262
274
  except Exception as e:
263
275
  logging.error(f"Error capturing frame: {e}", exc_info=True)
264
276
  return None
265
-
277
+
266
278
  finally:
267
- # Release device access for direct devices
268
279
  if stream_type == "direct":
269
- self.shared_device_client.release_device_access(url)
270
-
271
- process.terminate()
272
- process.wait()
280
+ try:
281
+ self.shared_device_client.release_device_access(url)
282
+ except Exception:
283
+ pass
284
+ if proc and proc.poll() is None:
285
+ try:
286
+ proc.terminate()
287
+ proc.wait(timeout=2)
288
+ except Exception:
289
+ try:
290
+ proc.kill()
291
+ except Exception:
292
+ pass
293
+
294
+
295
+ # ---------- RPCs ----------
273
296
 
274
297
  def update_pipeline_status(self, pipeline_id: str, status_code: str, token: str):
275
298
  if not self.stub:
@@ -282,7 +305,7 @@ class WorkerSourcePipelineClient(GrpcClientBase):
282
305
  pipeline_id=pipeline_id,
283
306
  status_code=status_code,
284
307
  timestamp=timestamp,
285
- token=token
308
+ token=token,
286
309
  )
287
310
  response = self.handle_rpc(self.stub.UpdateStatus, request)
288
311
 
@@ -303,11 +326,10 @@ class WorkerSourcePipelineClient(GrpcClientBase):
303
326
  response = self.handle_rpc(self.stub.GetListByWorkerId, request)
304
327
 
305
328
  if response and response.success:
306
- # Create a wrapper function that captures the token
307
329
  def update_status_callback(pipeline_id: str, status_code: str):
308
330
  return self.update_pipeline_status(pipeline_id, status_code, token)
309
-
310
- self.repo.sync_worker_source_pipelines(response, update_status_callback) # Sync includes delete, update, insert
331
+
332
+ self.repo.sync_worker_source_pipelines(response, update_status_callback)
311
333
  return {"success": True, "message": response.message, "data": response.data}
312
334
 
313
335
  return {"success": False, "message": response.message if response else "Unknown error"}
@@ -315,22 +337,22 @@ class WorkerSourcePipelineClient(GrpcClientBase):
315
337
  except Exception as e:
316
338
  logging.error(f"Error fetching worker source pipeline list: {e}")
317
339
  return {"success": False, "message": f"Error occurred: {e}"}
318
-
340
+
319
341
  def send_pipeline_image(self, worker_source_pipeline_id: str, uuid: str, url: str, token: str):
320
342
  if not self.stub:
321
343
  return {"success": False, "message": "gRPC connection is not established."}
322
344
 
323
345
  try:
324
346
  frame_bytes = self._get_single_frame_bytes(url)
325
-
347
+
326
348
  if not frame_bytes:
327
349
  return {"success": False, "message": "Failed to retrieve frame from source"}
328
-
350
+
329
351
  request = SendPipelineImageRequest(
330
352
  worker_source_pipeline_id=worker_source_pipeline_id,
331
353
  uuid=uuid,
332
354
  image=frame_bytes,
333
- token=token
355
+ token=token,
334
356
  )
335
357
  response = self.handle_rpc(self.stub.SendPipelineImage, request)
336
358
 
@@ -341,28 +363,19 @@ class WorkerSourcePipelineClient(GrpcClientBase):
341
363
  except Exception as e:
342
364
  logging.error(f"Error sending pipeline image: {e}")
343
365
  return {"success": False, "message": f"Error occurred: {e}"}
344
-
366
+
345
367
  @staticmethod
346
368
  def read_image_as_binary(image_path: str) -> bytes:
347
- """
348
- Reads an image file and returns its binary content.
349
-
350
- Args:
351
- image_path (str): Path to the image file.
352
-
353
- Returns:
354
- bytes: Binary content of the image.
355
- """
356
- with open(image_path, 'rb') as image_file:
357
- return image_file.read()
358
-
369
+ with open(image_path, "rb") as f:
370
+ return f.read()
371
+
359
372
  def sync_pipeline_debug(self, token: str):
360
373
  if not self.stub:
361
374
  return {"success": False, "message": "gRPC connection is not established."}
362
375
 
363
376
  try:
364
377
  debug_entries = self.debug_repo.get_debug_entries_with_data()
365
-
378
+
366
379
  for debug_entry in debug_entries:
367
380
  image_binary = self.read_image_as_binary(debug_entry.image_path)
368
381
 
@@ -371,7 +384,7 @@ class WorkerSourcePipelineClient(GrpcClientBase):
371
384
  uuid=debug_entry.uuid,
372
385
  data=debug_entry.data,
373
386
  image=image_binary,
374
- token=token
387
+ token=token,
375
388
  )
376
389
  response = self.handle_rpc(self.stub.SendPipelineDebug, request)
377
390
 
@@ -391,7 +404,7 @@ class WorkerSourcePipelineClient(GrpcClientBase):
391
404
 
392
405
  try:
393
406
  entries = self.detection_repo.get_entries()
394
-
407
+
395
408
  for entry in entries:
396
409
  image_binary = self.read_image_as_binary(entry.image_path)
397
410
 
@@ -400,7 +413,7 @@ class WorkerSourcePipelineClient(GrpcClientBase):
400
413
  data=entry.data,
401
414
  image=image_binary,
402
415
  timestamp=int(entry.created_at.timestamp() * 1000),
403
- token=token
416
+ token=token,
404
417
  )
405
418
  response = self.handle_rpc(self.stub.SendPipelineDetectionData, request)
406
419
 
@@ -412,4 +425,4 @@ class WorkerSourcePipelineClient(GrpcClientBase):
412
425
  return {"success": True, "message": "Successfully synced debug entries"}
413
426
 
414
427
  except Exception as e:
415
- logging.error(f"Error syncing pipeline debug: {e}")
428
+ logging.error(f"Error syncing pipeline debug: {e}")
@@ -16,9 +16,10 @@ class WorkerSourceUpdater:
16
16
  This class is thread-safe and can be used concurrently from multiple threads.
17
17
  """
18
18
 
19
- def __init__(self, server_host: str, token: str):
19
+ def __init__(self, worker_id: str, token: str):
20
20
  storage_paths = _get_storage_paths()
21
21
  self.source_file_path = storage_paths["files"] / "source_files"
22
+ self.worker_id = worker_id
22
23
  # Use shared client instead of creating new instance
23
24
  self.client = GrpcClientManager.get_shared_client(WorkerSourceClient)
24
25
  self.repo = WorkerSourceRepository()
@@ -49,7 +50,7 @@ class WorkerSourceUpdater:
49
50
  """
50
51
  with self._lock:
51
52
  try:
52
- worker_sources = self.repo.get_all_worker_sources()
53
+ worker_sources = self.repo.get_worker_sources_by_worker_id(self.worker_id)
53
54
  updated_records = []
54
55
 
55
56
  for source in worker_sources:
@@ -130,7 +131,7 @@ class WorkerSourceUpdater:
130
131
  """
131
132
  with self._lock:
132
133
  try:
133
- worker_sources = self.repo.get_all_worker_sources()
134
+ worker_sources = self.repo.get_worker_sources_by_worker_id(self.worker_id)
134
135
  updated_records = []
135
136
 
136
137
  for source in worker_sources:
@@ -0,0 +1,73 @@
1
+ import logging
2
+ import subprocess
3
+ import re
4
+ from typing import Dict, Any, Tuple, List
5
+
6
+
7
+ def get_ffmpeg_version() -> Tuple[int, int, int]:
8
+ try:
9
+ result = subprocess.run(
10
+ ["ffmpeg", "-version"],
11
+ capture_output=True,
12
+ text=True,
13
+ timeout=5,
14
+ check=False,
15
+ )
16
+ if result.returncode == 0:
17
+ m = re.search(r"ffmpeg version n?(\d+)\.(\d+)(?:\.(\d+))?", result.stdout)
18
+ if m:
19
+ major = int(m.group(1))
20
+ minor = int(m.group(2))
21
+ patch = int(m.group(3)) if m.group(3) else 0
22
+ return major, minor, patch
23
+ except Exception as e:
24
+ logging.warning(f"Could not determine FFmpeg version: {e}")
25
+ return 4, 4, 1
26
+
27
+
28
+ def _supports_rw_timeout(v: Tuple[int, int, int]) -> bool:
29
+ major, minor, _ = v
30
+ return major >= 5 or (major == 4 and minor >= 3)
31
+
32
+
33
+ def get_rtsp_ffmpeg_options() -> Dict[str, Any]:
34
+ v = get_ffmpeg_version()
35
+
36
+ opts = {
37
+ "rtsp_transport": "tcp",
38
+ "probesize": "256k",
39
+ "analyzeduration": "1000000",
40
+ "buffer_size": "1024000",
41
+ "max_delay": "700000",
42
+ "fflags": "nobuffer+genpts",
43
+ }
44
+
45
+ if _supports_rw_timeout(v):
46
+ opts["rw_timeout"] = "5000000"
47
+ else:
48
+ opts["stimeout"] = "5000000"
49
+
50
+ return opts
51
+
52
+
53
+ def get_rtsp_probe_options() -> List[str]:
54
+ v = get_ffmpeg_version()
55
+
56
+ opts = [
57
+ "-v", "error",
58
+ "-rtsp_transport", "tcp",
59
+ "-probesize", "256k",
60
+ "-analyzeduration", "1000000",
61
+ ]
62
+
63
+ opts += ["-rw_timeout" if _supports_rw_timeout(v) else "-stimeout", "5000000"]
64
+ return opts
65
+
66
+
67
+ def get_stream_timeout_duration(t: str) -> int:
68
+ return {
69
+ "rtsp": 30,
70
+ "hls": 20,
71
+ "direct": 10,
72
+ "video_file": 5,
73
+ }.get(t, 15)
@@ -8,6 +8,7 @@ import sys
8
8
  import platform
9
9
  from pathlib import Path
10
10
  from urllib.parse import urlparse
11
+ from .FFmpegUtil import get_rtsp_probe_options
11
12
 
12
13
 
13
14
  try:
@@ -102,8 +103,7 @@ class VideoProbeUtil:
102
103
  frame_rate = round(frame_rate, 2)
103
104
 
104
105
  cap.release()
105
-
106
- logging.info(f"✅ [APP] Successfully probed device {device_idx} directly: {width}x{height} @ {frame_rate}fps")
106
+
107
107
 
108
108
  return {
109
109
  "resolution": f"{width}x{height}",
@@ -182,8 +182,6 @@ class VideoProbeUtil:
182
182
  if not width or not height:
183
183
  logging.warning(f"⚠️ [APP] Invalid resolution from FFmpeg for device {device_idx}")
184
184
  return None
185
-
186
- logging.info(f"✅ [APP] Successfully probed device {device_idx} with FFmpeg: {width}x{height} @ {frame_rate}fps")
187
185
 
188
186
  return {
189
187
  "resolution": f"{width}x{height}",
@@ -245,13 +243,11 @@ class VideoProbeUtil:
245
243
  logging.debug(traceback.format_exc())
246
244
 
247
245
  # Fallback 1: Try direct OpenCV access
248
- logging.info(f"🔄 [APP] Daemon not available for device {device_idx}, falling back to direct OpenCV access")
249
246
  metadata = VideoProbeUtil._get_metadata_opencv_direct_device(device_idx)
250
247
  if metadata:
251
248
  return metadata
252
249
 
253
250
  # Fallback 2: Try FFmpeg for device access (Linux v4l2, Windows dshow)
254
- logging.info(f"🔄 [APP] OpenCV failed for device {device_idx}, trying FFmpeg device access")
255
251
  return VideoProbeUtil._get_metadata_ffmpeg_direct_device(device_idx)
256
252
 
257
253
  except Exception as e:
@@ -289,8 +285,10 @@ class VideoProbeUtil:
289
285
  "-show_entries", "stream=width,height,avg_frame_rate", "-of", "json"]
290
286
 
291
287
  if stream_type == "rtsp":
292
- cmd.insert(1, "-rtsp_transport")
293
- cmd.insert(2, "tcp")
288
+ probe_options = get_rtsp_probe_options()
289
+ # Insert options at the beginning (after ffprobe)
290
+ for i, option in enumerate(probe_options):
291
+ cmd.insert(1 + i, option)
294
292
 
295
293
  cmd.append(video_url)
296
294
 
@@ -61,7 +61,7 @@ class DataSenderWorker:
61
61
  self.restricted_area_manager = RestrictedAreaManager(self.server_host, self.worker_id, "worker_source_id", self.token)
62
62
  self.dataset_frame_sender = DatasetFrameSender(self.server_host, self.token)
63
63
 
64
- self.source_updater = WorkerSourceUpdater(self.server_host, self.token)
64
+ self.source_updater = WorkerSourceUpdater(self.worker_id, self.token)
65
65
 
66
66
  def start(self):
67
67
  """Start the Data Sender Worker threads."""
@@ -119,7 +119,7 @@ class PipelineImageWorker:
119
119
  if response.get("success"):
120
120
  logger.info("✅ [APP] Successfully sent Pipeline Image Preview.")
121
121
  else:
122
- logger.error(f"❌ [APP] Failed to send Pipeline Image Preview: {response.get('message')}")
122
+ logger.error(f"❌ [APP] Failed to send Pipeline Image Preview: {response.get('message')}", exc_info=True)
123
123
 
124
124
  except json.JSONDecodeError:
125
125
  logger.error("⚠️ [APP] Invalid JSON message format.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nedo-vision-worker
3
- Version: 1.2.0
3
+ Version: 1.2.2
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>
@@ -165,6 +165,8 @@ pip install -e .
165
165
  pip install -e .[dev]
166
166
  ```
167
167
 
168
+ See [INSTALL.md](INSTALL.md) for detailed installation instructions.
169
+
168
170
  ## 🔍 System Diagnostics
169
171
 
170
172
  Before running the worker service, use the built-in diagnostic tool to verify your system:
@@ -1,6 +1,6 @@
1
- nedo_vision_worker/__init__.py,sha256=8AbQ3PK5CGdbnFRLBVzw8dcVQNHlNS_bBiqyygqIJ-Q,203
2
- nedo_vision_worker/cli.py,sha256=d7oDe3wJMfXd-ddStnyFASkLU0F3VemFsD9i0K7CYqU,7457
3
- nedo_vision_worker/doctor.py,sha256=uZ-NM_PfaTG5CG5OWFnl7cEsOTBWMGXNuKWuTV07deg,47228
1
+ nedo_vision_worker/__init__.py,sha256=c7rG8vVkrVGLJAt4kkrLAadY2NQ_SG_t161AE3BwGRA,203
2
+ nedo_vision_worker/cli.py,sha256=ddWspJmSgVkcUYvRdkvTtMNuMTDvNCqLLuMVU9KE3Ik,7457
3
+ nedo_vision_worker/doctor.py,sha256=wNkpe8gLVd76Y_ViyK2h1ZFdqeSl37MnzZN5frWKu30,48410
4
4
  nedo_vision_worker/worker_service.py,sha256=rXUVmyxcJPGhQEZ4UQvjQS5UqlnLBYudHQZCj0dQDxo,10421
5
5
  nedo_vision_worker/config/ConfigurationManager.py,sha256=QrQaQ9Cdjpkcr2JE_miyrWJIZmMgZwJYBz-wE45Zzes,8011
6
6
  nedo_vision_worker/config/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
@@ -45,14 +45,14 @@ nedo_vision_worker/repositories/RestrictedAreaRepository.py,sha256=y3n2ZfQbth1I_
45
45
  nedo_vision_worker/repositories/WorkerSourcePipelineDebugRepository.py,sha256=kOlVEnPOoDRZdZIm8uWXlc89GMvBPI-36QyKecX7ucE,3350
46
46
  nedo_vision_worker/repositories/WorkerSourcePipelineDetectionRepository.py,sha256=cbgg_7p0eNUIgCHoPDZBaRZ1b2Y68p_dfSxpvuGMtRE,1773
47
47
  nedo_vision_worker/repositories/WorkerSourcePipelineRepository.py,sha256=xfmEvgnyt-DdfSApGyFfy0H0dXjFFkjeo4LMr0fVFXU,10053
48
- nedo_vision_worker/repositories/WorkerSourceRepository.py,sha256=Rw9wJ27TETECCNwDxQu19KaKipQ_XHU0JJP6-0rzgmU,1982
48
+ nedo_vision_worker/repositories/WorkerSourceRepository.py,sha256=AhAJLAacMFdsOgtQNiu7Pahl1DAGI0T1THHeUlKwQJc,2385
49
49
  nedo_vision_worker/repositories/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
50
50
  nedo_vision_worker/services/AIModelClient.py,sha256=lxRNax6FR-pV0G1NpJnlaqjbQeu3kRolIUNSw1RkoZA,15406
51
51
  nedo_vision_worker/services/ConnectionInfoClient.py,sha256=toC9zuY2Hrx1Cwq8Gycy_iFlaG1DvFT4qewlLlitpEQ,2214
52
52
  nedo_vision_worker/services/DatasetSourceClient.py,sha256=O5a7onxFl0z47zXaMXWxHAMPuuc-i_vzkd2w5fwrukc,3319
53
53
  nedo_vision_worker/services/DirectDeviceToRTMPStreamer.py,sha256=M5ei0cd3_KDhHZp6EkrOowhAY-hAHfAQh9YDVjQtbQI,22278
54
54
  nedo_vision_worker/services/FileToRTMPServer.py,sha256=yUJxrouoTLSq9XZ88dhDYhP-px10jLoHopkPoy4lQxk,2663
55
- nedo_vision_worker/services/GrpcClientBase.py,sha256=9tNOGFfcm1Vy7ELgiA78KmGYCT13d4nBzpZkRkhsFKI,7385
55
+ nedo_vision_worker/services/GrpcClientBase.py,sha256=bRNeajiPGcJZtNofD_HU7JhLHVPbnuGacqv5Dp62GC0,7400
56
56
  nedo_vision_worker/services/GrpcClientManager.py,sha256=DLXekmxlQogLo8V9-TNDXtyHT_UG-BaggqwsIups55k,5568
57
57
  nedo_vision_worker/services/ImageUploadClient.py,sha256=T353YsRfm74G7Mh-eWr5nvdQHXTfpKwHJFmNW8HyjT8,3019
58
58
  nedo_vision_worker/services/PPEDetectionClient.py,sha256=CC-b0LRAgrftfIKp6TFKpeBkTYefe-C6Z1oz_X3HArQ,4345
@@ -63,35 +63,36 @@ nedo_vision_worker/services/SharedVideoStreamServer.py,sha256=WMKVxkzMoyfbgYiJ0f
63
63
  nedo_vision_worker/services/SystemUsageClient.py,sha256=PbRuwDWKnMarcnkGtOKfYB5nA-3DeKv7V5_hZr8EDEo,3200
64
64
  nedo_vision_worker/services/SystemWideDeviceCoordinator.py,sha256=9zBJMCbTMZS7gwN67rFpoUiTr82El2rpIO7DKFzeMjM,9417
65
65
  nedo_vision_worker/services/VideoSharingDaemon.py,sha256=hYMjUIKNUVT1qSxuUuHN-7Bd85MDkxfqslxDLe2PBYQ,29721
66
- nedo_vision_worker/services/VideoStreamClient.py,sha256=jSStEGjIHId4GseL-eE7FNGYU1CXgLaSjOm5CFVliPE,6828
66
+ nedo_vision_worker/services/VideoStreamClient.py,sha256=QSgUV3LijYrNdnBG1ylABOdUaSatQamfXaqJhAiol9M,7260
67
67
  nedo_vision_worker/services/WorkerSourceClient.py,sha256=vDZeCuHL5QQ2-knZ4TOSA59jzmbbThGIwFKKLEZ72Ws,9198
68
- nedo_vision_worker/services/WorkerSourcePipelineClient.py,sha256=rkKTPDWDgXhutMTqo_qvGvx1uVkhHcgi-3mJMywub8s,17705
69
- nedo_vision_worker/services/WorkerSourceUpdater.py,sha256=MsUsKL75sXj2odCcbupkFDW0KXg9LSu6-67iMWpYkHs,7679
68
+ nedo_vision_worker/services/WorkerSourcePipelineClient.py,sha256=qaBx9T2gWMzpqZaeQdbIeklsXNwzWD5kqgB41rrSkBI,17135
69
+ nedo_vision_worker/services/WorkerSourceUpdater.py,sha256=r_pCL1NiUlgPUFrntE1DWFG-KJygZPK51lAUGPwlzxo,7758
70
70
  nedo_vision_worker/services/WorkerStatusClient.py,sha256=7kC5EZjEBwWtHOE6UQ29OPCpYnv_6HSuH7Tc0alK_2Q,2531
71
71
  nedo_vision_worker/services/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
72
+ nedo_vision_worker/util/FFmpegUtil.py,sha256=QnQrzurmllzGb7SlAAYCrzKBUblweoFU-0h-X-32IYg,1829
72
73
  nedo_vision_worker/util/HardwareID.py,sha256=rSW8-6stm7rjXEdkYGqXMUn56gyw62YiWnSwZQVCCLM,4315
73
74
  nedo_vision_worker/util/ImageUploader.py,sha256=2xipN3fwpKgFmbvoGIdElpGn5ARJyrgR4dXtbRf73hw,3764
74
75
  nedo_vision_worker/util/Networking.py,sha256=uOtL8HkKZXJp02ZZIHWYMAvAsaYb7BsAPTncfdvJx2c,3241
75
76
  nedo_vision_worker/util/PlatformDetector.py,sha256=-iLPrKs7hp_oltkCI3hESJQkC2uRyu1-8mAbZrvgWx0,1501
76
77
  nedo_vision_worker/util/SystemMonitor.py,sha256=2MWYaEXoL91UANT_-SuDWrFMq1ajPorh8co6Py9PV_c,11300
77
- nedo_vision_worker/util/VideoProbeUtil.py,sha256=whgvZvSeO5y7IG3E1mxn5KRKYqxyrGtLJowV3nHgwYA,13181
78
+ nedo_vision_worker/util/VideoProbeUtil.py,sha256=cF-vJ7hIDlXfEJby2a0s9tqwkPGVz_6B3Vv4D5pMmIw,12876
78
79
  nedo_vision_worker/util/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
79
80
  nedo_vision_worker/worker/CoreActionWorker.py,sha256=lb7zPY3yui6I3F4rX4Ii7JwpWZahLEO72rh3iWOgFmg,5441
80
- nedo_vision_worker/worker/DataSenderWorker.py,sha256=o32MT28EqYwAPmd9NKhX3rfNlDKekNOI2n8mZ6s8CpU,7162
81
+ nedo_vision_worker/worker/DataSenderWorker.py,sha256=9FudRRItiMOcQx5UfVyu4p0Enb9BbgwZZ5EgX6Ho2U4,7160
81
82
  nedo_vision_worker/worker/DataSyncWorker.py,sha256=WvYfi3bG4mOKHU09J_MavfjFPrVgmxrrZYtrlQ-bnio,6265
82
83
  nedo_vision_worker/worker/DatasetFrameSender.py,sha256=1SFYj8LJFNi-anBTapsbq8U_NGMM7mnoMKg9NeFAHys,8087
83
84
  nedo_vision_worker/worker/DatasetFrameWorker.py,sha256=Ni5gPeDPk9Rz4_cbg63u7Y6LVw_-Bz24OvfeY-6Yp44,19320
84
85
  nedo_vision_worker/worker/PPEDetectionManager.py,sha256=fAolWlrsY5SQAWygvjNBNU56IlC0HLlhPgpz7shL-gk,3588
85
86
  nedo_vision_worker/worker/PipelineActionWorker.py,sha256=xgvryjKtEsMj4BKqWzDIaK_lFny-DfMCj5Y2DxHnWww,5651
86
- nedo_vision_worker/worker/PipelineImageWorker.py,sha256=c8_cTasgN-NJABD_qHSRb3hatg81sY_rV3lAAnuW49U,5627
87
+ nedo_vision_worker/worker/PipelineImageWorker.py,sha256=J8VBUG0cwcH3qOJp2zTl30B-XhmPFyvJLjxitKJYq0E,5642
87
88
  nedo_vision_worker/worker/RabbitMQListener.py,sha256=9gR49MDplgpyb-D5HOH0K77-DJQFvhS2E7biL92SjSU,6950
88
89
  nedo_vision_worker/worker/RestrictedAreaManager.py,sha256=3yoXgQ459tV1bOa5choEzR9gE6LklrtHR_e0472U3L0,3521
89
90
  nedo_vision_worker/worker/SystemUsageManager.py,sha256=StutV4UyLUfduYfK20de4SbPd7wqkR7io0gsOajxWSU,4509
90
91
  nedo_vision_worker/worker/VideoStreamWorker.py,sha256=5n6v1PNO7IB-jj_McALLkUP-cBjJoIEw4UiSAs3vTb0,7606
91
92
  nedo_vision_worker/worker/WorkerManager.py,sha256=T0vMfhOd7YesgQ9o2w6soeJ6n9chUAcuwcGe7p31xr0,5298
92
93
  nedo_vision_worker/worker/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
93
- nedo_vision_worker-1.2.0.dist-info/METADATA,sha256=uj39xqzh3cXjTI02_xRZpZVPieB8PjqLUxIOvEbT12M,14591
94
- nedo_vision_worker-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
95
- nedo_vision_worker-1.2.0.dist-info/entry_points.txt,sha256=LrglS-8nCi8C_PL_pa6uxdgCe879hBETHDVXAckvs-8,60
96
- nedo_vision_worker-1.2.0.dist-info/top_level.txt,sha256=vgilhlkyD34YsEKkaBabmhIpcKSvF3XpzD2By68L-XI,19
97
- nedo_vision_worker-1.2.0.dist-info/RECORD,,
94
+ nedo_vision_worker-1.2.2.dist-info/METADATA,sha256=w8bht7PkcQq3kajb3eqOK4_c8bBqZTM_QG_ZI2ZnQw8,14661
95
+ nedo_vision_worker-1.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
96
+ nedo_vision_worker-1.2.2.dist-info/entry_points.txt,sha256=LrglS-8nCi8C_PL_pa6uxdgCe879hBETHDVXAckvs-8,60
97
+ nedo_vision_worker-1.2.2.dist-info/top_level.txt,sha256=vgilhlkyD34YsEKkaBabmhIpcKSvF3XpzD2By68L-XI,19
98
+ nedo_vision_worker-1.2.2.dist-info/RECORD,,