rocket-welder-sdk 1.0.4__py3-none-any.whl → 1.1.0__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 +30 -4
- rocket_welder_sdk/bytes_size.py +234 -0
- rocket_welder_sdk/connection_string.py +232 -0
- rocket_welder_sdk/controllers.py +609 -0
- rocket_welder_sdk/gst_metadata.py +240 -0
- rocket_welder_sdk/py.typed +2 -0
- rocket_welder_sdk/rocket_welder_client.py +170 -0
- rocket_welder_sdk-1.1.0.dist-info/METADATA +496 -0
- rocket_welder_sdk-1.1.0.dist-info/RECORD +11 -0
- rocket_welder_sdk/client.py +0 -183
- rocket_welder_sdk/rocket_welder_sdk/__init__.py +0 -20
- rocket_welder_sdk/rocket_welder_sdk/client.py +0 -326
- rocket_welder_sdk/rocket_welder_sdk/connection_string.py +0 -190
- rocket_welder_sdk/rocket_welder_sdk/exceptions.py +0 -23
- rocket_welder_sdk/rocket_welder_sdk/gst_caps.py +0 -224
- rocket_welder_sdk/rocket_welder_sdk/gst_metadata.py +0 -43
- rocket_welder_sdk-1.0.4.dist-info/METADATA +0 -36
- rocket_welder_sdk-1.0.4.dist-info/RECORD +0 -12
- {rocket_welder_sdk-1.0.4.dist-info → rocket_welder_sdk-1.1.0.dist-info}/WHEEL +0 -0
- {rocket_welder_sdk-1.0.4.dist-info → rocket_welder_sdk-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
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
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class GstCaps:
|
|
15
|
+
"""
|
|
16
|
+
GStreamer capabilities representation.
|
|
17
|
+
|
|
18
|
+
Represents video format capabilities including format, dimensions, framerate, etc.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
format: str | None = None
|
|
22
|
+
width: int | None = None
|
|
23
|
+
height: int | None = None
|
|
24
|
+
framerate: str | None = None # e.g., "30/1" or "25/1"
|
|
25
|
+
pixel_aspect_ratio: str | None = None # e.g., "1/1"
|
|
26
|
+
interlace_mode: str | None = None # e.g., "progressive"
|
|
27
|
+
colorimetry: str | None = None
|
|
28
|
+
chroma_site: str | None = None
|
|
29
|
+
|
|
30
|
+
# Additional fields can be stored here
|
|
31
|
+
extra_fields: dict[str, Any] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_dict(cls, data: dict[str, Any]) -> GstCaps:
|
|
35
|
+
"""
|
|
36
|
+
Create GstCaps from dictionary.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
data: Dictionary containing caps data
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
GstCaps instance
|
|
43
|
+
"""
|
|
44
|
+
# Extract known fields
|
|
45
|
+
caps = cls(
|
|
46
|
+
format=data.get("format"),
|
|
47
|
+
width=data.get("width"),
|
|
48
|
+
height=data.get("height"),
|
|
49
|
+
framerate=data.get("framerate"),
|
|
50
|
+
pixel_aspect_ratio=data.get("pixel-aspect-ratio"),
|
|
51
|
+
interlace_mode=data.get("interlace-mode"),
|
|
52
|
+
colorimetry=data.get("colorimetry"),
|
|
53
|
+
chroma_site=data.get("chroma-site"),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Store any extra fields
|
|
57
|
+
known_fields = {
|
|
58
|
+
"format",
|
|
59
|
+
"width",
|
|
60
|
+
"height",
|
|
61
|
+
"framerate",
|
|
62
|
+
"pixel-aspect-ratio",
|
|
63
|
+
"interlace-mode",
|
|
64
|
+
"colorimetry",
|
|
65
|
+
"chroma-site",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for key, value in data.items():
|
|
69
|
+
if key not in known_fields:
|
|
70
|
+
caps.extra_fields[key] = value
|
|
71
|
+
|
|
72
|
+
return caps
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_string(cls, caps_string: str) -> GstCaps:
|
|
76
|
+
"""
|
|
77
|
+
Parse GStreamer caps string.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
caps_string: GStreamer caps string (e.g., "video/x-raw,format=RGB,width=640,height=480")
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
GstCaps instance
|
|
84
|
+
"""
|
|
85
|
+
if not caps_string:
|
|
86
|
+
return cls()
|
|
87
|
+
|
|
88
|
+
# Remove media type prefix if present
|
|
89
|
+
if "/" in caps_string and "," in caps_string:
|
|
90
|
+
# Has media type prefix (e.g., "video/x-raw,format=RGB")
|
|
91
|
+
_, params = caps_string.split(",", 1)
|
|
92
|
+
else:
|
|
93
|
+
# No media type prefix (e.g., "format=RGB,width=320")
|
|
94
|
+
params = caps_string
|
|
95
|
+
|
|
96
|
+
# Parse parameters
|
|
97
|
+
data: dict[str, Any] = {}
|
|
98
|
+
for param in params.split(","):
|
|
99
|
+
if "=" in param:
|
|
100
|
+
key, value = param.split("=", 1)
|
|
101
|
+
key = key.strip()
|
|
102
|
+
value_str = value.strip()
|
|
103
|
+
|
|
104
|
+
# Try to parse numeric values
|
|
105
|
+
if value_str.isdigit():
|
|
106
|
+
data[key] = int(value_str)
|
|
107
|
+
elif key in ["width", "height"] and value_str.startswith("(int)"):
|
|
108
|
+
data[key] = int(value_str[5:].strip())
|
|
109
|
+
else:
|
|
110
|
+
data[key] = value_str
|
|
111
|
+
|
|
112
|
+
return cls.from_dict(data)
|
|
113
|
+
|
|
114
|
+
def to_dict(self) -> dict[str, Any]:
|
|
115
|
+
"""Convert to dictionary representation."""
|
|
116
|
+
result: dict[str, Any] = {}
|
|
117
|
+
|
|
118
|
+
if self.format is not None:
|
|
119
|
+
result["format"] = self.format
|
|
120
|
+
if self.width is not None:
|
|
121
|
+
result["width"] = self.width
|
|
122
|
+
if self.height is not None:
|
|
123
|
+
result["height"] = self.height
|
|
124
|
+
if self.framerate is not None:
|
|
125
|
+
result["framerate"] = self.framerate
|
|
126
|
+
if self.pixel_aspect_ratio is not None:
|
|
127
|
+
result["pixel-aspect-ratio"] = self.pixel_aspect_ratio
|
|
128
|
+
if self.interlace_mode is not None:
|
|
129
|
+
result["interlace-mode"] = self.interlace_mode
|
|
130
|
+
if self.colorimetry is not None:
|
|
131
|
+
result["colorimetry"] = self.colorimetry
|
|
132
|
+
if self.chroma_site is not None:
|
|
133
|
+
result["chroma-site"] = self.chroma_site
|
|
134
|
+
|
|
135
|
+
# Add extra fields
|
|
136
|
+
result.update(self.extra_fields)
|
|
137
|
+
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
def to_string(self) -> str:
|
|
141
|
+
"""Convert to GStreamer caps string format."""
|
|
142
|
+
params = []
|
|
143
|
+
|
|
144
|
+
if self.format:
|
|
145
|
+
params.append(f"format={self.format}")
|
|
146
|
+
if self.width is not None:
|
|
147
|
+
params.append(f"width={self.width}")
|
|
148
|
+
if self.height is not None:
|
|
149
|
+
params.append(f"height={self.height}")
|
|
150
|
+
if self.framerate:
|
|
151
|
+
params.append(f"framerate={self.framerate}")
|
|
152
|
+
if self.pixel_aspect_ratio:
|
|
153
|
+
params.append(f"pixel-aspect-ratio={self.pixel_aspect_ratio}")
|
|
154
|
+
if self.interlace_mode:
|
|
155
|
+
params.append(f"interlace-mode={self.interlace_mode}")
|
|
156
|
+
|
|
157
|
+
# Add extra fields
|
|
158
|
+
for key, value in self.extra_fields.items():
|
|
159
|
+
params.append(f"{key}={value}")
|
|
160
|
+
|
|
161
|
+
if params:
|
|
162
|
+
return "video/x-raw," + ",".join(params)
|
|
163
|
+
else:
|
|
164
|
+
return "video/x-raw"
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def framerate_tuple(self) -> tuple[int, int] | None:
|
|
168
|
+
"""Get framerate as a tuple of (numerator, denominator)."""
|
|
169
|
+
if not self.framerate or "/" not in self.framerate:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
num, denom = self.framerate.split("/")
|
|
174
|
+
return (int(num), int(denom))
|
|
175
|
+
except (ValueError, AttributeError):
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def fps(self) -> float | None:
|
|
180
|
+
"""Get framerate as floating-point FPS value."""
|
|
181
|
+
fr = self.framerate_tuple
|
|
182
|
+
if fr and fr[1] != 0:
|
|
183
|
+
return fr[0] / fr[1]
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class GstMetadata:
|
|
189
|
+
"""
|
|
190
|
+
GStreamer metadata structure.
|
|
191
|
+
|
|
192
|
+
Matches the JSON structure written by GStreamer plugins.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
type: str
|
|
196
|
+
version: str
|
|
197
|
+
caps: GstCaps
|
|
198
|
+
element_name: str
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def from_json(cls, json_data: str | bytes | dict[str, Any]) -> GstMetadata:
|
|
202
|
+
"""
|
|
203
|
+
Create GstMetadata from JSON data.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
json_data: JSON string, bytes, or dictionary
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
GstMetadata instance
|
|
210
|
+
"""
|
|
211
|
+
data = json.loads(json_data) if isinstance(json_data, (str, bytes)) else json_data
|
|
212
|
+
|
|
213
|
+
# Parse caps
|
|
214
|
+
caps_data = data.get("caps", {})
|
|
215
|
+
if isinstance(caps_data, str):
|
|
216
|
+
caps = GstCaps.from_string(caps_data)
|
|
217
|
+
elif isinstance(caps_data, dict):
|
|
218
|
+
caps = GstCaps.from_dict(caps_data)
|
|
219
|
+
else:
|
|
220
|
+
caps = GstCaps()
|
|
221
|
+
|
|
222
|
+
return cls(
|
|
223
|
+
type=data.get("type", ""),
|
|
224
|
+
version=data.get("version", ""),
|
|
225
|
+
caps=caps,
|
|
226
|
+
element_name=data.get("element_name", ""),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def to_json(self) -> str:
|
|
230
|
+
"""Convert to JSON string."""
|
|
231
|
+
return json.dumps(self.to_dict())
|
|
232
|
+
|
|
233
|
+
def to_dict(self) -> dict[str, Any]:
|
|
234
|
+
"""Convert to dictionary representation."""
|
|
235
|
+
return {
|
|
236
|
+
"type": self.type,
|
|
237
|
+
"version": self.version,
|
|
238
|
+
"caps": self.caps.to_dict(),
|
|
239
|
+
"element_name": self.element_name,
|
|
240
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
|
|
24
|
+
class RocketWelderClient:
|
|
25
|
+
"""
|
|
26
|
+
Main client for RocketWelder video streaming services.
|
|
27
|
+
|
|
28
|
+
Provides a unified interface for different connection types and protocols.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, connection: str | ConnectionString, logger: logging.Logger | None = None):
|
|
32
|
+
"""
|
|
33
|
+
Initialize the RocketWelder client.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
connection: Connection string or ConnectionString object
|
|
37
|
+
logger: Optional logger instance
|
|
38
|
+
"""
|
|
39
|
+
if isinstance(connection, str):
|
|
40
|
+
self._connection = ConnectionString.parse(connection)
|
|
41
|
+
else:
|
|
42
|
+
self._connection = connection
|
|
43
|
+
|
|
44
|
+
self._logger = logger or logging.getLogger(__name__)
|
|
45
|
+
self._controller: IController | None = None
|
|
46
|
+
self._lock = threading.Lock()
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def connection(self) -> ConnectionString:
|
|
50
|
+
"""Get the connection configuration."""
|
|
51
|
+
return self._connection
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_running(self) -> bool:
|
|
55
|
+
"""Check if the client is running."""
|
|
56
|
+
with self._lock:
|
|
57
|
+
return self._controller is not None and self._controller.is_running
|
|
58
|
+
|
|
59
|
+
def get_metadata(self) -> GstMetadata | None:
|
|
60
|
+
"""
|
|
61
|
+
Get the current GStreamer metadata.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
GstMetadata or None if not available
|
|
65
|
+
"""
|
|
66
|
+
with self._lock:
|
|
67
|
+
if self._controller:
|
|
68
|
+
return self._controller.get_metadata()
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
def start(
|
|
72
|
+
self,
|
|
73
|
+
on_frame: Callable[[Mat], None] | Callable[[Mat, Mat], None],
|
|
74
|
+
cancellation_token: threading.Event | None = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Start receiving/processing video frames.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
on_frame: Callback for frame processing.
|
|
81
|
+
For one-way: (input_frame) -> None
|
|
82
|
+
For duplex: (input_frame, output_frame) -> None
|
|
83
|
+
cancellation_token: Optional cancellation token
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
RuntimeError: If already running
|
|
87
|
+
ValueError: If connection type is not supported
|
|
88
|
+
"""
|
|
89
|
+
with self._lock:
|
|
90
|
+
if self._controller and self._controller.is_running:
|
|
91
|
+
raise RuntimeError("Client is already running")
|
|
92
|
+
|
|
93
|
+
# Create appropriate controller based on connection
|
|
94
|
+
if self._connection.protocol == Protocol.SHM:
|
|
95
|
+
if self._connection.connection_mode == ConnectionMode.DUPLEX:
|
|
96
|
+
self._controller = DuplexShmController(self._connection, self._logger)
|
|
97
|
+
else:
|
|
98
|
+
self._controller = OneWayShmController(self._connection, self._logger)
|
|
99
|
+
else:
|
|
100
|
+
raise ValueError(f"Unsupported protocol: {self._connection.protocol}")
|
|
101
|
+
|
|
102
|
+
# Start the controller
|
|
103
|
+
self._controller.start(on_frame, cancellation_token) # type: ignore[arg-type]
|
|
104
|
+
self._logger.info("RocketWelder client started with %s", self._connection)
|
|
105
|
+
|
|
106
|
+
def stop(self) -> None:
|
|
107
|
+
"""Stop the client and clean up resources."""
|
|
108
|
+
with self._lock:
|
|
109
|
+
if self._controller:
|
|
110
|
+
self._controller.stop()
|
|
111
|
+
self._controller = None
|
|
112
|
+
self._logger.info("RocketWelder client stopped")
|
|
113
|
+
|
|
114
|
+
def __enter__(self) -> RocketWelderClient:
|
|
115
|
+
"""Context manager entry."""
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
119
|
+
"""Context manager exit."""
|
|
120
|
+
self.stop()
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def create_oneway_shm(
|
|
124
|
+
cls,
|
|
125
|
+
buffer_name: str,
|
|
126
|
+
buffer_size: str = "256MB",
|
|
127
|
+
metadata_size: str = "4KB",
|
|
128
|
+
logger: logging.Logger | None = None,
|
|
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
|
+
logger: Optional logger
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Configured RocketWelderClient instance
|
|
141
|
+
"""
|
|
142
|
+
connection_str = (
|
|
143
|
+
f"shm://{buffer_name}?size={buffer_size}&metadata={metadata_size}&mode=OneWay"
|
|
144
|
+
)
|
|
145
|
+
return cls(connection_str, logger)
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def create_duplex_shm(
|
|
149
|
+
cls,
|
|
150
|
+
buffer_name: str,
|
|
151
|
+
buffer_size: str = "256MB",
|
|
152
|
+
metadata_size: str = "4KB",
|
|
153
|
+
logger: logging.Logger | None = None,
|
|
154
|
+
) -> RocketWelderClient:
|
|
155
|
+
"""
|
|
156
|
+
Create a duplex shared memory client.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
buffer_name: Name of the shared memory buffer
|
|
160
|
+
buffer_size: Size of the buffer (e.g., "256MB")
|
|
161
|
+
metadata_size: Size of metadata buffer (e.g., "4KB")
|
|
162
|
+
logger: Optional logger
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Configured RocketWelderClient instance
|
|
166
|
+
"""
|
|
167
|
+
connection_str = (
|
|
168
|
+
f"shm://{buffer_name}?size={buffer_size}&metadata={metadata_size}&mode=Duplex"
|
|
169
|
+
)
|
|
170
|
+
return cls(connection_str, logger)
|