matrice-streaming 0.1.14__py3-none-any.whl → 0.1.65__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.
- matrice_streaming/__init__.py +44 -32
- matrice_streaming/streaming_gateway/camera_streamer/__init__.py +68 -1
- matrice_streaming/streaming_gateway/camera_streamer/async_camera_worker.py +1388 -0
- matrice_streaming/streaming_gateway/camera_streamer/async_ffmpeg_worker.py +966 -0
- matrice_streaming/streaming_gateway/camera_streamer/camera_streamer.py +188 -24
- matrice_streaming/streaming_gateway/camera_streamer/device_detection.py +507 -0
- matrice_streaming/streaming_gateway/camera_streamer/encoding_pool_manager.py +136 -0
- matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_camera_streamer.py +1048 -0
- matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_config.py +192 -0
- matrice_streaming/streaming_gateway/camera_streamer/ffmpeg_worker_manager.py +470 -0
- matrice_streaming/streaming_gateway/camera_streamer/gstreamer_camera_streamer.py +1368 -0
- matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker.py +1063 -0
- matrice_streaming/streaming_gateway/camera_streamer/gstreamer_worker_manager.py +546 -0
- matrice_streaming/streaming_gateway/camera_streamer/message_builder.py +60 -15
- matrice_streaming/streaming_gateway/camera_streamer/nvdec.py +1330 -0
- matrice_streaming/streaming_gateway/camera_streamer/nvdec_worker_manager.py +412 -0
- matrice_streaming/streaming_gateway/camera_streamer/platform_pipelines.py +680 -0
- matrice_streaming/streaming_gateway/camera_streamer/stream_statistics.py +111 -4
- matrice_streaming/streaming_gateway/camera_streamer/video_capture_manager.py +223 -27
- matrice_streaming/streaming_gateway/camera_streamer/worker_manager.py +694 -0
- matrice_streaming/streaming_gateway/debug/__init__.py +27 -2
- matrice_streaming/streaming_gateway/debug/benchmark.py +727 -0
- matrice_streaming/streaming_gateway/debug/debug_gstreamer_gateway.py +599 -0
- matrice_streaming/streaming_gateway/debug/debug_streaming_gateway.py +245 -95
- matrice_streaming/streaming_gateway/debug/debug_utils.py +29 -0
- matrice_streaming/streaming_gateway/debug/test_videoplayback.py +318 -0
- matrice_streaming/streaming_gateway/dynamic_camera_manager.py +656 -39
- matrice_streaming/streaming_gateway/metrics_reporter.py +676 -139
- matrice_streaming/streaming_gateway/streaming_action.py +71 -20
- matrice_streaming/streaming_gateway/streaming_gateway.py +1026 -78
- matrice_streaming/streaming_gateway/streaming_gateway_utils.py +175 -20
- matrice_streaming/streaming_gateway/streaming_status_listener.py +89 -0
- {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/METADATA +1 -1
- matrice_streaming-0.1.65.dist-info/RECORD +56 -0
- matrice_streaming-0.1.14.dist-info/RECORD +0 -38
- {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/WHEEL +0 -0
- {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/licenses/LICENSE.txt +0 -0
- {matrice_streaming-0.1.14.dist-info → matrice_streaming-0.1.65.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"""Platform-specific GStreamer pipeline builders.
|
|
2
|
+
|
|
3
|
+
This module implements the Strategy pattern for constructing optimized GStreamer pipelines
|
|
4
|
+
based on detected hardware platform (Jetson, Desktop NVIDIA GPU, Intel/AMD GPU, CPU-only).
|
|
5
|
+
|
|
6
|
+
Each builder encapsulates platform-specific optimizations:
|
|
7
|
+
- Jetson: nvjpegenc, nvvidconv, NVMM memory, nvv4l2decoder
|
|
8
|
+
- Desktop NVIDIA GPU: nvdec decode, nvh264enc encode, CPU jpegenc (no HW JPEG)
|
|
9
|
+
- Intel/AMD GPU: VAAPI (vaapijpegenc, vaapih264dec)
|
|
10
|
+
- CPU-only: Software fallback (x264enc, jpegenc, avdec_h264)
|
|
11
|
+
"""
|
|
12
|
+
import logging
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from typing import Dict, Tuple, Optional
|
|
15
|
+
|
|
16
|
+
from .device_detection import PlatformInfo, PlatformType
|
|
17
|
+
from .gstreamer_camera_streamer import GStreamerConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PipelineBuilder(ABC):
|
|
21
|
+
"""Abstract base class for platform-specific pipeline builders.
|
|
22
|
+
|
|
23
|
+
Implements Template Method pattern with platform-specific overrides.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: GStreamerConfig, platform_info: PlatformInfo):
|
|
27
|
+
"""Initialize pipeline builder.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
config: GStreamer configuration
|
|
31
|
+
platform_info: Detected platform information
|
|
32
|
+
"""
|
|
33
|
+
self.config = config
|
|
34
|
+
self.platform_info = platform_info
|
|
35
|
+
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def build_decode_element(self, source_type: str, source: str) -> str:
|
|
39
|
+
"""Build hardware-accelerated decode element for source.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
source_type: Type of source (rtsp, file, camera)
|
|
43
|
+
source: Source URI or path
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
GStreamer decode element string
|
|
47
|
+
"""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def build_convert_element(self) -> str:
|
|
52
|
+
"""Build color conversion element (CPU or GPU accelerated).
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
GStreamer converter element string
|
|
56
|
+
"""
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def build_encode_element(self, encoder: str, quality: int, bitrate: int) -> Tuple[str, str]:
|
|
61
|
+
"""Build encoder element and output caps.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
encoder: Encoder type (jpeg, h264, h265)
|
|
65
|
+
quality: Quality setting (JPEG quality or video quality hint)
|
|
66
|
+
bitrate: Bitrate for video encoders (bps)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Tuple of (encoder_string, output_caps)
|
|
70
|
+
"""
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def build_memory_element(self, encoder: str) -> str:
|
|
75
|
+
"""Build memory transfer/format element (NVMM, CUDA, standard).
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
encoder: Encoder type being used
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
GStreamer memory/format caps string
|
|
82
|
+
"""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
def build_complete_pipeline(self, builder_config: Dict) -> str:
|
|
86
|
+
"""Build complete GStreamer pipeline (Template Method).
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
builder_config: Pipeline configuration dict with:
|
|
90
|
+
- source_type: str
|
|
91
|
+
- source: str
|
|
92
|
+
- width: int
|
|
93
|
+
- height: int
|
|
94
|
+
- fps: int
|
|
95
|
+
- encoder: str
|
|
96
|
+
- quality: int
|
|
97
|
+
- bitrate: int
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Complete GStreamer pipeline string
|
|
101
|
+
"""
|
|
102
|
+
source_type = builder_config['source_type']
|
|
103
|
+
source = builder_config['source']
|
|
104
|
+
width = builder_config['width']
|
|
105
|
+
height = builder_config['height']
|
|
106
|
+
fps = builder_config['fps']
|
|
107
|
+
encoder = builder_config['encoder']
|
|
108
|
+
quality = builder_config['quality']
|
|
109
|
+
bitrate = builder_config['bitrate']
|
|
110
|
+
|
|
111
|
+
# Build pipeline elements
|
|
112
|
+
decode = self.build_decode_element(source_type, source)
|
|
113
|
+
convert = self.build_convert_element()
|
|
114
|
+
memory_caps = self.build_memory_element(encoder)
|
|
115
|
+
encode_elem, caps_out = self.build_encode_element(encoder, quality, bitrate)
|
|
116
|
+
|
|
117
|
+
# Assemble pipeline
|
|
118
|
+
pipeline = self._assemble_pipeline(
|
|
119
|
+
decode, convert, memory_caps, encode_elem, caps_out,
|
|
120
|
+
width, height, fps
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if self.config.verbose_pipeline_logging:
|
|
124
|
+
self.logger.info(f"Pipeline: {pipeline}")
|
|
125
|
+
|
|
126
|
+
return pipeline
|
|
127
|
+
|
|
128
|
+
def _assemble_pipeline(
|
|
129
|
+
self,
|
|
130
|
+
decode: str,
|
|
131
|
+
convert: str,
|
|
132
|
+
memory_caps: str,
|
|
133
|
+
encode: str,
|
|
134
|
+
caps_out: str,
|
|
135
|
+
width: int,
|
|
136
|
+
height: int,
|
|
137
|
+
fps: int
|
|
138
|
+
) -> str:
|
|
139
|
+
"""Assemble complete pipeline from components.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
decode: Decode element string
|
|
143
|
+
convert: Convert element string
|
|
144
|
+
memory_caps: Memory/format caps string
|
|
145
|
+
encode: Encode element string
|
|
146
|
+
caps_out: Output caps string
|
|
147
|
+
width: Target width
|
|
148
|
+
height: Target height
|
|
149
|
+
fps: Target FPS
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Complete pipeline string
|
|
153
|
+
"""
|
|
154
|
+
# Build pipeline with appropriate queue settings
|
|
155
|
+
# Use smaller buffer for hardware encoders (nvh264enc, nvh265enc, nvjpegenc, nvv4l2h264enc, etc.)
|
|
156
|
+
is_hw_encoder = any(hw in encode for hw in ['nvh264enc', 'nvh265enc', 'nvjpegenc', 'nvv4l2', 'vaapi'])
|
|
157
|
+
buffer_size = 1 if is_hw_encoder else 2
|
|
158
|
+
|
|
159
|
+
if memory_caps:
|
|
160
|
+
# Hardware-accelerated pipeline with specific memory format
|
|
161
|
+
pipeline = (
|
|
162
|
+
f"{decode} ! "
|
|
163
|
+
f"{convert} ! "
|
|
164
|
+
f"video/x-raw,width={width},height={height} ! "
|
|
165
|
+
f"{memory_caps} ! "
|
|
166
|
+
f"videorate ! video/x-raw,framerate={fps}/1 ! "
|
|
167
|
+
f"queue max-size-buffers={buffer_size} leaky=downstream ! "
|
|
168
|
+
f"{encode} ! {caps_out} ! "
|
|
169
|
+
f"queue max-size-buffers={buffer_size} leaky=downstream ! "
|
|
170
|
+
f"appsink name=sink sync=false async=false "
|
|
171
|
+
f"max-buffers={buffer_size} drop=true enable-last-sample=false"
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
# Standard pipeline
|
|
175
|
+
pipeline = (
|
|
176
|
+
f"{decode} ! "
|
|
177
|
+
f"{convert} ! "
|
|
178
|
+
f"videoscale ! "
|
|
179
|
+
f"video/x-raw,width={width},height={height} ! "
|
|
180
|
+
f"videorate ! video/x-raw,framerate={fps}/1 ! "
|
|
181
|
+
f"queue max-size-buffers={buffer_size} leaky=downstream ! "
|
|
182
|
+
f"{encode} ! {caps_out} ! "
|
|
183
|
+
f"queue max-size-buffers={buffer_size} leaky=downstream ! "
|
|
184
|
+
f"appsink name=sink sync=false async=false "
|
|
185
|
+
f"max-buffers={buffer_size} drop=true enable-last-sample=false"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return pipeline
|
|
189
|
+
|
|
190
|
+
def build_dual_appsink_pipeline(self, builder_config: Dict) -> str:
|
|
191
|
+
"""Build pipeline with dual appsinks for FrameOptimizer.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
builder_config: Pipeline configuration dict
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Pipeline string with similarity-sink and output-sink
|
|
198
|
+
"""
|
|
199
|
+
source_type = builder_config['source_type']
|
|
200
|
+
source = builder_config['source']
|
|
201
|
+
width = builder_config['width']
|
|
202
|
+
height = builder_config['height']
|
|
203
|
+
fps = builder_config['fps']
|
|
204
|
+
encoder = builder_config['encoder']
|
|
205
|
+
quality = builder_config['quality']
|
|
206
|
+
bitrate = builder_config['bitrate']
|
|
207
|
+
|
|
208
|
+
# Build pipeline elements
|
|
209
|
+
decode = self.build_decode_element(source_type, source)
|
|
210
|
+
convert = self.build_convert_element()
|
|
211
|
+
memory_caps = self.build_memory_element(encoder)
|
|
212
|
+
encode_elem, caps_out = self.build_encode_element(encoder, quality, bitrate)
|
|
213
|
+
|
|
214
|
+
# Similarity detection: downscale to 160x120 for performance (16x fewer pixels)
|
|
215
|
+
similarity_width = 160
|
|
216
|
+
similarity_height = 120
|
|
217
|
+
|
|
218
|
+
# Build dual-sink pipeline with tee
|
|
219
|
+
pipeline = (
|
|
220
|
+
f"{decode} ! "
|
|
221
|
+
f"{convert} ! "
|
|
222
|
+
f"video/x-raw,width={width},height={height} ! "
|
|
223
|
+
f"videorate ! video/x-raw,framerate={fps}/1 ! "
|
|
224
|
+
f"tee name=t "
|
|
225
|
+
# Branch 1: Similarity detection (downscaled, raw frames)
|
|
226
|
+
f"t. ! queue max-size-buffers=1 leaky=downstream ! "
|
|
227
|
+
f"videoscale ! video/x-raw,width={similarity_width},height={similarity_height},format=RGB ! "
|
|
228
|
+
f"appsink name=similarity-sink sync=false async=false max-buffers=1 drop=true enable-last-sample=false "
|
|
229
|
+
# Branch 2: Full encode for output
|
|
230
|
+
f"t. ! queue max-size-buffers=1 leaky=downstream ! "
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Add memory caps if needed (for hardware encoding)
|
|
234
|
+
if memory_caps:
|
|
235
|
+
pipeline += f"{memory_caps} ! "
|
|
236
|
+
|
|
237
|
+
pipeline += (
|
|
238
|
+
f"{encode_elem} ! {caps_out} ! "
|
|
239
|
+
f"appsink name=output-sink sync=false async=false max-buffers=1 drop=true enable-last-sample=false"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if self.config.verbose_pipeline_logging:
|
|
243
|
+
self.logger.info(f"Dual-appsink pipeline: {pipeline}")
|
|
244
|
+
|
|
245
|
+
return pipeline
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class JetsonPipelineBuilder(PipelineBuilder):
|
|
249
|
+
"""Jetson-optimized pipeline builder.
|
|
250
|
+
|
|
251
|
+
Optimizations:
|
|
252
|
+
- nvjpegenc for hardware JPEG encoding (10x faster than CPU)
|
|
253
|
+
- nvvidconv for GPU-accelerated color conversion
|
|
254
|
+
- NVMM zero-copy memory
|
|
255
|
+
- nvv4l2decoder for hardware video decode
|
|
256
|
+
- nvv4l2h264enc/h265enc for hardware video encode
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
def build_decode_element(self, source_type: str, source: str) -> str:
|
|
260
|
+
if not self.config.use_hardware_decode:
|
|
261
|
+
return self._build_cpu_decode(source_type, source)
|
|
262
|
+
|
|
263
|
+
if source_type == "rtsp":
|
|
264
|
+
# RTSP with hardware decode
|
|
265
|
+
return (
|
|
266
|
+
f"rtspsrc location={source} latency=50 buffer-mode=auto ! "
|
|
267
|
+
f"rtph264depay ! h264parse ! "
|
|
268
|
+
f"nvv4l2decoder enable-max-performance=1"
|
|
269
|
+
)
|
|
270
|
+
elif source_type == "file":
|
|
271
|
+
# Video file with hardware decode
|
|
272
|
+
ext = source.lower().split('.')[-1] if '.' in source else ''
|
|
273
|
+
if ext in ('mp4', 'mov', 'm4v'):
|
|
274
|
+
return (
|
|
275
|
+
f"filesrc location={source} ! qtdemux ! h264parse ! "
|
|
276
|
+
f"nvv4l2decoder"
|
|
277
|
+
)
|
|
278
|
+
elif ext in ('mkv', 'webm'):
|
|
279
|
+
return f"filesrc location={source} ! matroskademux ! h264parse ! nvv4l2decoder"
|
|
280
|
+
elif ext == 'avi':
|
|
281
|
+
return f"filesrc location={source} ! avidemux ! h264parse ! nvv4l2decoder"
|
|
282
|
+
else:
|
|
283
|
+
# Unknown format, try qtdemux
|
|
284
|
+
return f"filesrc location={source} ! qtdemux ! h264parse ! nvv4l2decoder"
|
|
285
|
+
|
|
286
|
+
# Fallback
|
|
287
|
+
return self._build_cpu_decode(source_type, source)
|
|
288
|
+
|
|
289
|
+
def _build_cpu_decode(self, source_type: str, source: str) -> str:
|
|
290
|
+
"""Fallback CPU decode."""
|
|
291
|
+
if source_type == "rtsp":
|
|
292
|
+
return (
|
|
293
|
+
f"rtspsrc location={source} latency=100 ! "
|
|
294
|
+
f"rtph264depay ! h264parse ! avdec_h264"
|
|
295
|
+
)
|
|
296
|
+
elif source_type == "file":
|
|
297
|
+
ext = source.lower().split('.')[-1] if '.' in source else ''
|
|
298
|
+
if ext in ('mp4', 'mov'):
|
|
299
|
+
return f"filesrc location={source} ! qtdemux ! avdec_h264"
|
|
300
|
+
elif ext in ('mkv', 'webm'):
|
|
301
|
+
return f"filesrc location={source} ! matroskademux ! avdec_h264"
|
|
302
|
+
else:
|
|
303
|
+
return f"filesrc location={source} ! qtdemux ! avdec_h264"
|
|
304
|
+
|
|
305
|
+
return "videotestsrc pattern=smpte is-live=true"
|
|
306
|
+
|
|
307
|
+
def build_convert_element(self) -> str:
|
|
308
|
+
# Use nvvidconv (GPU-accelerated) instead of videoconvert
|
|
309
|
+
if 'nvvidconv' in self.platform_info.available_converters:
|
|
310
|
+
return "nvvidconv"
|
|
311
|
+
return "videoconvert"
|
|
312
|
+
|
|
313
|
+
def build_encode_element(self, encoder: str, quality: int, bitrate: int) -> Tuple[str, str]:
|
|
314
|
+
if encoder == "jpeg":
|
|
315
|
+
# Hardware JPEG encoding - HUGE performance improvement
|
|
316
|
+
if 'nvjpegenc' in self.platform_info.available_encoders and self.config.use_hardware_jpeg:
|
|
317
|
+
return (
|
|
318
|
+
f"nvjpegenc quality={quality}",
|
|
319
|
+
"image/jpeg"
|
|
320
|
+
)
|
|
321
|
+
# Fallback to CPU JPEG
|
|
322
|
+
return (f"jpegenc quality={quality} idct-method=ifast", "image/jpeg")
|
|
323
|
+
|
|
324
|
+
elif encoder == "h264":
|
|
325
|
+
# Hardware H.264 encoding
|
|
326
|
+
if 'nvv4l2h264enc' in self.platform_info.available_encoders:
|
|
327
|
+
return (
|
|
328
|
+
f"nvv4l2h264enc preset-level=1 bitrate={bitrate} "
|
|
329
|
+
f"iframeinterval={self.config.gop_size} insert-sps-pps=true",
|
|
330
|
+
"video/x-h264,profile=main"
|
|
331
|
+
)
|
|
332
|
+
elif 'nvh264enc' in self.platform_info.available_encoders:
|
|
333
|
+
return (
|
|
334
|
+
f"nvh264enc preset={self.config.preset} bitrate={bitrate//1000} "
|
|
335
|
+
f"gop-size={self.config.gop_size} zerolatency=true bframes=0",
|
|
336
|
+
"video/x-h264,profile=main"
|
|
337
|
+
)
|
|
338
|
+
# Fallback to x264
|
|
339
|
+
return (
|
|
340
|
+
f"x264enc speed-preset=ultrafast tune=zerolatency bitrate={bitrate//1000} threads=2",
|
|
341
|
+
"video/x-h264,profile=baseline"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
elif encoder == "h265":
|
|
345
|
+
# Hardware H.265 encoding
|
|
346
|
+
if 'nvv4l2h265enc' in self.platform_info.available_encoders:
|
|
347
|
+
return (
|
|
348
|
+
f"nvv4l2h265enc preset-level=1 bitrate={bitrate} "
|
|
349
|
+
f"iframeinterval={self.config.gop_size}",
|
|
350
|
+
"video/x-h265,profile=main"
|
|
351
|
+
)
|
|
352
|
+
elif 'nvh265enc' in self.platform_info.available_encoders:
|
|
353
|
+
return (
|
|
354
|
+
f"nvh265enc preset={self.config.preset} bitrate={bitrate//1000} "
|
|
355
|
+
f"gop-size={self.config.gop_size}",
|
|
356
|
+
"video/x-h265,profile=main"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Unknown encoder
|
|
360
|
+
raise ValueError(f"Unsupported encoder: {encoder}")
|
|
361
|
+
|
|
362
|
+
def build_memory_element(self, encoder: str) -> str:
|
|
363
|
+
# NVMM zero-copy memory for Jetson
|
|
364
|
+
if self.config.jetson_use_nvmm and self.platform_info.supports_nvmm:
|
|
365
|
+
if encoder == "jpeg" and 'nvjpegenc' in self.platform_info.available_encoders:
|
|
366
|
+
return "video/x-raw(memory:NVMM),format=NV12"
|
|
367
|
+
elif encoder in ("h264", "h265"):
|
|
368
|
+
return "video/x-raw(memory:NVMM),format=NV12"
|
|
369
|
+
return ""
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class DesktopNvidiaGpuPipelineBuilder(PipelineBuilder):
|
|
373
|
+
"""Desktop NVIDIA GPU pipeline builder.
|
|
374
|
+
|
|
375
|
+
Optimizations:
|
|
376
|
+
- nvdec for hardware video decode
|
|
377
|
+
- nvh264enc/nvh265enc for hardware video encode
|
|
378
|
+
- CUDA memory for zero-copy (video codecs only)
|
|
379
|
+
|
|
380
|
+
Limitations:
|
|
381
|
+
- NO hardware JPEG encoding (GStreamer limitation on desktop GPUs)
|
|
382
|
+
- Must use CPU jpegenc
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
def build_decode_element(self, source_type: str, source: str) -> str:
|
|
386
|
+
if not self.config.use_hardware_decode:
|
|
387
|
+
return self._build_cpu_decode(source_type, source)
|
|
388
|
+
|
|
389
|
+
if 'nvdec' not in self.platform_info.available_decoders:
|
|
390
|
+
return self._build_cpu_decode(source_type, source)
|
|
391
|
+
|
|
392
|
+
if source_type == "rtsp":
|
|
393
|
+
return (
|
|
394
|
+
f"rtspsrc location={source} latency=100 buffer-mode=auto ! "
|
|
395
|
+
f"rtph264depay ! h264parse ! "
|
|
396
|
+
f"nvdec"
|
|
397
|
+
)
|
|
398
|
+
elif source_type == "file":
|
|
399
|
+
ext = source.lower().split('.')[-1] if '.' in source else ''
|
|
400
|
+
if ext in ('mp4', 'mov', 'm4v'):
|
|
401
|
+
return f"filesrc location={source} ! qtdemux ! h264parse ! nvdec"
|
|
402
|
+
elif ext in ('mkv', 'webm'):
|
|
403
|
+
return f"filesrc location={source} ! matroskademux ! h264parse ! nvdec"
|
|
404
|
+
elif ext == 'avi':
|
|
405
|
+
return f"filesrc location={source} ! avidemux ! h264parse ! nvdec"
|
|
406
|
+
else:
|
|
407
|
+
return f"filesrc location={source} ! qtdemux ! h264parse ! nvdec"
|
|
408
|
+
|
|
409
|
+
return self._build_cpu_decode(source_type, source)
|
|
410
|
+
|
|
411
|
+
def _build_cpu_decode(self, source_type: str, source: str) -> str:
|
|
412
|
+
if source_type == "rtsp":
|
|
413
|
+
return f"rtspsrc location={source} latency=100 ! rtph264depay ! h264parse ! avdec_h264"
|
|
414
|
+
elif source_type == "file":
|
|
415
|
+
ext = source.lower().split('.')[-1] if '.' in source else ''
|
|
416
|
+
if ext in ('mp4', 'mov'):
|
|
417
|
+
return f"filesrc location={source} ! qtdemux ! avdec_h264"
|
|
418
|
+
elif ext in ('mkv', 'webm'):
|
|
419
|
+
return f"filesrc location={source} ! matroskademux ! avdec_h264"
|
|
420
|
+
else:
|
|
421
|
+
return f"filesrc location={source} ! qtdemux ! avdec_h264"
|
|
422
|
+
return "videotestsrc pattern=smpte is-live=true"
|
|
423
|
+
|
|
424
|
+
def build_convert_element(self) -> str:
|
|
425
|
+
# Use standard videoconvert (GPU conversion for CUDA memory handled separately)
|
|
426
|
+
return "videoconvert"
|
|
427
|
+
|
|
428
|
+
def build_encode_element(self, encoder: str, quality: int, bitrate: int) -> Tuple[str, str]:
|
|
429
|
+
if encoder == "jpeg":
|
|
430
|
+
# LIMITATION: No hardware JPEG on desktop GPUs in GStreamer
|
|
431
|
+
# Must use CPU jpegenc
|
|
432
|
+
self.logger.debug("Using CPU JPEG encoding (no hardware support on desktop GPU)")
|
|
433
|
+
return (
|
|
434
|
+
f"jpegenc quality={quality} idct-method=ifast",
|
|
435
|
+
"image/jpeg"
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
elif encoder == "h264":
|
|
439
|
+
# Hardware H.264 encoding with NVENC
|
|
440
|
+
if 'nvh264enc' in self.platform_info.available_encoders:
|
|
441
|
+
return (
|
|
442
|
+
f"nvh264enc cuda-device-id={self.config.gpu_id} "
|
|
443
|
+
f"preset={self.config.preset} bitrate={bitrate//1000} "
|
|
444
|
+
f"gop-size={self.config.gop_size} zerolatency=true "
|
|
445
|
+
f"rc-lookahead=0 bframes=0 rc-mode=cbr-ld-hq",
|
|
446
|
+
"video/x-h264,profile=main"
|
|
447
|
+
)
|
|
448
|
+
# Fallback to x264
|
|
449
|
+
return (
|
|
450
|
+
f"x264enc speed-preset=ultrafast tune=zerolatency bitrate={bitrate//1000} threads=4",
|
|
451
|
+
"video/x-h264,profile=baseline"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
elif encoder == "h265":
|
|
455
|
+
# Hardware H.265 encoding with NVENC
|
|
456
|
+
if 'nvh265enc' in self.platform_info.available_encoders:
|
|
457
|
+
return (
|
|
458
|
+
f"nvh265enc cuda-device-id={self.config.gpu_id} "
|
|
459
|
+
f"preset={self.config.preset} bitrate={bitrate//1000} "
|
|
460
|
+
f"gop-size={self.config.gop_size} zerolatency=true "
|
|
461
|
+
f"rc-lookahead=0 bframes=0",
|
|
462
|
+
"video/x-h265,profile=main"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
raise ValueError(f"Unsupported encoder: {encoder}")
|
|
466
|
+
|
|
467
|
+
def build_memory_element(self, encoder: str) -> str:
|
|
468
|
+
# CUDA memory for video codecs (if enabled and using NVENC)
|
|
469
|
+
if encoder in ("h264", "h265") and self.config.use_cuda_memory:
|
|
470
|
+
if f"nv{encoder}enc" in self.platform_info.available_encoders:
|
|
471
|
+
return f"cudaupload cuda-device-id={self.config.gpu_id} ! video/x-raw(memory:CUDAMemory),format=NV12"
|
|
472
|
+
return ""
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class IntelGpuPipelineBuilder(PipelineBuilder):
|
|
476
|
+
"""Intel GPU pipeline builder with VAAPI.
|
|
477
|
+
|
|
478
|
+
Optimizations:
|
|
479
|
+
- vaapijpegenc for hardware JPEG encoding
|
|
480
|
+
- vaapih264enc/h265enc for hardware video encode
|
|
481
|
+
- vaapih264dec for hardware video decode
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
def build_decode_element(self, source_type: str, source: str) -> str:
|
|
485
|
+
if not self.config.use_hardware_decode or 'vaapih264dec' not in self.platform_info.available_decoders:
|
|
486
|
+
return self._build_cpu_decode(source_type, source)
|
|
487
|
+
|
|
488
|
+
if source_type == "rtsp":
|
|
489
|
+
return (
|
|
490
|
+
f"rtspsrc location={source} latency=100 ! "
|
|
491
|
+
f"rtph264depay ! h264parse ! "
|
|
492
|
+
f"vaapih264dec"
|
|
493
|
+
)
|
|
494
|
+
elif source_type == "file":
|
|
495
|
+
ext = source.lower().split('.')[-1] if '.' in source else ''
|
|
496
|
+
if ext in ('mp4', 'mov', 'm4v'):
|
|
497
|
+
return f"filesrc location={source} ! qtdemux ! h264parse ! vaapih264dec"
|
|
498
|
+
elif ext in ('mkv', 'webm'):
|
|
499
|
+
return f"filesrc location={source} ! matroskademux ! h264parse ! vaapih264dec"
|
|
500
|
+
else:
|
|
501
|
+
return f"filesrc location={source} ! qtdemux ! h264parse ! vaapih264dec"
|
|
502
|
+
|
|
503
|
+
return self._build_cpu_decode(source_type, source)
|
|
504
|
+
|
|
505
|
+
def _build_cpu_decode(self, source_type: str, source: str) -> str:
|
|
506
|
+
if source_type == "rtsp":
|
|
507
|
+
return f"rtspsrc location={source} latency=100 ! rtph264depay ! h264parse ! avdec_h264"
|
|
508
|
+
elif source_type == "file":
|
|
509
|
+
ext = source.lower().split('.')[-1] if '.' in source else ''
|
|
510
|
+
if ext in ('mp4', 'mov'):
|
|
511
|
+
return f"filesrc location={source} ! qtdemux ! avdec_h264"
|
|
512
|
+
else:
|
|
513
|
+
return f"filesrc location={source} ! matroskademux ! avdec_h264"
|
|
514
|
+
return "videotestsrc pattern=smpte is-live=true"
|
|
515
|
+
|
|
516
|
+
def build_convert_element(self) -> str:
|
|
517
|
+
return "videoconvert"
|
|
518
|
+
|
|
519
|
+
def build_encode_element(self, encoder: str, quality: int, bitrate: int) -> Tuple[str, str]:
|
|
520
|
+
if encoder == "jpeg":
|
|
521
|
+
# Hardware JPEG encoding via VAAPI
|
|
522
|
+
if 'vaapijpegenc' in self.platform_info.available_encoders and self.config.use_hardware_jpeg:
|
|
523
|
+
return (
|
|
524
|
+
f"vaapijpegenc quality={quality}",
|
|
525
|
+
"image/jpeg"
|
|
526
|
+
)
|
|
527
|
+
# Fallback to CPU
|
|
528
|
+
return (f"jpegenc quality={quality} idct-method=ifast", "image/jpeg")
|
|
529
|
+
|
|
530
|
+
elif encoder == "h264":
|
|
531
|
+
# Hardware H.264 encoding via VAAPI
|
|
532
|
+
if 'vaapih264enc' in self.platform_info.available_encoders:
|
|
533
|
+
return (
|
|
534
|
+
f"vaapih264enc bitrate={bitrate//1000} rate-control=cbr keyframe-period={self.config.gop_size}",
|
|
535
|
+
"video/x-h264,profile=main"
|
|
536
|
+
)
|
|
537
|
+
# Fallback to x264
|
|
538
|
+
return (
|
|
539
|
+
f"x264enc speed-preset=ultrafast tune=zerolatency bitrate={bitrate//1000} threads=4",
|
|
540
|
+
"video/x-h264,profile=baseline"
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
elif encoder == "h265":
|
|
544
|
+
# Hardware H.265 encoding via VAAPI
|
|
545
|
+
if 'vaapih265enc' in self.platform_info.available_encoders:
|
|
546
|
+
return (
|
|
547
|
+
f"vaapih265enc bitrate={bitrate//1000} rate-control=cbr",
|
|
548
|
+
"video/x-h265,profile=main"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
raise ValueError(f"Unsupported encoder: {encoder}")
|
|
552
|
+
|
|
553
|
+
def build_memory_element(self, encoder: str) -> str:
|
|
554
|
+
# VAAPI uses its own internal memory management
|
|
555
|
+
return ""
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
class AmdGpuPipelineBuilder(IntelGpuPipelineBuilder):
|
|
559
|
+
"""AMD GPU pipeline builder with VAAPI.
|
|
560
|
+
|
|
561
|
+
Inherits from IntelGpuPipelineBuilder as AMD uses same VAAPI interface.
|
|
562
|
+
Can be extended in future for AMD-specific optimizations.
|
|
563
|
+
"""
|
|
564
|
+
pass
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
class CpuOnlyPipelineBuilder(PipelineBuilder):
|
|
568
|
+
"""CPU-only fallback pipeline builder.
|
|
569
|
+
|
|
570
|
+
Uses software encoders/decoders:
|
|
571
|
+
- jpegenc for JPEG
|
|
572
|
+
- x264enc for H.264
|
|
573
|
+
- avdec_h264 for decode
|
|
574
|
+
"""
|
|
575
|
+
|
|
576
|
+
def build_decode_element(self, source_type: str, source: str) -> str:
|
|
577
|
+
if source_type == "rtsp":
|
|
578
|
+
return (
|
|
579
|
+
f"rtspsrc location={source} latency=100 buffer-mode=auto ! "
|
|
580
|
+
f"rtph264depay ! h264parse ! "
|
|
581
|
+
f"avdec_h264"
|
|
582
|
+
)
|
|
583
|
+
elif source_type == "file":
|
|
584
|
+
ext = source.lower().split('.')[-1] if '.' in source else ''
|
|
585
|
+
if ext in ('mp4', 'mov', 'm4v'):
|
|
586
|
+
return f"filesrc location={source} ! qtdemux ! avdec_h264"
|
|
587
|
+
elif ext in ('mkv', 'webm'):
|
|
588
|
+
return f"filesrc location={source} ! matroskademux ! avdec_h264"
|
|
589
|
+
elif ext == 'avi':
|
|
590
|
+
return f"filesrc location={source} ! avidemux ! avdec_h264"
|
|
591
|
+
else:
|
|
592
|
+
return f"filesrc location={source} ! qtdemux ! avdec_h264"
|
|
593
|
+
|
|
594
|
+
return "videotestsrc pattern=smpte is-live=true"
|
|
595
|
+
|
|
596
|
+
def build_convert_element(self) -> str:
|
|
597
|
+
return "videoconvert"
|
|
598
|
+
|
|
599
|
+
def build_encode_element(self, encoder: str, quality: int, bitrate: int) -> Tuple[str, str]:
|
|
600
|
+
if encoder == "jpeg":
|
|
601
|
+
return (f"jpegenc quality={quality} idct-method=ifast", "image/jpeg")
|
|
602
|
+
|
|
603
|
+
elif encoder == "h264":
|
|
604
|
+
# Software H.264 encoding with x264
|
|
605
|
+
threads = min(4, self.platform_info.gpu_count or 4)
|
|
606
|
+
return (
|
|
607
|
+
f"x264enc speed-preset=ultrafast tune=zerolatency "
|
|
608
|
+
f"bitrate={bitrate//1000} key-int-max={self.config.gop_size} "
|
|
609
|
+
f"bframes=0 threads={threads} sliced-threads=true",
|
|
610
|
+
"video/x-h264,profile=baseline"
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
elif encoder == "h265":
|
|
614
|
+
# Software H.265 encoding (if available)
|
|
615
|
+
if 'x265enc' in self.platform_info.available_encoders:
|
|
616
|
+
return (
|
|
617
|
+
f"x265enc speed-preset=ultrafast bitrate={bitrate//1000} "
|
|
618
|
+
f"key-int-max={self.config.gop_size}",
|
|
619
|
+
"video/x-h265,profile=main"
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
raise ValueError(f"Unsupported encoder: {encoder}")
|
|
623
|
+
|
|
624
|
+
def build_memory_element(self, encoder: str) -> str:
|
|
625
|
+
return ""
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
class PipelineFactory:
|
|
629
|
+
"""Factory for selecting appropriate pipeline builder based on platform."""
|
|
630
|
+
|
|
631
|
+
@staticmethod
|
|
632
|
+
def get_builder(config: GStreamerConfig, platform_info: PlatformInfo) -> PipelineBuilder:
|
|
633
|
+
"""Select and instantiate appropriate pipeline builder.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
config: GStreamer configuration
|
|
637
|
+
platform_info: Detected platform information
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
Platform-specific PipelineBuilder instance
|
|
641
|
+
"""
|
|
642
|
+
logger = logging.getLogger(__name__)
|
|
643
|
+
|
|
644
|
+
# Manual override if specified (and override enabled)
|
|
645
|
+
if config.platform != "auto" and config.enable_platform_override:
|
|
646
|
+
platform_map = {
|
|
647
|
+
"jetson": JetsonPipelineBuilder,
|
|
648
|
+
"desktop-gpu": DesktopNvidiaGpuPipelineBuilder,
|
|
649
|
+
"intel": IntelGpuPipelineBuilder,
|
|
650
|
+
"amd": AmdGpuPipelineBuilder,
|
|
651
|
+
"cpu": CpuOnlyPipelineBuilder,
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
builder_class = platform_map.get(config.platform)
|
|
655
|
+
if builder_class:
|
|
656
|
+
logger.info(f"Using manual platform override: {config.platform}")
|
|
657
|
+
return builder_class(config, platform_info)
|
|
658
|
+
else:
|
|
659
|
+
logger.warning(f"Invalid platform override '{config.platform}', using auto-detect")
|
|
660
|
+
|
|
661
|
+
# Auto-detect based on platform_info
|
|
662
|
+
if platform_info.platform_type == PlatformType.JETSON:
|
|
663
|
+
logger.info("Selected JetsonPipelineBuilder")
|
|
664
|
+
return JetsonPipelineBuilder(config, platform_info)
|
|
665
|
+
|
|
666
|
+
elif platform_info.platform_type == PlatformType.DESKTOP_NVIDIA_GPU:
|
|
667
|
+
logger.info("Selected DesktopNvidiaGpuPipelineBuilder")
|
|
668
|
+
return DesktopNvidiaGpuPipelineBuilder(config, platform_info)
|
|
669
|
+
|
|
670
|
+
elif platform_info.platform_type == PlatformType.INTEL_GPU:
|
|
671
|
+
logger.info("Selected IntelGpuPipelineBuilder")
|
|
672
|
+
return IntelGpuPipelineBuilder(config, platform_info)
|
|
673
|
+
|
|
674
|
+
elif platform_info.platform_type == PlatformType.AMD_GPU:
|
|
675
|
+
logger.info("Selected AmdGpuPipelineBuilder")
|
|
676
|
+
return AmdGpuPipelineBuilder(config, platform_info)
|
|
677
|
+
|
|
678
|
+
else:
|
|
679
|
+
logger.info("Selected CpuOnlyPipelineBuilder (fallback)")
|
|
680
|
+
return CpuOnlyPipelineBuilder(config, platform_info)
|