nedo-vision-worker-core 0.3.3__py3-none-any.whl → 0.3.4__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.
Potentially problematic release.
This version of nedo-vision-worker-core might be problematic. Click here for more details.
- nedo_vision_worker_core/__init__.py +1 -1
- nedo_vision_worker_core/detection/RFDETRDetector.py +3 -0
- nedo_vision_worker_core/pipeline/ModelManager.py +139 -0
- nedo_vision_worker_core/pipeline/PipelineManager.py +3 -3
- nedo_vision_worker_core/pipeline/PipelineProcessor.py +36 -29
- nedo_vision_worker_core/pipeline/PipelineSyncThread.py +61 -109
- nedo_vision_worker_core/repositories/AIModelRepository.py +21 -1
- nedo_vision_worker_core/streams/RTMPStreamer.py +178 -233
- nedo_vision_worker_core/streams/SharedVideoDeviceManager.py +5 -1
- nedo_vision_worker_core/streams/VideoStream.py +201 -262
- nedo_vision_worker_core/streams/VideoStreamManager.py +14 -18
- nedo_vision_worker_core/util/PlatformDetector.py +100 -0
- {nedo_vision_worker_core-0.3.3.dist-info → nedo_vision_worker_core-0.3.4.dist-info}/METADATA +1 -1
- {nedo_vision_worker_core-0.3.3.dist-info → nedo_vision_worker_core-0.3.4.dist-info}/RECORD +17 -16
- nedo_vision_worker_core/detection/DetectionManager.py +0 -83
- {nedo_vision_worker_core-0.3.3.dist-info → nedo_vision_worker_core-0.3.4.dist-info}/WHEEL +0 -0
- {nedo_vision_worker_core-0.3.3.dist-info → nedo_vision_worker_core-0.3.4.dist-info}/entry_points.txt +0 -0
- {nedo_vision_worker_core-0.3.3.dist-info → nedo_vision_worker_core-0.3.4.dist-info}/top_level.txt +0 -0
|
@@ -4,281 +4,226 @@ import threading
|
|
|
4
4
|
import time
|
|
5
5
|
import numpy as np
|
|
6
6
|
import os
|
|
7
|
+
import sys
|
|
8
|
+
import cv2
|
|
9
|
+
import queue
|
|
7
10
|
from typing import Optional
|
|
11
|
+
from ..util.PlatformDetector import PlatformDetector
|
|
8
12
|
|
|
9
13
|
class RTMPStreamer:
|
|
10
14
|
"""
|
|
11
|
-
Streams raw BGR frames to
|
|
12
|
-
|
|
13
|
-
- Latest-only frame buffer (no backlog)
|
|
14
|
-
- Resilient auto-restart on failure
|
|
15
|
+
Streams raw BGR frames to an RTMP server using a GStreamer-first approach
|
|
16
|
+
with a reliable FFmpeg subprocess fallback.
|
|
15
17
|
"""
|
|
16
18
|
|
|
17
19
|
def __init__(self, pipeline_id: str, fps: int = 25, bitrate: str = "1500k"):
|
|
18
20
|
self.rtmp_server = os.environ.get("RTMP_SERVER", "rtmp://localhost:1935/live")
|
|
19
21
|
self.rtmp_url = f"{self.rtmp_server}/{pipeline_id}"
|
|
20
22
|
self.fps = max(int(fps), 1)
|
|
21
|
-
self.bitrate = bitrate
|
|
22
|
-
|
|
23
|
-
# VBV aligned to target bitrate by default (override via env if needed)
|
|
24
|
-
self.maxrate = os.environ.get("RTMP_MAXRATE", self.bitrate)
|
|
25
|
-
self.bufsize = os.environ.get("RTMP_BUFSIZE", f"{self._kbps(self.bitrate) * 2}k")
|
|
23
|
+
self.bitrate = self._kbps(bitrate) # Store as integer kbps
|
|
26
24
|
|
|
27
25
|
self.width: Optional[int] = None
|
|
28
26
|
self.height: Optional[int] = None
|
|
29
|
-
self.
|
|
30
|
-
|
|
31
|
-
self.
|
|
27
|
+
self._platform = PlatformDetector()
|
|
28
|
+
|
|
29
|
+
self._backend = None # "gstreamer" or "ffmpeg"
|
|
30
|
+
self._gstreamer_writer: Optional[cv2.VideoWriter] = None
|
|
31
|
+
self._ffmpeg_process: Optional[subprocess.Popen] = None
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
self._frame_queue = queue.Queue(maxsize=2)
|
|
34
34
|
self._writer_thread: Optional[threading.Thread] = None
|
|
35
35
|
self._stop_evt = threading.Event()
|
|
36
|
-
self._lock = threading.Lock()
|
|
37
|
-
self._latest_frame: Optional[np.ndarray] = None # last BGR frame set by push_frame()
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
def _kbps(self, rate_str: str) -> int:
|
|
38
|
+
return int(str(rate_str).lower().replace("k", "").strip())
|
|
41
39
|
|
|
42
|
-
# --------------------
|
|
40
|
+
# -------------------- Public API --------------------
|
|
43
41
|
|
|
44
42
|
def is_active(self) -> bool:
|
|
45
|
-
|
|
43
|
+
if self._backend == "gstreamer":
|
|
44
|
+
return self._gstreamer_writer is not None and self._gstreamer_writer.isOpened()
|
|
45
|
+
if self._backend == "ffmpeg":
|
|
46
|
+
return self._ffmpeg_process is not None and self._ffmpeg_process.poll() is None
|
|
47
|
+
return False
|
|
46
48
|
|
|
47
49
|
def push_frame(self, frame: np.ndarray):
|
|
48
|
-
|
|
49
|
-
Provide a frame to the streamer. The internal writer thread will pick up
|
|
50
|
-
the latest frame at the correct fps. Older frames are dropped.
|
|
51
|
-
"""
|
|
52
|
-
if frame is None or getattr(frame, "size", 0) == 0:
|
|
53
|
-
return
|
|
54
|
-
if frame.ndim != 3 or frame.shape[2] != 3:
|
|
50
|
+
if self._stop_evt.is_set():
|
|
55
51
|
return
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return
|
|
62
|
-
self.width, self.height = w, h
|
|
63
|
-
self._start_ffmpeg_and_writer()
|
|
53
|
+
if self._writer_thread is None or not self._writer_thread.is_alive():
|
|
54
|
+
if frame is None: return
|
|
55
|
+
self.height, self.width = frame.shape[:2]
|
|
56
|
+
self._start_stream()
|
|
64
57
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
frame = frame.astype(np.uint8, copy=False)
|
|
69
|
-
if not frame.flags['C_CONTIGUOUS']:
|
|
70
|
-
frame = np.ascontiguousarray(frame)
|
|
71
|
-
self._latest_frame = frame
|
|
72
|
-
|
|
73
|
-
def stop_stream(self):
|
|
74
|
-
"""Stop writer thread and FFmpeg cleanly."""
|
|
75
|
-
self.active = False
|
|
76
|
-
self._stop_evt.set()
|
|
77
|
-
|
|
78
|
-
if self._writer_thread and self._writer_thread.is_alive():
|
|
58
|
+
try:
|
|
59
|
+
self._frame_queue.put_nowait(frame)
|
|
60
|
+
except queue.Full:
|
|
79
61
|
try:
|
|
80
|
-
self.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
62
|
+
self._frame_queue.get_nowait()
|
|
63
|
+
self._frame_queue.put_nowait(frame)
|
|
64
|
+
except queue.Empty:
|
|
65
|
+
pass
|
|
84
66
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
self.ffmpeg_process = None
|
|
88
|
-
if not proc:
|
|
89
|
-
logging.info("RTMP streaming process already stopped.")
|
|
67
|
+
def stop_stream(self):
|
|
68
|
+
if self._stop_evt.is_set():
|
|
90
69
|
return
|
|
91
|
-
|
|
70
|
+
logging.info(f"Stopping RTMP stream for {self.rtmp_url}")
|
|
71
|
+
self._stop_evt.set()
|
|
72
|
+
|
|
92
73
|
try:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
74
|
+
self._frame_queue.put_nowait(None)
|
|
75
|
+
except queue.Full:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
if self._writer_thread and self._writer_thread.is_alive() and threading.current_thread() != self._writer_thread:
|
|
79
|
+
self._writer_thread.join(timeout=2.0)
|
|
80
|
+
|
|
81
|
+
if self._gstreamer_writer:
|
|
82
|
+
self._gstreamer_writer.release()
|
|
83
|
+
self._gstreamer_writer = None
|
|
84
|
+
logging.info("GStreamer writer released.")
|
|
85
|
+
|
|
86
|
+
if self._ffmpeg_process:
|
|
106
87
|
try:
|
|
107
|
-
|
|
88
|
+
if self._ffmpeg_process.stdin: self._ffmpeg_process.stdin.close()
|
|
89
|
+
self._ffmpeg_process.terminate()
|
|
90
|
+
self._ffmpeg_process.wait(timeout=2.0)
|
|
108
91
|
except Exception:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
logging.info("
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
"
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
"-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"-video_size", f"{self.width}x{self.height}",
|
|
133
|
-
"-framerate", str(self.fps),
|
|
134
|
-
"-i", "-",
|
|
135
|
-
|
|
136
|
-
# encoder
|
|
137
|
-
"-c:v", "libx264",
|
|
138
|
-
"-tune", "zerolatency",
|
|
139
|
-
"-preset", "ultrafast",
|
|
140
|
-
"-profile:v", "main",
|
|
141
|
-
"-pix_fmt", "yuv420p",
|
|
142
|
-
|
|
143
|
-
# CBR-like VBV
|
|
144
|
-
"-b:v", self.bitrate,
|
|
145
|
-
"-maxrate", self.maxrate,
|
|
146
|
-
"-bufsize", self.bufsize,
|
|
147
|
-
|
|
148
|
-
# GOP / keyframes
|
|
149
|
-
"-g", str(gop),
|
|
150
|
-
"-keyint_min", str(gop),
|
|
151
|
-
"-sc_threshold", "0",
|
|
152
|
-
"-x264-params", "open_gop=0:aud=1:repeat-headers=1:nal-hrd=cbr",
|
|
153
|
-
# force an IDR every 1s for faster player join/recovery
|
|
154
|
-
"-force_key_frames", "expr:gte(t,n_forced*1)",
|
|
155
|
-
|
|
156
|
-
# single scaling location (if needed downstream)
|
|
157
|
-
"-vf", "scale='min(1024,iw)':-2",
|
|
158
|
-
|
|
159
|
-
# no audio
|
|
160
|
-
"-an",
|
|
161
|
-
|
|
162
|
-
# reduce container buffering
|
|
163
|
-
"-flvflags", "no_duration_filesize",
|
|
164
|
-
"-flush_packets", "1",
|
|
165
|
-
"-rtmp_live", "live",
|
|
166
|
-
"-muxpreload", "0",
|
|
167
|
-
"-muxdelay", "0",
|
|
168
|
-
|
|
169
|
-
"-f", "flv",
|
|
170
|
-
self.rtmp_url,
|
|
171
|
-
]
|
|
92
|
+
if self._ffmpeg_process: self._ffmpeg_process.kill()
|
|
93
|
+
self._ffmpeg_process = None
|
|
94
|
+
logging.info("FFmpeg process stopped.")
|
|
95
|
+
|
|
96
|
+
self._backend = None
|
|
97
|
+
|
|
98
|
+
# -------------------- Internal Stream Management --------------------
|
|
99
|
+
|
|
100
|
+
def _start_stream(self):
|
|
101
|
+
self._stop_evt.clear()
|
|
102
|
+
|
|
103
|
+
gstreamer_pipeline = self._build_gstreamer_pipeline()
|
|
104
|
+
self._gstreamer_writer = cv2.VideoWriter(gstreamer_pipeline, cv2.CAP_GSTREAMER, 0, self.fps, (self.width, self.height))
|
|
105
|
+
|
|
106
|
+
if self._gstreamer_writer.isOpened():
|
|
107
|
+
self._backend = "gstreamer"
|
|
108
|
+
self._writer_thread = threading.Thread(target=self._gstreamer_writer_loop, daemon=True)
|
|
109
|
+
self._writer_thread.start()
|
|
110
|
+
logging.info(f"RTMP streaming started with GStreamer (HW-Accel): {self.rtmp_url}")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
logging.warning("GStreamer VideoWriter failed to open. Falling back to FFmpeg subprocess.")
|
|
114
|
+
self._gstreamer_writer = None
|
|
172
115
|
|
|
173
|
-
def _start_ffmpeg_and_writer(self):
|
|
174
|
-
"""Start ffmpeg process and the pacing writer thread."""
|
|
175
116
|
cmd = self._build_ffmpeg_cmd()
|
|
176
117
|
try:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
stdin=subprocess.PIPE,
|
|
181
|
-
stdout=devnull,
|
|
182
|
-
stderr=devnull if os.environ.get("FFSILENT", "1") == "1" else None,
|
|
183
|
-
bufsize=0, # unbuffered for low latency
|
|
184
|
-
)
|
|
185
|
-
self.started = True
|
|
186
|
-
self.active = True
|
|
187
|
-
self._stop_evt.clear()
|
|
188
|
-
self._writer_thread = threading.Thread(
|
|
189
|
-
target=self._writer_loop, name=f"rtmp-writer-{os.getpid()}",
|
|
190
|
-
daemon=True
|
|
191
|
-
)
|
|
118
|
+
self._ffmpeg_process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
119
|
+
self._backend = "ffmpeg"
|
|
120
|
+
self._writer_thread = threading.Thread(target=self._ffmpeg_pacing_loop, daemon=True)
|
|
192
121
|
self._writer_thread.start()
|
|
193
|
-
logging.info(f"RTMP streaming started: {self.rtmp_url}
|
|
122
|
+
logging.info(f"RTMP streaming started with FFmpeg (Fallback): {self.rtmp_url}")
|
|
194
123
|
except Exception as e:
|
|
195
|
-
logging.error(f"Failed to start FFmpeg: {e}")
|
|
196
|
-
self.
|
|
197
|
-
self.active = False
|
|
198
|
-
self.started = False
|
|
199
|
-
|
|
200
|
-
def _writer_loop(self):
|
|
201
|
-
"""Paces frames at exact fps, writing latest frame only. Auto-restarts on failure."""
|
|
202
|
-
next_deadline = time.monotonic() + self._frame_period
|
|
203
|
-
idle_frame: Optional[np.ndarray] = None # reuse last good frame if no new one arrived
|
|
124
|
+
logging.error(f"Failed to start FFmpeg fallback: {e}")
|
|
125
|
+
self._ffmpeg_process = None
|
|
204
126
|
|
|
127
|
+
def _gstreamer_writer_loop(self):
|
|
205
128
|
while not self._stop_evt.is_set():
|
|
206
129
|
try:
|
|
207
|
-
|
|
208
|
-
now = time.monotonic()
|
|
209
|
-
sleep_for = next_deadline - now
|
|
210
|
-
if sleep_for > 0:
|
|
211
|
-
time.sleep(sleep_for)
|
|
212
|
-
next_deadline += self._frame_period
|
|
213
|
-
if next_deadline < now - self._frame_period:
|
|
214
|
-
next_deadline = now + self._frame_period
|
|
215
|
-
|
|
216
|
-
# get the freshest frame
|
|
217
|
-
with self._lock:
|
|
218
|
-
frame = self._latest_frame
|
|
219
|
-
|
|
130
|
+
frame = self._frame_queue.get(timeout=1.0)
|
|
220
131
|
if frame is None:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
else:
|
|
226
|
-
frame_to_send = frame
|
|
227
|
-
idle_frame = frame
|
|
228
|
-
|
|
229
|
-
if not self.is_active():
|
|
230
|
-
raise BrokenPipeError("FFmpeg not active")
|
|
231
|
-
|
|
232
|
-
# write raw BGR24
|
|
233
|
-
self.ffmpeg_process.stdin.write(frame_to_send.tobytes())
|
|
234
|
-
|
|
235
|
-
except BrokenPipeError:
|
|
236
|
-
# logging.error("RTMP pipe broken. Restarting encoder.")
|
|
237
|
-
self._restart_ffmpeg()
|
|
238
|
-
# On restart we keep width/height; writer will continue
|
|
132
|
+
break
|
|
133
|
+
self._gstreamer_writer.write(frame)
|
|
134
|
+
except queue.Empty:
|
|
135
|
+
continue
|
|
239
136
|
except Exception as e:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
137
|
+
logging.error(f"Error in GStreamer writer loop: {e}. Stopping thread.")
|
|
138
|
+
self._stop_evt.set()
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
# -------------------- Pipeline Builders --------------------
|
|
142
|
+
|
|
143
|
+
def _build_gstreamer_pipeline(self) -> str:
|
|
144
|
+
if self._platform.is_jetson():
|
|
145
|
+
encoder = "nvv4l2h264enc insert-sps-pps=true"
|
|
146
|
+
converter = "nvvidconv ! video/x-raw(memory:NVMM) ! "
|
|
147
|
+
else:
|
|
148
|
+
encoder = "nvh264enc preset=low-latency-hq"
|
|
149
|
+
converter = "videoconvert"
|
|
150
|
+
|
|
151
|
+
pipeline = (
|
|
152
|
+
f"appsrc ! "
|
|
153
|
+
f"video/x-raw,format=BGR,width={self.width},height={self.height},framerate={self.fps}/1 ! "
|
|
154
|
+
f"{converter} ! {encoder} bitrate={self.bitrate} ! "
|
|
155
|
+
f"h264parse ! flvmux ! "
|
|
156
|
+
f"rtmpsink location=\"{self.rtmp_url}\""
|
|
157
|
+
)
|
|
158
|
+
return pipeline
|
|
159
|
+
|
|
160
|
+
# --- FFmpeg Fallback Methods ---
|
|
161
|
+
|
|
162
|
+
def _ffmpeg_pacing_loop(self):
|
|
163
|
+
frame_period = 1.0 / self.fps
|
|
164
|
+
last_frame_sent = None
|
|
244
165
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
proc = self.ffmpeg_process
|
|
249
|
-
self.ffmpeg_process = None
|
|
250
|
-
if proc:
|
|
166
|
+
while not self._stop_evt.is_set():
|
|
167
|
+
start_time = time.monotonic()
|
|
168
|
+
|
|
251
169
|
try:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
pass
|
|
261
|
-
proc.terminate()
|
|
262
|
-
proc.wait(timeout=3)
|
|
263
|
-
except Exception:
|
|
264
|
-
try:
|
|
265
|
-
proc.kill()
|
|
266
|
-
except Exception:
|
|
267
|
-
pass
|
|
170
|
+
frame = self._frame_queue.get_nowait()
|
|
171
|
+
last_frame_sent = frame
|
|
172
|
+
except queue.Empty:
|
|
173
|
+
frame = last_frame_sent
|
|
174
|
+
|
|
175
|
+
if frame is None:
|
|
176
|
+
time.sleep(frame_period)
|
|
177
|
+
continue
|
|
268
178
|
|
|
269
|
-
# restart only if we have size info
|
|
270
|
-
if self.width and self.height:
|
|
271
179
|
try:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
180
|
+
if not self.is_active():
|
|
181
|
+
raise BrokenPipeError("FFmpeg process is not active")
|
|
182
|
+
self._ffmpeg_process.stdin.write(frame.tobytes())
|
|
183
|
+
except (BrokenPipeError, OSError) as e:
|
|
184
|
+
logging.error(f"FFmpeg process pipe broken: {e}. Stopping thread.")
|
|
185
|
+
self._stop_evt.set()
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
elapsed = time.monotonic() - start_time
|
|
189
|
+
sleep_duration = max(0, frame_period - elapsed)
|
|
190
|
+
time.sleep(sleep_duration)
|
|
191
|
+
|
|
192
|
+
def _build_ffmpeg_cmd(self) -> list[str]:
|
|
193
|
+
encoder_args = self._select_ffmpeg_encoder()
|
|
194
|
+
encoder_name = encoder_args[1]
|
|
195
|
+
|
|
196
|
+
# Base command arguments
|
|
197
|
+
cmd = [
|
|
198
|
+
'ffmpeg', '-y', '-loglevel', 'error', '-nostats', '-hide_banner',
|
|
199
|
+
'-f', 'rawvideo', '-pixel_format', 'bgr24',
|
|
200
|
+
'-video_size', f'{self.width}x{self.height}',
|
|
201
|
+
'-framerate', str(self.fps), '-i', '-',
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
# Add the selected encoder
|
|
205
|
+
cmd.extend(encoder_args)
|
|
206
|
+
|
|
207
|
+
# Add common arguments for all encoders
|
|
208
|
+
cmd.extend([
|
|
209
|
+
'-profile:v', 'main', '-pix_fmt', 'yuv420p',
|
|
210
|
+
'-b:v', f"{self.bitrate}k", '-maxrate', f"{self.bitrate}k", '-bufsize', f"{self.bitrate*2}k",
|
|
211
|
+
'-g', str(self.fps), '-keyint_min', str(self.fps),
|
|
212
|
+
'-force_key_frames', 'expr:gte(t,n_forced*1)', '-an',
|
|
213
|
+
'-flvflags', 'no_duration_filesize', '-f', 'flv', self.rtmp_url,
|
|
214
|
+
])
|
|
215
|
+
|
|
216
|
+
# Conditionally add arguments that are ONLY valid for the libx264 encoder
|
|
217
|
+
if encoder_name == "libx264":
|
|
218
|
+
cmd.extend([
|
|
219
|
+
"-tune", "zerolatency",
|
|
220
|
+
"-x264-params", "open_gop=0:aud=1:repeat-headers=1:nal-hrd=cbr",
|
|
221
|
+
])
|
|
222
|
+
|
|
223
|
+
return cmd
|
|
224
|
+
|
|
225
|
+
def _select_ffmpeg_encoder(self) -> list:
|
|
226
|
+
if sys.platform == "darwin": return ["-c:v", "h264_videotoolbox"]
|
|
227
|
+
if os.environ.get("NVIDIA_VISIBLE_DEVICES") is not None or os.path.exists("/proc/driver/nvidia/version"):
|
|
228
|
+
return ["-c:v", "h264_nvenc", "-preset", "llhp"]
|
|
229
|
+
return ["-c:v", "libx264"]
|
|
@@ -8,6 +8,10 @@ from .VideoStream import VideoStream
|
|
|
8
8
|
from ..services.SharedVideoStreamServer import get_shared_stream_server
|
|
9
9
|
from ..services.VideoSharingDaemonManager import get_daemon_manager
|
|
10
10
|
|
|
11
|
+
import numpy as np
|
|
12
|
+
from numpy.typing import NDArray
|
|
13
|
+
MatLike = NDArray[np.uint8]
|
|
14
|
+
|
|
11
15
|
try:
|
|
12
16
|
from nedo_vision_worker_core.services.VideoSharingDaemon import VideoSharingClient
|
|
13
17
|
except ImportError:
|
|
@@ -77,7 +81,7 @@ class SharedVideoDeviceManager:
|
|
|
77
81
|
pass
|
|
78
82
|
return False, None
|
|
79
83
|
|
|
80
|
-
def subscribe_to_device(self, source, subscriber_id: str, callback: Callable[[
|
|
84
|
+
def subscribe_to_device(self, source, subscriber_id: str, callback: Callable[[MatLike], None]) -> bool:
|
|
81
85
|
"""
|
|
82
86
|
Subscribe to a direct video device.
|
|
83
87
|
|