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
@@ -7,6 +7,8 @@ import ffmpeg
7
7
  import fractions
8
8
  from urllib.parse import urlparse
9
9
  from .GrpcClientBase import GrpcClientBase
10
+ from .SharedDirectDeviceClient import SharedDirectDeviceClient
11
+ from ..util.FFmpegUtil import get_rtsp_ffmpeg_options, get_rtsp_probe_options
10
12
  from ..protos.VisionWorkerService_pb2_grpc import VideoStreamServiceStub
11
13
  from ..protos.VisionWorkerService_pb2 import VideoFrame
12
14
 
@@ -15,9 +17,12 @@ class VideoStreamClient(GrpcClientBase):
15
17
  def __init__(self, server_host: str, server_port: int = 50051):
16
18
  """Initialize the video stream client."""
17
19
  super().__init__(server_host, server_port)
20
+ self.shared_device_client = SharedDirectDeviceClient()
18
21
 
19
22
  def _detect_stream_type(self, url):
20
- """Detect whether the stream is RTSP or HLS based on the URL scheme."""
23
+ if isinstance(url, str) and url.isdigit():
24
+ return "direct"
25
+
21
26
  parsed_url = urlparse(url)
22
27
  if parsed_url.scheme == "rtsp":
23
28
  return "rtsp"
@@ -27,8 +32,12 @@ class VideoStreamClient(GrpcClientBase):
27
32
  return "unknown"
28
33
 
29
34
  def _get_video_properties(self, url, stream_type):
30
- """Extract FPS, resolution, and pixel format dynamically for RTSP or HLS."""
31
35
  try:
36
+ if stream_type == "direct":
37
+ # Use the shared device client for direct devices
38
+ width, height, fps, pixel_format = self.shared_device_client.get_video_properties(url)
39
+ return width, height, fps, pixel_format
40
+
32
41
  probe_cmd = [
33
42
  "ffprobe",
34
43
  "-i", url,
@@ -39,8 +48,10 @@ class VideoStreamClient(GrpcClientBase):
39
48
  ]
40
49
 
41
50
  if stream_type == "rtsp":
42
- probe_cmd.insert(1, "-rtsp_transport")
43
- 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)
44
55
 
45
56
  result = subprocess.run(probe_cmd, capture_output=True, text=True)
46
57
  probe_data = json.loads(result.stdout)
@@ -59,12 +70,10 @@ class VideoStreamClient(GrpcClientBase):
59
70
  return None, None, None, None
60
71
 
61
72
  def _get_bytes_per_pixel(self, pixel_format):
62
- """Determine bytes per pixel based on pixel format."""
63
73
  pixel_map = {"rgb24": 3, "yuv420p": 1.5, "gray": 1}
64
- return pixel_map.get(pixel_format, 3) # Default to 3 bytes (RGB)
74
+ return pixel_map.get(pixel_format, 3)
65
75
 
66
76
  def _generate_frames(self, url, worker_id, uuid, stream_duration):
67
- """Generator function to continuously stream video frames to gRPC."""
68
77
  stream_type = self._detect_stream_type(url)
69
78
  if stream_type == "unknown":
70
79
  logging.error(f"Unsupported stream type: {url}")
@@ -77,17 +86,22 @@ class VideoStreamClient(GrpcClientBase):
77
86
 
78
87
  bytes_per_pixel = self._get_bytes_per_pixel(pixel_format)
79
88
  frame_size = int(width * height * bytes_per_pixel)
80
- frame_interval = 1.0 / fps # Time between frames
89
+ frame_interval = 1.0 / fps
81
90
  start_time = time.time()
82
91
  empty_frame_count = 0
83
92
 
84
93
  logging.info(f"Streaming {stream_type.upper()} from: {url} for {stream_duration} seconds...")
85
94
 
86
- if stream_type == "rtsp":
87
- ffmpeg_input = (
88
- ffmpeg
89
- .input(url, rtsp_transport="tcp", fflags="nobuffer", timeout="5000000")
90
- )
95
+ if stream_type == "direct":
96
+ # Use the shared device client for direct devices
97
+ try:
98
+ ffmpeg_input = self.shared_device_client.create_ffmpeg_input(url, width, height, fps)
99
+ except Exception as e:
100
+ logging.error(f"Failed to create ffmpeg input for direct device: {e}")
101
+ return
102
+ elif stream_type == "rtsp":
103
+ rtsp_options = get_rtsp_ffmpeg_options()
104
+ ffmpeg_input = ffmpeg.input(url, **rtsp_options)
91
105
  elif stream_type == "hls":
92
106
  ffmpeg_input = (
93
107
  ffmpeg
@@ -112,12 +126,12 @@ class VideoStreamClient(GrpcClientBase):
112
126
  empty_frame_count += 1
113
127
  logging.warning(f"Empty frame received ({empty_frame_count}), retrying...")
114
128
 
115
- if empty_frame_count > 5: # Stop if 5 consecutive empty frames
129
+ if empty_frame_count > 5:
116
130
  logging.error("Too many empty frames, stopping stream...")
117
131
  break
118
- continue # Try reading the next frame
132
+ continue
119
133
 
120
- empty_frame_count = 0 # Reset empty frame count
134
+ empty_frame_count = 0
121
135
  yield VideoFrame(
122
136
  worker_id=worker_id,
123
137
  uuid=uuid,
@@ -125,16 +139,25 @@ class VideoStreamClient(GrpcClientBase):
125
139
  timestamp=int(time.time() * 1000),
126
140
  )
127
141
 
128
- time.sleep(frame_interval) # Ensure proper frame timing
142
+ time.sleep(frame_interval)
129
143
 
130
144
  except Exception as e:
131
145
  logging.error(f"Streaming error: {e}")
132
146
 
133
147
  finally:
134
- stderr_output = process.stderr.read().decode()
135
- logging.error(f"FFmpeg stderr: {stderr_output}") # Log errors from FFmpeg
148
+ # Release device access for direct devices
149
+ if stream_type == "direct":
150
+ self.shared_device_client.release_device_access(url)
151
+
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
+
136
159
  process.terminate()
137
- process.wait() # Ensures cleanup
160
+ process.wait()
138
161
 
139
162
  def stream_video(self, worker_id, uuid, url, stream_duration):
140
163
  """
@@ -87,7 +87,7 @@ class WorkerSourceClient(GrpcClientBase):
87
87
  if existing_source.worker_id != source.worker_id:
88
88
  changes.append(f"worker_id: {existing_source.worker_id} → {source.worker_id}")
89
89
  if existing_source.type_code != source.type_code:
90
- if source.type_code == "live":
90
+ if source.type_code in ["live", "direct"]:
91
91
  self.delete_local_source_file(existing_source.file_path)
92
92
  elif existing_source.file_path == source.file_path:
93
93
  self.download_source_file(source)
@@ -8,7 +8,9 @@ from pathlib import Path
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 get_rtsp_ffmpeg_options, get_stream_timeout_duration
11
12
  from .GrpcClientBase import GrpcClientBase
13
+ from .SharedDirectDeviceClient import SharedDirectDeviceClient
12
14
  from ..protos.WorkerSourcePipelineService_pb2_grpc import WorkerSourcePipelineServiceStub
13
15
  from ..protos.WorkerSourcePipelineService_pb2 import GetListByWorkerIdRequest, SendPipelineImageRequest, UpdatePipelineStatusRequest, SendPipelineDebugRequest, SendPipelineDetectionDataRequest
14
16
  from ..repositories.WorkerSourcePipelineRepository import WorkerSourcePipelineRepository
@@ -22,6 +24,7 @@ class WorkerSourcePipelineClient(GrpcClientBase):
22
24
  self.detection_repo = WorkerSourcePipelineDetectionRepository()
23
25
  storage_paths = _get_storage_paths()
24
26
  self.source_file_path = storage_paths["files"] / "source_files"
27
+ self.shared_device_client = SharedDirectDeviceClient()
25
28
 
26
29
  # Track video playback positions and last fetch times
27
30
  self.video_positions = {} # {video_path: current_position_in_seconds}
@@ -34,7 +37,9 @@ class WorkerSourcePipelineClient(GrpcClientBase):
34
37
  self.stub = None
35
38
 
36
39
  def _detect_stream_type(self, url):
37
- """Detect whether the stream is RTSP, HLS, or video file based on the URL scheme."""
40
+ if isinstance(url, str) and url.isdigit():
41
+ return "direct"
42
+
38
43
  parsed_url = urlparse(url)
39
44
  if parsed_url.scheme == "rtsp":
40
45
  return "rtsp"
@@ -169,14 +174,29 @@ class WorkerSourcePipelineClient(GrpcClientBase):
169
174
  return status
170
175
 
171
176
  def _get_single_frame_bytes(self, url):
172
- """Get a single frame from RTSP, HLS, or video file as JPEG bytes."""
173
177
  stream_type = self._detect_stream_type(url)
174
178
 
175
- if stream_type == "rtsp":
176
- ffmpeg_input = (
177
- ffmpeg
178
- .input(url, rtsp_transport="tcp", fflags="nobuffer", timeout="5000000")
179
- )
179
+ if stream_type == "direct":
180
+ device_index = int(url)
181
+ logging.info(f"📹 [APP] Capturing frame from direct video device: {device_index}")
182
+
183
+ # Use the shared device client for direct devices
184
+ try:
185
+ # Get device properties first
186
+ width, height, fps, pixel_format = self.shared_device_client.get_video_properties(url)
187
+ if not width or not height:
188
+ logging.error(f"Failed to get properties for device {device_index}")
189
+ return None
190
+
191
+ # Create ffmpeg input using shared device client
192
+ ffmpeg_input = self.shared_device_client.create_ffmpeg_input(url, width, height, fps)
193
+
194
+ except Exception as e:
195
+ logging.error(f"Error setting up direct device {device_index}: {e}")
196
+ return None
197
+ elif stream_type == "rtsp":
198
+ rtsp_options = get_rtsp_ffmpeg_options()
199
+ ffmpeg_input = ffmpeg.input(url, **rtsp_options)
180
200
  elif stream_type == "hls":
181
201
  ffmpeg_input = (
182
202
  ffmpeg
@@ -186,13 +206,11 @@ class WorkerSourcePipelineClient(GrpcClientBase):
186
206
  file_path = self.source_file_path / os.path.basename(url)
187
207
  file_path_str = str(file_path)
188
208
 
189
- # Check if file exists
190
209
  if not os.path.exists(file_path_str):
191
210
  logging.error(f"Video file does not exist: {file_path_str}")
192
211
  return None
193
212
 
194
213
  current_position = self._get_current_video_position(file_path_str)
195
-
196
214
  logging.info(f"🎬 [APP] Capturing video frame at position {current_position:.2f}s from {file_path_str}")
197
215
 
198
216
  ffmpeg_input = (
@@ -201,7 +219,6 @@ class WorkerSourcePipelineClient(GrpcClientBase):
201
219
  )
202
220
  elif stream_type == "image_file":
203
221
  file_path = self.source_file_path / os.path.basename(url)
204
-
205
222
  logging.info(f"🖼️ [APP] Capturing image frame from {file_path}")
206
223
 
207
224
  ffmpeg_input = (
@@ -228,11 +245,13 @@ class WorkerSourcePipelineClient(GrpcClientBase):
228
245
  )
229
246
 
230
247
  try:
231
- stdout, stderr = process.communicate(timeout=15)
248
+ # Use appropriate timeout for different stream types
249
+ timeout_duration = get_stream_timeout_duration(stream_type)
250
+ stdout, stderr = process.communicate(timeout=timeout_duration)
232
251
 
233
252
  if process.returncode != 0:
234
253
  error_msg = stderr.decode('utf-8', errors='ignore')
235
- logging.error(f"FFmpeg error: {error_msg}")
254
+ logging.error(f"FFmpeg error for {stream_type} stream: {error_msg}")
236
255
  return None
237
256
 
238
257
  if not stdout:
@@ -246,6 +265,10 @@ class WorkerSourcePipelineClient(GrpcClientBase):
246
265
  return None
247
266
 
248
267
  finally:
268
+ # Release device access for direct devices
269
+ if stream_type == "direct":
270
+ self.shared_device_client.release_device_access(url)
271
+
249
272
  process.terminate()
250
273
  process.wait()
251
274
 
@@ -27,8 +27,13 @@ class WorkerSourceUpdater:
27
27
  self._lock = threading.RLock()
28
28
 
29
29
  def _get_source_metadata(self, source):
30
- """Get metadata for a worker source."""
31
- url = source.url if source.type_code == "live" else source.file_path
30
+ if source.type_code == "live":
31
+ url = source.url
32
+ elif source.type_code == "direct":
33
+ url = source.url
34
+ else:
35
+ url = source.file_path
36
+
32
37
  if not url:
33
38
  return None
34
39
 
@@ -50,7 +55,29 @@ class WorkerSourceUpdater:
50
55
  for source in worker_sources:
51
56
  metadata = self._get_source_metadata(source)
52
57
  if not metadata:
53
- logger.warning(f"⚠️ [APP] Failed to probe video for Worker Source ID {source.id}")
58
+ logger.warning(f"⚠️ [APP] Failed to probe video for Worker Source ID {source.id} (type: {source.type_code}, url: {source.url if hasattr(source, 'url') else 'N/A'})")
59
+ # Set disconnected status for failed probes
60
+ if source.status_code != "disconnected":
61
+ source.status_code = "disconnected"
62
+ source.resolution = None
63
+ source.frame_rate = None
64
+ updated_records.append(source)
65
+
66
+ # Send gRPC update for disconnected status
67
+ worker_timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
68
+ response = self.client.update_worker_source(
69
+ worker_source_id=source.id,
70
+ resolution=None,
71
+ status_code="disconnected",
72
+ frame_rate=None,
73
+ worker_timestamp=worker_timestamp,
74
+ token=self.token,
75
+ )
76
+
77
+ if response.get("success"):
78
+ logger.info(f"✅ [APP] Updated Worker Source ID {source.id} to disconnected")
79
+ else:
80
+ logger.error(f"🚨 [APP] Failed to update Worker Source ID {source.id} to disconnected: {response.get('message')}")
54
81
  continue
55
82
 
56
83
  # Extract details
@@ -0,0 +1,124 @@
1
+ """
2
+ FFmpeg utilities for Jetson compatibility and RTSP stream handling.
3
+
4
+ This module provides common FFmpeg configurations and utilities that work
5
+ reliably on Jetson devices with FFmpeg 4.4.1 and newer versions.
6
+ """
7
+
8
+ import logging
9
+ import subprocess
10
+ import re
11
+ from typing import Dict, Any, Tuple
12
+
13
+
14
+ def get_ffmpeg_version() -> Tuple[int, int, int]:
15
+ """
16
+ Get the FFmpeg version as a tuple of (major, minor, patch).
17
+
18
+ Returns:
19
+ Tuple[int, int, int]: Version tuple (major, minor, patch)
20
+ """
21
+ try:
22
+ result = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True, timeout=5)
23
+ if result.returncode == 0:
24
+ # Extract version from output like "ffmpeg version n7.1.1" or "ffmpeg version 4.4.1"
25
+ match = re.search(r'ffmpeg version n?(\d+)\.(\d+)\.(\d+)', result.stdout)
26
+ if match:
27
+ return (int(match.group(1)), int(match.group(2)), int(match.group(3)))
28
+ except Exception as e:
29
+ logging.warning(f"Could not determine FFmpeg version: {e}")
30
+
31
+ # Default to a reasonable version if detection fails
32
+ return (4, 4, 1)
33
+
34
+
35
+ def get_rtsp_ffmpeg_options() -> Dict[str, Any]:
36
+ """
37
+ Get FFmpeg options optimized for RTSP streams with version compatibility.
38
+
39
+ These options work across different FFmpeg versions:
40
+ - FFmpeg 4.4.x: Uses stimeout
41
+ - FFmpeg 5.x+: Uses timeout
42
+ - FFmpeg 7.x+: Uses timeout
43
+
44
+ Returns:
45
+ Dict[str, Any]: FFmpeg input options for RTSP streams
46
+ """
47
+ version = get_ffmpeg_version()
48
+ major, minor, patch = version
49
+
50
+ # Base options that work across all versions
51
+ options = {
52
+ "rtsp_transport": "tcp",
53
+ "fflags": "nobuffer+genpts",
54
+ "max_delay": "5000000", # Max buffering delay
55
+ "buffer_size": "1024000", # Input buffer size
56
+ "avoid_negative_ts": "make_zero" # Handle timestamp issues
57
+ }
58
+
59
+ # Add version-specific timeout option
60
+ if major == 4 and minor == 4:
61
+ # FFmpeg 4.4.x uses stimeout
62
+ options["stimeout"] = "5000000"
63
+ logging.debug("Using stimeout for FFmpeg 4.4.x")
64
+ else:
65
+ # FFmpeg 5.x+ uses timeout (microseconds)
66
+ options["timeout"] = "5000000"
67
+ logging.debug(f"Using timeout for FFmpeg {major}.{minor}.{patch}")
68
+
69
+ return options
70
+
71
+
72
+ def get_rtsp_probe_options() -> list:
73
+ """
74
+ Get ffprobe command line options for RTSP streams with version compatibility.
75
+
76
+ Returns:
77
+ list: Command line options to insert into ffprobe command
78
+ """
79
+ version = get_ffmpeg_version()
80
+ major, minor, patch = version
81
+
82
+ base_options = ["-rtsp_transport", "tcp"]
83
+
84
+ # Add version-specific timeout option
85
+ if major == 4 and minor == 4:
86
+ # FFmpeg 4.4.x uses stimeout
87
+ return base_options + ["-stimeout", "5000000"]
88
+ else:
89
+ # FFmpeg 5.x+ uses timeout
90
+ return base_options + ["-timeout", "5000000"]
91
+
92
+
93
+ def log_ffmpeg_version_info():
94
+ """Log information about FFmpeg compatibility."""
95
+ version = get_ffmpeg_version()
96
+ major, minor, patch = version
97
+
98
+ logging.info(f"Detected FFmpeg version: {major}.{minor}.{patch}")
99
+
100
+ if major == 4 and minor == 4:
101
+ logging.info("Using 'stimeout' parameter for FFmpeg 4.4.x compatibility")
102
+ else:
103
+ logging.info(f"Using 'timeout' parameter for FFmpeg {major}.{minor}.{patch}")
104
+
105
+ logging.info("RTSP configuration optimized for embedded devices")
106
+
107
+
108
+ def get_stream_timeout_duration(stream_type: str) -> int:
109
+ """
110
+ Get appropriate timeout duration for different stream types.
111
+
112
+ Args:
113
+ stream_type (str): Type of stream (rtsp, hls, direct, etc.)
114
+
115
+ Returns:
116
+ int: Timeout duration in seconds
117
+ """
118
+ timeouts = {
119
+ "rtsp": 30, # RTSP streams may take longer to connect
120
+ "hls": 20, # HLS streams need time for manifest download
121
+ "direct": 10, # Direct device access should be faster
122
+ "video_file": 5 # Local files should be very fast
123
+ }
124
+ return timeouts.get(stream_type, 15) # Default 15 seconds