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
|
@@ -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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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)
|
|
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
|
|
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 == "
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.
|
|
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:
|
|
129
|
+
if empty_frame_count > 5:
|
|
116
130
|
logging.error("Too many empty frames, stopping stream...")
|
|
117
131
|
break
|
|
118
|
-
continue
|
|
132
|
+
continue
|
|
119
133
|
|
|
120
|
-
empty_frame_count = 0
|
|
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)
|
|
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
|
-
|
|
135
|
-
|
|
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()
|
|
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
|
|
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
|
-
|
|
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 == "
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|