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.
- nedo_vision_worker/__init__.py +1 -1
- nedo_vision_worker/cli.py +1 -1
- nedo_vision_worker/doctor.py +32 -2
- nedo_vision_worker/repositories/WorkerSourceRepository.py +10 -1
- nedo_vision_worker/services/GrpcClientBase.py +1 -1
- nedo_vision_worker/services/VideoStreamClient.py +14 -8
- nedo_vision_worker/services/WorkerSourcePipelineClient.py +189 -176
- nedo_vision_worker/services/WorkerSourceUpdater.py +4 -3
- nedo_vision_worker/util/FFmpegUtil.py +73 -0
- nedo_vision_worker/util/VideoProbeUtil.py +6 -8
- nedo_vision_worker/worker/DataSenderWorker.py +1 -1
- nedo_vision_worker/worker/PipelineImageWorker.py +1 -1
- {nedo_vision_worker-1.2.0.dist-info → nedo_vision_worker-1.2.2.dist-info}/METADATA +3 -1
- {nedo_vision_worker-1.2.0.dist-info → nedo_vision_worker-1.2.2.dist-info}/RECORD +17 -16
- {nedo_vision_worker-1.2.0.dist-info → nedo_vision_worker-1.2.2.dist-info}/WHEEL +0 -0
- {nedo_vision_worker-1.2.0.dist-info → nedo_vision_worker-1.2.2.dist-info}/entry_points.txt +0 -0
- {nedo_vision_worker-1.2.0.dist-info → nedo_vision_worker-1.2.2.dist-info}/top_level.txt +0 -0
nedo_vision_worker/__init__.py
CHANGED
nedo_vision_worker/cli.py
CHANGED
nedo_vision_worker/doctor.py
CHANGED
|
@@ -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,
|
|
91
|
-
recommended_version = (3,
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
29
|
-
self.
|
|
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
|
-
|
|
81
|
+
if parsed_url.scheme in ["http", "https"] and url.endswith(".m3u8"):
|
|
46
82
|
return "hls"
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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(
|
|
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
|
-
|
|
102
|
-
|
|
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] =
|
|
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
|
-
|
|
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] =
|
|
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":
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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)
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
-
|
|
293
|
-
|
|
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.
|
|
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.
|
|
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=
|
|
2
|
-
nedo_vision_worker/cli.py,sha256=
|
|
3
|
-
nedo_vision_worker/doctor.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
69
|
-
nedo_vision_worker/services/WorkerSourceUpdater.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
94
|
-
nedo_vision_worker-1.2.
|
|
95
|
-
nedo_vision_worker-1.2.
|
|
96
|
-
nedo_vision_worker-1.2.
|
|
97
|
-
nedo_vision_worker-1.2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|