rocket-welder-sdk 1.0.5__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.
@@ -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,2 @@
1
+ # Marker file for PEP 561
2
+ # This package contains inline type hints
@@ -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)