rocket-welder-sdk 0.0.0.dev0__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.
- rocket_welder_sdk/__init__.py +56 -0
- rocket_welder_sdk/bytes_size.py +234 -0
- rocket_welder_sdk/connection_string.py +232 -0
- rocket_welder_sdk/controllers.py +668 -0
- rocket_welder_sdk/gst_metadata.py +411 -0
- rocket_welder_sdk/py.typed +2 -0
- rocket_welder_sdk/rocket_welder_client.py +167 -0
- rocket_welder_sdk-0.0.0.dev0.dist-info/METADATA +497 -0
- rocket_welder_sdk-0.0.0.dev0.dist-info/RECORD +11 -0
- rocket_welder_sdk-0.0.0.dev0.dist-info/WHEEL +5 -0
- rocket_welder_sdk-0.0.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GStreamer metadata structures for RocketWelder SDK.
|
|
3
|
+
Matches C# GstCaps and GstMetadata functionality.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import numpy.typing as npt
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class GstCaps:
|
|
19
|
+
"""
|
|
20
|
+
GStreamer capabilities representation.
|
|
21
|
+
|
|
22
|
+
Represents video format capabilities including format, dimensions, framerate, etc.
|
|
23
|
+
Matches the C# GstCaps implementation with proper parsing and numpy integration.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
width: int
|
|
27
|
+
height: int
|
|
28
|
+
format: str
|
|
29
|
+
depth_type: type[np.uint8] | type[np.uint16]
|
|
30
|
+
channels: int
|
|
31
|
+
bytes_per_pixel: int
|
|
32
|
+
framerate_num: int | None = None
|
|
33
|
+
framerate_den: int | None = None
|
|
34
|
+
interlace_mode: str | None = None
|
|
35
|
+
colorimetry: str | None = None
|
|
36
|
+
caps_string: str | None = None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def frame_size(self) -> int:
|
|
40
|
+
"""Calculate the expected frame size in bytes."""
|
|
41
|
+
return self.width * self.height * self.bytes_per_pixel
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def framerate(self) -> float | None:
|
|
45
|
+
"""Get framerate as double (FPS)."""
|
|
46
|
+
if (
|
|
47
|
+
self.framerate_num is not None
|
|
48
|
+
and self.framerate_den is not None
|
|
49
|
+
and self.framerate_den > 0
|
|
50
|
+
):
|
|
51
|
+
return self.framerate_num / self.framerate_den
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def parse(cls, caps_string: str) -> GstCaps:
|
|
56
|
+
"""
|
|
57
|
+
Parse GStreamer caps string.
|
|
58
|
+
Example: "video/x-raw, format=(string)RGB, width=(int)640, height=(int)480, framerate=(fraction)30/1"
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
caps_string: GStreamer caps string
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
GstCaps instance
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: If caps string is invalid
|
|
68
|
+
"""
|
|
69
|
+
if not caps_string or not caps_string.strip():
|
|
70
|
+
raise ValueError("Empty caps string")
|
|
71
|
+
|
|
72
|
+
caps_string = caps_string.strip()
|
|
73
|
+
|
|
74
|
+
# Check if it's a video caps
|
|
75
|
+
if not caps_string.startswith("video/x-raw"):
|
|
76
|
+
raise ValueError(f"Not a video/x-raw caps string: {caps_string}")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
# Parse width
|
|
80
|
+
width_match = re.search(r"width=\(int\)(\d+)", caps_string)
|
|
81
|
+
if not width_match:
|
|
82
|
+
raise ValueError("Missing width in caps string")
|
|
83
|
+
width = int(width_match.group(1))
|
|
84
|
+
|
|
85
|
+
# Parse height
|
|
86
|
+
height_match = re.search(r"height=\(int\)(\d+)", caps_string)
|
|
87
|
+
if not height_match:
|
|
88
|
+
raise ValueError("Missing height in caps string")
|
|
89
|
+
height = int(height_match.group(1))
|
|
90
|
+
|
|
91
|
+
# Parse format
|
|
92
|
+
format_match = re.search(r"format=\(string\)(\w+)", caps_string)
|
|
93
|
+
format_str = format_match.group(1) if format_match else "RGB"
|
|
94
|
+
|
|
95
|
+
# Parse framerate (optional)
|
|
96
|
+
framerate_num = None
|
|
97
|
+
framerate_den = None
|
|
98
|
+
framerate_match = re.search(r"framerate=\(fraction\)(\d+)/(\d+)", caps_string)
|
|
99
|
+
if framerate_match:
|
|
100
|
+
framerate_num = int(framerate_match.group(1))
|
|
101
|
+
framerate_den = int(framerate_match.group(2))
|
|
102
|
+
|
|
103
|
+
# Parse interlace mode (optional)
|
|
104
|
+
interlace_mode = None
|
|
105
|
+
interlace_match = re.search(r"interlace-mode=\(string\)(\w+)", caps_string)
|
|
106
|
+
if interlace_match:
|
|
107
|
+
interlace_mode = interlace_match.group(1)
|
|
108
|
+
|
|
109
|
+
# Parse colorimetry (optional)
|
|
110
|
+
colorimetry = None
|
|
111
|
+
colorimetry_match = re.search(r"colorimetry=\(string\)([\w:]+)", caps_string)
|
|
112
|
+
if colorimetry_match:
|
|
113
|
+
colorimetry = colorimetry_match.group(1)
|
|
114
|
+
|
|
115
|
+
# Map format to numpy dtype and get channel info
|
|
116
|
+
depth_type, channels, bytes_per_pixel = cls._map_gstreamer_format_to_numpy(format_str)
|
|
117
|
+
|
|
118
|
+
return cls(
|
|
119
|
+
width=width,
|
|
120
|
+
height=height,
|
|
121
|
+
format=format_str,
|
|
122
|
+
depth_type=depth_type,
|
|
123
|
+
channels=channels,
|
|
124
|
+
bytes_per_pixel=bytes_per_pixel,
|
|
125
|
+
framerate_num=framerate_num,
|
|
126
|
+
framerate_den=framerate_den,
|
|
127
|
+
interlace_mode=interlace_mode,
|
|
128
|
+
colorimetry=colorimetry,
|
|
129
|
+
caps_string=caps_string,
|
|
130
|
+
)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
raise ValueError(f"Failed to parse caps string: {caps_string}") from e
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_simple(cls, width: int, height: int, format: str = "RGB") -> GstCaps:
|
|
136
|
+
"""
|
|
137
|
+
Create GstCaps from simple parameters.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
width: Frame width
|
|
141
|
+
height: Frame height
|
|
142
|
+
format: Pixel format (default: "RGB")
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
GstCaps instance
|
|
146
|
+
"""
|
|
147
|
+
depth_type, channels, bytes_per_pixel = cls._map_gstreamer_format_to_numpy(format)
|
|
148
|
+
return cls(
|
|
149
|
+
width=width,
|
|
150
|
+
height=height,
|
|
151
|
+
format=format,
|
|
152
|
+
depth_type=depth_type,
|
|
153
|
+
channels=channels,
|
|
154
|
+
bytes_per_pixel=bytes_per_pixel,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _map_gstreamer_format_to_numpy(
|
|
159
|
+
format: str,
|
|
160
|
+
) -> tuple[type[np.uint8] | type[np.uint16], int, int]:
|
|
161
|
+
"""
|
|
162
|
+
Map GStreamer format strings to numpy dtype.
|
|
163
|
+
Reference: https://gstreamer.freedesktop.org/documentation/video/video-format.html
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
format: GStreamer format string
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Tuple of (numpy dtype, channels, bytes_per_pixel)
|
|
170
|
+
"""
|
|
171
|
+
format_upper = format.upper() if format else "RGB"
|
|
172
|
+
|
|
173
|
+
format_map = {
|
|
174
|
+
# RGB formats
|
|
175
|
+
"RGB": (np.uint8, 3, 3),
|
|
176
|
+
"BGR": (np.uint8, 3, 3),
|
|
177
|
+
"RGBA": (np.uint8, 4, 4),
|
|
178
|
+
"BGRA": (np.uint8, 4, 4),
|
|
179
|
+
"ARGB": (np.uint8, 4, 4),
|
|
180
|
+
"ABGR": (np.uint8, 4, 4),
|
|
181
|
+
"RGBX": (np.uint8, 4, 4), # RGB with padding
|
|
182
|
+
"BGRX": (np.uint8, 4, 4), # BGR with padding
|
|
183
|
+
"XRGB": (np.uint8, 4, 4), # RGB with padding
|
|
184
|
+
"XBGR": (np.uint8, 4, 4), # BGR with padding
|
|
185
|
+
# 16-bit RGB formats
|
|
186
|
+
"RGB16": (np.uint16, 3, 6),
|
|
187
|
+
"BGR16": (np.uint16, 3, 6),
|
|
188
|
+
# Grayscale formats
|
|
189
|
+
"GRAY8": (np.uint8, 1, 1),
|
|
190
|
+
"GRAY16_LE": (np.uint16, 1, 2),
|
|
191
|
+
"GRAY16_BE": (np.uint16, 1, 2),
|
|
192
|
+
# YUV planar formats (Y plane only for simplicity)
|
|
193
|
+
"I420": (np.uint8, 1, 1),
|
|
194
|
+
"YV12": (np.uint8, 1, 1),
|
|
195
|
+
"NV12": (np.uint8, 1, 1),
|
|
196
|
+
"NV21": (np.uint8, 1, 1),
|
|
197
|
+
# YUV packed formats
|
|
198
|
+
"YUY2": (np.uint8, 2, 2),
|
|
199
|
+
"UYVY": (np.uint8, 2, 2),
|
|
200
|
+
"YVYU": (np.uint8, 2, 2),
|
|
201
|
+
# Bayer formats (raw sensor data)
|
|
202
|
+
"BGGR": (np.uint8, 1, 1),
|
|
203
|
+
"RGGB": (np.uint8, 1, 1),
|
|
204
|
+
"GRBG": (np.uint8, 1, 1),
|
|
205
|
+
"GBRG": (np.uint8, 1, 1),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# Default to RGB if unknown
|
|
209
|
+
return format_map.get(format_upper, (np.uint8, 3, 3))
|
|
210
|
+
|
|
211
|
+
def create_array(
|
|
212
|
+
self, data: bytes | memoryview | npt.NDArray[np.uint8] | npt.NDArray[np.uint16]
|
|
213
|
+
) -> npt.NDArray[np.uint8] | npt.NDArray[np.uint16]:
|
|
214
|
+
"""
|
|
215
|
+
Create numpy array with proper format from data.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
data: Frame data as bytes, memoryview, or existing numpy array
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Numpy array with proper shape and dtype
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
ValueError: If data size doesn't match expected frame size
|
|
225
|
+
"""
|
|
226
|
+
# Convert memoryview to bytes if needed
|
|
227
|
+
if isinstance(data, memoryview):
|
|
228
|
+
data = bytes(data)
|
|
229
|
+
|
|
230
|
+
# If it's already a numpy array, check size and reshape if needed
|
|
231
|
+
if isinstance(data, np.ndarray):
|
|
232
|
+
if data.size * data.itemsize != self.frame_size:
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f"Data size mismatch. Expected {self.frame_size} bytes for "
|
|
235
|
+
f"{self.width}x{self.height} {self.format}, got {data.size * data.itemsize}"
|
|
236
|
+
)
|
|
237
|
+
# Reshape if needed
|
|
238
|
+
if self.channels == 1:
|
|
239
|
+
return data.reshape((self.height, self.width))
|
|
240
|
+
else:
|
|
241
|
+
return data.reshape((self.height, self.width, self.channels))
|
|
242
|
+
|
|
243
|
+
# Check data size
|
|
244
|
+
if len(data) != self.frame_size:
|
|
245
|
+
raise ValueError(
|
|
246
|
+
f"Data size mismatch. Expected {self.frame_size} bytes for "
|
|
247
|
+
f"{self.width}x{self.height} {self.format}, got {len(data)}"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Create array from bytes
|
|
251
|
+
arr = np.frombuffer(data, dtype=self.depth_type)
|
|
252
|
+
|
|
253
|
+
# Reshape based on channels
|
|
254
|
+
if self.channels == 1:
|
|
255
|
+
return arr.reshape((self.height, self.width))
|
|
256
|
+
else:
|
|
257
|
+
# For multi-channel images, reshape to (height, width, channels)
|
|
258
|
+
total_pixels = self.width * self.height * self.channels
|
|
259
|
+
if self.depth_type == np.uint16:
|
|
260
|
+
# For 16-bit formats, we need to account for the item size
|
|
261
|
+
arr = arr[:total_pixels]
|
|
262
|
+
return arr.reshape((self.height, self.width, self.channels))
|
|
263
|
+
|
|
264
|
+
def create_array_from_pointer(
|
|
265
|
+
self, ptr: int, copy: bool = False
|
|
266
|
+
) -> npt.NDArray[np.uint8] | npt.NDArray[np.uint16]:
|
|
267
|
+
"""
|
|
268
|
+
Create numpy array from memory pointer (zero-copy by default).
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
ptr: Memory pointer as integer
|
|
272
|
+
copy: If True, make a copy of the data; if False, create a view
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Numpy array with proper shape and dtype
|
|
276
|
+
"""
|
|
277
|
+
# Calculate total elements based on depth type
|
|
278
|
+
if self.depth_type == np.uint16:
|
|
279
|
+
total_elements = self.width * self.height * self.channels
|
|
280
|
+
else:
|
|
281
|
+
total_elements = self.frame_size
|
|
282
|
+
|
|
283
|
+
# Create array from pointer using ctypes
|
|
284
|
+
import ctypes
|
|
285
|
+
|
|
286
|
+
# Create a buffer from the pointer
|
|
287
|
+
buffer_size = total_elements * self.depth_type.itemsize
|
|
288
|
+
c_buffer = (ctypes.c_byte * buffer_size).from_address(ptr)
|
|
289
|
+
arr = np.frombuffer(c_buffer, dtype=self.depth_type)
|
|
290
|
+
|
|
291
|
+
# Reshape based on channels
|
|
292
|
+
if self.channels == 1:
|
|
293
|
+
shaped = arr.reshape((self.height, self.width))
|
|
294
|
+
else:
|
|
295
|
+
shaped = arr.reshape((self.height, self.width, self.channels))
|
|
296
|
+
|
|
297
|
+
return shaped.copy() if copy else shaped
|
|
298
|
+
|
|
299
|
+
def __str__(self) -> str:
|
|
300
|
+
"""String representation."""
|
|
301
|
+
# If we have the original caps string, return it for perfect round-tripping
|
|
302
|
+
if self.caps_string:
|
|
303
|
+
return self.caps_string
|
|
304
|
+
|
|
305
|
+
# Otherwise build a simple display string
|
|
306
|
+
fps = f" @ {self.framerate:.2f}fps" if self.framerate else ""
|
|
307
|
+
return f"{self.width}x{self.height} {self.format}{fps}"
|
|
308
|
+
|
|
309
|
+
def to_dict(self) -> dict[str, Any]:
|
|
310
|
+
"""Convert to dictionary representation."""
|
|
311
|
+
result = {
|
|
312
|
+
"width": self.width,
|
|
313
|
+
"height": self.height,
|
|
314
|
+
"format": self.format,
|
|
315
|
+
"channels": self.channels,
|
|
316
|
+
"bytes_per_pixel": self.bytes_per_pixel,
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if self.framerate_num is not None:
|
|
320
|
+
result["framerate_num"] = self.framerate_num
|
|
321
|
+
if self.framerate_den is not None:
|
|
322
|
+
result["framerate_den"] = self.framerate_den
|
|
323
|
+
if self.interlace_mode:
|
|
324
|
+
result["interlace_mode"] = self.interlace_mode
|
|
325
|
+
if self.colorimetry:
|
|
326
|
+
result["colorimetry"] = self.colorimetry
|
|
327
|
+
|
|
328
|
+
return result
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@dataclass
|
|
332
|
+
class GstMetadata:
|
|
333
|
+
"""
|
|
334
|
+
GStreamer metadata structure.
|
|
335
|
+
|
|
336
|
+
Matches the JSON structure written by GStreamer plugins.
|
|
337
|
+
Compatible with C# GstMetadata record.
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
type: str
|
|
341
|
+
version: str
|
|
342
|
+
caps: GstCaps
|
|
343
|
+
element_name: str
|
|
344
|
+
|
|
345
|
+
@classmethod
|
|
346
|
+
def from_json(cls, json_data: str | bytes | dict[str, Any]) -> GstMetadata:
|
|
347
|
+
"""
|
|
348
|
+
Create GstMetadata from JSON data.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
json_data: JSON string, bytes, or dictionary
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
GstMetadata instance
|
|
355
|
+
|
|
356
|
+
Raises:
|
|
357
|
+
ValueError: If JSON is invalid or missing required fields
|
|
358
|
+
"""
|
|
359
|
+
# Parse JSON if needed
|
|
360
|
+
if isinstance(json_data, (str, bytes)):
|
|
361
|
+
if isinstance(json_data, bytes):
|
|
362
|
+
json_data = json_data.decode("utf-8")
|
|
363
|
+
try:
|
|
364
|
+
data = json.loads(json_data)
|
|
365
|
+
except json.JSONDecodeError as e:
|
|
366
|
+
raise ValueError(f"Invalid JSON: {e}") from e
|
|
367
|
+
else:
|
|
368
|
+
data = json_data
|
|
369
|
+
|
|
370
|
+
# Validate required fields
|
|
371
|
+
if not isinstance(data, dict):
|
|
372
|
+
raise ValueError("JSON must be an object/dictionary")
|
|
373
|
+
|
|
374
|
+
# Get required fields
|
|
375
|
+
type_str = data.get("type", "")
|
|
376
|
+
version = data.get("version", "")
|
|
377
|
+
element_name = data.get("element_name", "")
|
|
378
|
+
|
|
379
|
+
# Parse caps - it's a STRING in the JSON!
|
|
380
|
+
caps_data = data.get("caps")
|
|
381
|
+
if isinstance(caps_data, str):
|
|
382
|
+
# This is the normal case - caps is a string that needs parsing
|
|
383
|
+
caps = GstCaps.parse(caps_data)
|
|
384
|
+
elif isinstance(caps_data, dict):
|
|
385
|
+
# Fallback for dict format (shouldn't happen with real GStreamer)
|
|
386
|
+
# Create a simple caps from dict
|
|
387
|
+
width = caps_data.get("width", 640)
|
|
388
|
+
height = caps_data.get("height", 480)
|
|
389
|
+
format_str = caps_data.get("format", "RGB")
|
|
390
|
+
caps = GstCaps.from_simple(width, height, format_str)
|
|
391
|
+
else:
|
|
392
|
+
raise ValueError(f"Invalid caps data type: {type(caps_data)}")
|
|
393
|
+
|
|
394
|
+
return cls(type=type_str, version=version, caps=caps, element_name=element_name)
|
|
395
|
+
|
|
396
|
+
def to_json(self) -> str:
|
|
397
|
+
"""Convert to JSON string."""
|
|
398
|
+
return json.dumps(self.to_dict())
|
|
399
|
+
|
|
400
|
+
def to_dict(self) -> dict[str, Any]:
|
|
401
|
+
"""Convert to dictionary representation."""
|
|
402
|
+
return {
|
|
403
|
+
"type": self.type,
|
|
404
|
+
"version": self.version,
|
|
405
|
+
"caps": str(self.caps), # Caps as string for C# compatibility
|
|
406
|
+
"element_name": self.element_name,
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
def __str__(self) -> str:
|
|
410
|
+
"""String representation."""
|
|
411
|
+
return f"GstMetadata(type={self.type}, element={self.element_name}, caps={self.caps})"
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enterprise-grade RocketWelder client for video streaming.
|
|
3
|
+
Main entry point for the RocketWelder SDK.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import threading
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from .connection_string import ConnectionMode, ConnectionString, Protocol
|
|
15
|
+
from .controllers import DuplexShmController, IController, OneWayShmController
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .gst_metadata import GstMetadata
|
|
19
|
+
|
|
20
|
+
# Type alias for OpenCV Mat
|
|
21
|
+
Mat = np.ndarray[Any, Any]
|
|
22
|
+
|
|
23
|
+
# Module logger
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RocketWelderClient:
|
|
28
|
+
"""
|
|
29
|
+
Main client for RocketWelder video streaming services.
|
|
30
|
+
|
|
31
|
+
Provides a unified interface for different connection types and protocols.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, connection: str | ConnectionString):
|
|
35
|
+
"""
|
|
36
|
+
Initialize the RocketWelder client.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
connection: Connection string or ConnectionString object
|
|
40
|
+
"""
|
|
41
|
+
if isinstance(connection, str):
|
|
42
|
+
self._connection = ConnectionString.parse(connection)
|
|
43
|
+
else:
|
|
44
|
+
self._connection = connection
|
|
45
|
+
|
|
46
|
+
self._controller: IController | None = None
|
|
47
|
+
self._lock = threading.Lock()
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def connection(self) -> ConnectionString:
|
|
51
|
+
"""Get the connection configuration."""
|
|
52
|
+
return self._connection
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def is_running(self) -> bool:
|
|
56
|
+
"""Check if the client is running."""
|
|
57
|
+
with self._lock:
|
|
58
|
+
return self._controller is not None and self._controller.is_running
|
|
59
|
+
|
|
60
|
+
def get_metadata(self) -> GstMetadata | None:
|
|
61
|
+
"""
|
|
62
|
+
Get the current GStreamer metadata.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
GstMetadata or None if not available
|
|
66
|
+
"""
|
|
67
|
+
with self._lock:
|
|
68
|
+
if self._controller:
|
|
69
|
+
return self._controller.get_metadata()
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def start(
|
|
73
|
+
self,
|
|
74
|
+
on_frame: Callable[[Mat], None] | Callable[[Mat, Mat], None],
|
|
75
|
+
cancellation_token: threading.Event | None = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Start receiving/processing video frames.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
on_frame: Callback for frame processing.
|
|
82
|
+
For one-way: (input_frame) -> None
|
|
83
|
+
For duplex: (input_frame, output_frame) -> None
|
|
84
|
+
cancellation_token: Optional cancellation token
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
RuntimeError: If already running
|
|
88
|
+
ValueError: If connection type is not supported
|
|
89
|
+
"""
|
|
90
|
+
with self._lock:
|
|
91
|
+
if self._controller and self._controller.is_running:
|
|
92
|
+
raise RuntimeError("Client is already running")
|
|
93
|
+
|
|
94
|
+
# Create appropriate controller based on connection
|
|
95
|
+
if self._connection.protocol == Protocol.SHM:
|
|
96
|
+
if self._connection.connection_mode == ConnectionMode.DUPLEX:
|
|
97
|
+
self._controller = DuplexShmController(self._connection)
|
|
98
|
+
else:
|
|
99
|
+
self._controller = OneWayShmController(self._connection)
|
|
100
|
+
else:
|
|
101
|
+
raise ValueError(f"Unsupported protocol: {self._connection.protocol}")
|
|
102
|
+
|
|
103
|
+
# Start the controller
|
|
104
|
+
self._controller.start(on_frame, cancellation_token) # type: ignore[arg-type]
|
|
105
|
+
logger.info("RocketWelder client started with %s", self._connection)
|
|
106
|
+
|
|
107
|
+
def stop(self) -> None:
|
|
108
|
+
"""Stop the client and clean up resources."""
|
|
109
|
+
with self._lock:
|
|
110
|
+
if self._controller:
|
|
111
|
+
self._controller.stop()
|
|
112
|
+
self._controller = None
|
|
113
|
+
logger.info("RocketWelder client stopped")
|
|
114
|
+
|
|
115
|
+
def __enter__(self) -> RocketWelderClient:
|
|
116
|
+
"""Context manager entry."""
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
120
|
+
"""Context manager exit."""
|
|
121
|
+
self.stop()
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def create_oneway_shm(
|
|
125
|
+
cls,
|
|
126
|
+
buffer_name: str,
|
|
127
|
+
buffer_size: str = "256MB",
|
|
128
|
+
metadata_size: str = "4KB",
|
|
129
|
+
) -> RocketWelderClient:
|
|
130
|
+
"""
|
|
131
|
+
Create a one-way shared memory client.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
buffer_name: Name of the shared memory buffer
|
|
135
|
+
buffer_size: Size of the buffer (e.g., "256MB")
|
|
136
|
+
metadata_size: Size of metadata buffer (e.g., "4KB")
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Configured RocketWelderClient instance
|
|
140
|
+
"""
|
|
141
|
+
connection_str = (
|
|
142
|
+
f"shm://{buffer_name}?size={buffer_size}&metadata={metadata_size}&mode=OneWay"
|
|
143
|
+
)
|
|
144
|
+
return cls(connection_str)
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def create_duplex_shm(
|
|
148
|
+
cls,
|
|
149
|
+
buffer_name: str,
|
|
150
|
+
buffer_size: str = "256MB",
|
|
151
|
+
metadata_size: str = "4KB",
|
|
152
|
+
) -> RocketWelderClient:
|
|
153
|
+
"""
|
|
154
|
+
Create a duplex shared memory client.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
buffer_name: Name of the shared memory buffer
|
|
158
|
+
buffer_size: Size of the buffer (e.g., "256MB")
|
|
159
|
+
metadata_size: Size of metadata buffer (e.g., "4KB")
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Configured RocketWelderClient instance
|
|
163
|
+
"""
|
|
164
|
+
connection_str = (
|
|
165
|
+
f"shm://{buffer_name}?size={buffer_size}&metadata={metadata_size}&mode=Duplex"
|
|
166
|
+
)
|
|
167
|
+
return cls(connection_str)
|