rocket-welder-sdk 1.0.2__tar.gz → 1.0.4__tar.gz

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.
Files changed (19) hide show
  1. rocket_welder_sdk-1.0.4/MANIFEST.in +6 -0
  2. {rocket_welder_sdk-1.0.2 → rocket_welder_sdk-1.0.4}/PKG-INFO +89 -10
  3. rocket_welder_sdk-1.0.4/logo.png +0 -0
  4. rocket_welder_sdk-1.0.4/rocket_welder_sdk/rocket_welder_sdk/__init__.py +20 -0
  5. rocket_welder_sdk-1.0.4/rocket_welder_sdk/rocket_welder_sdk/client.py +326 -0
  6. rocket_welder_sdk-1.0.4/rocket_welder_sdk/rocket_welder_sdk/connection_string.py +190 -0
  7. rocket_welder_sdk-1.0.4/rocket_welder_sdk/rocket_welder_sdk/exceptions.py +23 -0
  8. rocket_welder_sdk-1.0.4/rocket_welder_sdk/rocket_welder_sdk/gst_caps.py +224 -0
  9. rocket_welder_sdk-1.0.4/rocket_welder_sdk/rocket_welder_sdk/gst_metadata.py +43 -0
  10. {rocket_welder_sdk-1.0.2 → rocket_welder_sdk-1.0.4}/rocket_welder_sdk.egg-info/PKG-INFO +89 -10
  11. rocket_welder_sdk-1.0.4/rocket_welder_sdk.egg-info/SOURCES.txt +17 -0
  12. {rocket_welder_sdk-1.0.2 → rocket_welder_sdk-1.0.4}/rocket_welder_sdk.egg-info/requires.txt +1 -1
  13. {rocket_welder_sdk-1.0.2 → rocket_welder_sdk-1.0.4}/setup.py +4 -4
  14. rocket_welder_sdk-1.0.2/rocket_welder_sdk.egg-info/SOURCES.txt +0 -8
  15. {rocket_welder_sdk-1.0.2 → rocket_welder_sdk-1.0.4}/rocket_welder_sdk/__init__.py +0 -0
  16. {rocket_welder_sdk-1.0.2 → rocket_welder_sdk-1.0.4}/rocket_welder_sdk/client.py +0 -0
  17. {rocket_welder_sdk-1.0.2 → rocket_welder_sdk-1.0.4}/rocket_welder_sdk.egg-info/dependency_links.txt +0 -0
  18. {rocket_welder_sdk-1.0.2 → rocket_welder_sdk-1.0.4}/rocket_welder_sdk.egg-info/top_level.txt +0 -0
  19. {rocket_welder_sdk-1.0.2 → rocket_welder_sdk-1.0.4}/setup.cfg +0 -0
@@ -0,0 +1,6 @@
1
+ include README.md
2
+ include ../README.md
3
+ include logo.png
4
+ include LICENSE
5
+ include ../LICENSE
6
+ recursive-include rocket_welder_sdk *.py
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rocket-welder-sdk
3
- Version: 1.0.2
4
- Summary: Client library for RocketWelder video streaming services
3
+ Version: 1.0.4
4
+ Summary: High-performance video streaming client library for RocketWelder services
5
5
  Home-page: https://github.com/modelingevolution/rocket-welder-sdk
6
- Author: RocketWelder
6
+ Author: ModelingEvolution
7
7
  Classifier: Development Status :: 3 - Alpha
8
8
  Classifier: Intended Audience :: Developers
9
9
  Classifier: Topic :: Multimedia :: Video
@@ -18,7 +18,7 @@ Requires-Python: >=3.8
18
18
  Description-Content-Type: text/markdown
19
19
  Requires-Dist: numpy>=1.20.0
20
20
  Requires-Dist: opencv-python>=4.5.0
21
- Requires-Dist: zerobuffer-ipc>=1.0.0
21
+ Requires-Dist: zerobuffer-ipc>=1.1.0
22
22
  Provides-Extra: dev
23
23
  Requires-Dist: pytest>=7.0; extra == "dev"
24
24
  Requires-Dist: black>=22.0; extra == "dev"
@@ -35,6 +35,11 @@ Dynamic: summary
35
35
 
36
36
  # Rocket Welder SDK
37
37
 
38
+ [![NuGet](https://img.shields.io/nuget/v/RocketWelder.SDK.svg)](https://www.nuget.org/packages/RocketWelder.SDK/)
39
+ [![PyPI](https://img.shields.io/pypi/v/rocket-welder-sdk.svg)](https://pypi.org/project/rocket-welder-sdk/)
40
+ [![vcpkg](https://img.shields.io/badge/vcpkg-rocket--welder--sdk-blue)](https://github.com/modelingevolution/rocket-welder-sdk-vcpkg-registry)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
42
+
38
43
  Multi-language client libraries for interacting with RocketWelder video streaming services.
39
44
 
40
45
  ## Overview
@@ -100,22 +105,98 @@ CONNECTION_STRING=shm://camera_feed?buffer_size=20MB&metadata_size=4KB
100
105
 
101
106
  ## Installation
102
107
 
103
- ### C++ (vcpkg)
108
+ ### C++ with vcpkg
104
109
 
110
+ Configure the custom registry in your `vcpkg-configuration.json`:
111
+ ```json
112
+ {
113
+ "registries": [
114
+ {
115
+ "kind": "git",
116
+ "repository": "https://github.com/modelingevolution/rocket-welder-sdk-vcpkg-registry",
117
+ "baseline": "YOUR_BASELINE_HERE",
118
+ "packages": ["rocket-welder-sdk"]
119
+ }
120
+ ]
121
+ }
122
+ ```
123
+
124
+ Then install:
105
125
  ```bash
126
+ # Install via vcpkg
106
127
  vcpkg install rocket-welder-sdk
128
+
129
+ # Or integrate with CMake
130
+ find_package(rocket-welder-sdk CONFIG REQUIRED)
131
+ target_link_libraries(your_app PRIVATE rocket-welder-sdk::rocket-welder-sdk)
107
132
  ```
108
133
 
109
- ### C# (NuGet)
134
+ ### C# with NuGet
135
+
136
+ [![NuGet Downloads](https://img.shields.io/nuget/dt/RocketWelder.SDK.svg)](https://www.nuget.org/packages/RocketWelder.SDK/)
110
137
 
111
138
  ```bash
139
+ # Package Manager Console
140
+ Install-Package RocketWelder.SDK
141
+
142
+ # .NET CLI
112
143
  dotnet add package RocketWelder.SDK
144
+
145
+ # PackageReference in .csproj
146
+ <PackageReference Include="RocketWelder.SDK" Version="1.0.*" />
113
147
  ```
114
148
 
115
- ### Python (pip)
149
+ ### Python with pip
150
+
151
+ [![PyPI Downloads](https://img.shields.io/pypi/dm/rocket-welder-sdk.svg)](https://pypi.org/project/rocket-welder-sdk/)
116
152
 
117
153
  ```bash
154
+ # Install from PyPI
118
155
  pip install rocket-welder-sdk
156
+
157
+ # Install with optional dependencies
158
+ pip install rocket-welder-sdk[opencv] # Includes OpenCV
159
+ pip install rocket-welder-sdk[all] # All optional dependencies
160
+
161
+ # Install specific version
162
+ pip install rocket-welder-sdk==1.0.0
163
+ ```
164
+
165
+ ## Quick Start
166
+
167
+ ### C++ Quick Start
168
+ ```cpp
169
+ #include <rocket_welder/client.hpp>
170
+
171
+ auto client = rocket_welder::Client::from_connection_string("shm://my-buffer");
172
+ client.on_frame([](cv::Mat& frame) {
173
+ // Process frame
174
+ });
175
+ client.start();
176
+ ```
177
+
178
+ ### C# Quick Start
179
+ ```csharp
180
+ using RocketWelder.SDK;
181
+
182
+ var client = RocketWelderClient.FromConnectionString("shm://my-buffer");
183
+ client.Start(frame => {
184
+ // Process frame
185
+ });
186
+ ```
187
+
188
+ ### Python Quick Start
189
+ ```python
190
+ import rocket_welder_sdk as rw
191
+
192
+ client = rw.Client.from_connection_string("shm://my-buffer")
193
+
194
+ @client.on_frame
195
+ def process(frame):
196
+ # Process frame
197
+ pass
198
+
199
+ client.start()
119
200
  ```
120
201
 
121
202
  ## Usage Examples
@@ -178,7 +259,7 @@ class Program
178
259
  int frameCount = 0;
179
260
 
180
261
  // Process frames as OpenCV Mat
181
- client.OnFrame((Mat frame) =>
262
+ client.Start((Mat frame) =>
182
263
  {
183
264
  // Add overlay text
184
265
  Cv2.PutText(frame, "Processing", new Point(10, 30),
@@ -188,8 +269,6 @@ class Program
188
269
  Cv2.PutText(frame, $"Frame: {frameCount++}", new Point(10, 60),
189
270
  HersheyFonts.HersheySimplex, 0.5, new Scalar(255, 255, 255), 1);
190
271
  });
191
-
192
- client.Start();
193
272
  }
194
273
  }
195
274
  ```
Binary file
@@ -0,0 +1,20 @@
1
+ """
2
+ Rocket Welder SDK - Python client for RocketWelder video streaming services
3
+
4
+ Zero-copy video streaming and processing with shared memory buffers.
5
+ """
6
+
7
+ __version__ = "1.0.0"
8
+
9
+ from .connection_string import ConnectionString, Protocol
10
+ from .gst_caps import GstCaps
11
+ from .client import RocketWelderClient
12
+ from .exceptions import RocketWelderException
13
+
14
+ __all__ = [
15
+ "ConnectionString",
16
+ "Protocol",
17
+ "GstCaps",
18
+ "RocketWelderClient",
19
+ "RocketWelderException",
20
+ ]
@@ -0,0 +1,326 @@
1
+ """
2
+ RocketWelder client implementation with zero-copy frame processing
3
+
4
+ Mirrors the C# implementation with Python idioms.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import json
10
+ import logging
11
+ import threading
12
+ import struct
13
+ from typing import Optional, Callable, Dict, Any
14
+ from pathlib import Path
15
+
16
+ import cv2
17
+ import numpy as np
18
+
19
+ # Add ZeroBuffer to path
20
+ zerobuffer_path = Path("/mnt/d/source/modelingevolution/streamer/src/zerobuffer/python")
21
+ if zerobuffer_path.exists() and str(zerobuffer_path) not in sys.path:
22
+ sys.path.insert(0, str(zerobuffer_path))
23
+
24
+ from zerobuffer import Reader, Writer, BufferConfig
25
+ from zerobuffer.exceptions import WriterDeadException
26
+
27
+ from .connection_string import ConnectionString, Protocol
28
+ from .gst_caps import GstCaps
29
+ from .gst_metadata import GstMetadata
30
+ from .exceptions import RocketWelderException, ConnectionException
31
+
32
+
33
+ class RocketWelderClient:
34
+ """
35
+ Client for RocketWelder video streaming services
36
+
37
+ Provides zero-copy access to video frames from shared memory or network streams.
38
+ """
39
+
40
+ def __init__(self, connection_string: str, logger: Optional[logging.Logger] = None):
41
+ """
42
+ Initialize client with connection string
43
+
44
+ Args:
45
+ connection_string: Connection string in format protocol://host:port/path
46
+ logger: Optional logger instance
47
+ """
48
+ if not connection_string:
49
+ raise ValueError("Connection string cannot be empty")
50
+
51
+ self._connection = ConnectionString.parse(connection_string)
52
+ self._logger = logger or logging.getLogger(__name__)
53
+ self._frame_callback: Optional[Callable[[np.ndarray], None]] = None
54
+ self._running = False
55
+ self._thread: Optional[threading.Thread] = None
56
+ self._stop_event = threading.Event()
57
+
58
+ # ZeroBuffer components
59
+ self._reader: Optional[Reader] = None
60
+ self._writer: Optional[Writer] = None
61
+
62
+ # Cached video format from metadata
63
+ self._video_format: Optional[GstCaps] = None
64
+
65
+ @classmethod
66
+ def from_args(cls, args: list) -> 'RocketWelderClient':
67
+ """
68
+ Create client from command line arguments
69
+
70
+ Args:
71
+ args: Command line arguments
72
+
73
+ Returns:
74
+ RocketWelderClient instance
75
+ """
76
+ # Check environment variable first
77
+ connection_string = os.environ.get("CONNECTION_STRING")
78
+
79
+ # Override with command line args if present
80
+ if args:
81
+ for arg in args:
82
+ if (arg.startswith("shm://") or
83
+ arg.startswith("mjpeg+http://") or
84
+ arg.startswith("mjpeg+tcp://")):
85
+ connection_string = arg
86
+ break
87
+
88
+ return cls(connection_string or "shm://default")
89
+
90
+ @classmethod
91
+ def from_config(cls, config: Dict[str, Any], logger: Optional[logging.Logger] = None) -> 'RocketWelderClient':
92
+ """
93
+ Create client from configuration dictionary
94
+
95
+ Args:
96
+ config: Configuration dictionary
97
+ logger: Optional logger instance
98
+
99
+ Returns:
100
+ RocketWelderClient instance
101
+ """
102
+ connection_string = ConnectionString.from_config(config)
103
+ return cls(str(connection_string), logger)
104
+
105
+ @classmethod
106
+ def from_environment(cls) -> 'RocketWelderClient':
107
+ """
108
+ Create client from CONNECTION_STRING environment variable
109
+
110
+ Returns:
111
+ RocketWelderClient instance
112
+ """
113
+ connection_string = os.environ.get("CONNECTION_STRING", "shm://default")
114
+ return cls(connection_string)
115
+
116
+ @property
117
+ def connection(self) -> ConnectionString:
118
+ """Get connection string configuration"""
119
+ return self._connection
120
+
121
+ @property
122
+ def is_running(self) -> bool:
123
+ """Check if client is running"""
124
+ return self._running
125
+
126
+ def on_frame(self, callback: Callable[[np.ndarray], None]) -> None:
127
+ """
128
+ Set callback for frame processing
129
+
130
+ Args:
131
+ callback: Function to call with each frame (receives np.ndarray)
132
+ """
133
+ if callback is None:
134
+ raise ValueError("Callback cannot be None")
135
+ self._frame_callback = callback
136
+
137
+ def start(self) -> None:
138
+ """Start frame processing"""
139
+ if self._running:
140
+ return
141
+
142
+ if self._frame_callback is None:
143
+ raise RuntimeError("Frame callback must be set before starting")
144
+
145
+ self._running = True
146
+ self._stop_event.clear()
147
+
148
+ # Start processing thread based on protocol
149
+ if self._connection.protocol == Protocol.SHM:
150
+ self._thread = threading.Thread(target=self._process_shared_memory)
151
+ elif self._connection.protocol == (Protocol.MJPEG | Protocol.HTTP):
152
+ self._thread = threading.Thread(target=self._process_mjpeg_http)
153
+ elif self._connection.protocol == (Protocol.MJPEG | Protocol.TCP):
154
+ self._thread = threading.Thread(target=self._process_mjpeg_tcp)
155
+ else:
156
+ raise NotImplementedError(f"Protocol {self._connection.protocol} not supported")
157
+
158
+ self._thread.daemon = True
159
+ self._thread.start()
160
+
161
+ def stop(self) -> None:
162
+ """Stop frame processing"""
163
+ if not self._running:
164
+ return
165
+
166
+ self._running = False
167
+ self._stop_event.set()
168
+
169
+ if self._thread:
170
+ self._thread.join(timeout=5.0)
171
+ self._thread = None
172
+
173
+ if self._reader:
174
+ self._reader.close()
175
+ self._reader = None
176
+
177
+ if self._writer:
178
+ self._writer.close()
179
+ self._writer = None
180
+
181
+ def _process_shared_memory(self) -> None:
182
+ """Process frames from shared memory (zero-copy)"""
183
+ try:
184
+ buffer_name = self._connection.buffer_name or "default"
185
+ buffer_size = self._connection.buffer_size
186
+ metadata_size = self._connection.metadata_size
187
+
188
+ config = BufferConfig(
189
+ metadata_size=metadata_size,
190
+ payload_size=buffer_size
191
+ )
192
+
193
+ # Create reader - this creates the shared memory buffer
194
+ self._reader = Reader(buffer_name, config, logger=self._logger)
195
+
196
+ if self._connection.mode == "duplex":
197
+ self._logger.warning("Duplex mode not fully implemented yet, operating in read-only mode")
198
+
199
+ self._logger.debug(
200
+ "Created shared memory buffer: %s (size: %d, metadata: %d)",
201
+ buffer_name, buffer_size, metadata_size
202
+ )
203
+
204
+ while not self._stop_event.is_set():
205
+ try:
206
+ # Read frame from shared memory (zero-copy)
207
+ frame = self._reader.read_frame(timeout=1.0)
208
+
209
+ if frame is None or not frame.is_valid:
210
+ self._logger.info("No valid frame read, waiting for next frame")
211
+ continue
212
+
213
+ # Use context manager for proper Frame disposal (RAII)
214
+ with frame:
215
+ # Parse metadata on first frame or when not yet parsed
216
+ if self._video_format is None:
217
+ self._parse_metadata()
218
+
219
+ if self._video_format is None:
220
+ raise RuntimeError("No video format detected")
221
+
222
+ # Create numpy array from frame data (zero-copy)
223
+ # frame.data is a memoryview that directly points to shared memory
224
+ mat = self._video_format.create_mat(frame.data)
225
+
226
+ # Call frame callback with zero-copy array
227
+ self._frame_callback(mat)
228
+
229
+ except WriterDeadException:
230
+ self._logger.warning("Writer process died")
231
+ break
232
+ except Exception as e:
233
+ self._logger.error("Error reading from shared memory: %s", e)
234
+ if not self._stop_event.wait(0.1):
235
+ continue
236
+
237
+ except Exception as e:
238
+ self._logger.error("Error in shared memory processing: %s", e)
239
+ raise
240
+
241
+ def _parse_metadata(self) -> None:
242
+ """Parse video format from metadata"""
243
+ try:
244
+ metadata_view = self._reader.get_metadata()
245
+ if metadata_view is None or len(metadata_view) == 0:
246
+ return
247
+
248
+ # Python's get_metadata() returns raw JSON without any prefixes
249
+ json_str = bytes(metadata_view).decode('utf-8')
250
+
251
+ # Deserialize to strongly-typed GstMetadata
252
+ metadata = GstMetadata.from_json(json_str)
253
+
254
+ # Use the already-parsed GstCaps from metadata
255
+ self._video_format = metadata.caps
256
+ self._logger.info(
257
+ "Parsed metadata - Type: %s, Version: %s, Element: %s, Format: %s",
258
+ metadata.type,
259
+ metadata.version,
260
+ metadata.element_name,
261
+ self._video_format
262
+ )
263
+
264
+ except Exception as e:
265
+ self._logger.warning("Failed to parse metadata: %s", e)
266
+
267
+ def _process_mjpeg_http(self) -> None:
268
+ """Process MJPEG stream over HTTP"""
269
+ url = f"http://{self._connection.host}:{self._connection.port or 80}"
270
+ if self._connection.path:
271
+ url += f"/{self._connection.path}"
272
+ self._process_mjpeg_stream(url)
273
+
274
+ def _process_mjpeg_tcp(self) -> None:
275
+ """Process MJPEG stream over TCP"""
276
+ url = f"tcp://{self._connection.host}:{self._connection.port or 8080}"
277
+ if self._connection.path:
278
+ url += f"/{self._connection.path}"
279
+ self._process_mjpeg_stream(url)
280
+
281
+ def _process_mjpeg_stream(self, url: str) -> None:
282
+ """Process MJPEG stream using OpenCV VideoCapture"""
283
+ try:
284
+ # Use OpenCV VideoCapture which can handle MJPEG streams directly
285
+ cap = cv2.VideoCapture(url)
286
+
287
+ if not cap.isOpened():
288
+ raise ConnectionException(f"Failed to open video stream: {url}")
289
+
290
+ self._logger.info("Opened MJPEG stream: %s", url)
291
+
292
+ while not self._stop_event.is_set():
293
+ try:
294
+ # Read frame from stream
295
+ ret, frame = cap.read()
296
+
297
+ if ret and frame is not None:
298
+ # Process the frame
299
+ self._frame_callback(frame)
300
+ else:
301
+ # Small delay if no frame available
302
+ if self._stop_event.wait(0.01):
303
+ break
304
+
305
+ except Exception as e:
306
+ self._logger.error("Error reading from MJPEG stream: %s", e)
307
+ if self._stop_event.wait(0.1):
308
+ break
309
+
310
+ cap.release()
311
+
312
+ except Exception as e:
313
+ self._logger.error("Error in MJPEG processing: %s", e)
314
+ raise
315
+
316
+ def __enter__(self):
317
+ """Context manager entry"""
318
+ return self
319
+
320
+ def __exit__(self, exc_type, exc_val, exc_tb):
321
+ """Context manager exit"""
322
+ self.stop()
323
+
324
+ def __del__(self):
325
+ """Cleanup on deletion"""
326
+ self.stop()
@@ -0,0 +1,190 @@
1
+ """
2
+ Connection string parsing for RocketWelder SDK
3
+
4
+ Mirrors the C# implementation with Python idioms.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from enum import Flag, auto
9
+ from typing import Optional, Dict, Any
10
+ from urllib.parse import urlparse, parse_qs
11
+ import os
12
+
13
+
14
+ class Protocol(Flag):
15
+ """Protocol flags that can be combined using bitwise operations"""
16
+ NONE = 0
17
+ SHM = auto()
18
+ MJPEG = auto()
19
+ HTTP = auto()
20
+ TCP = auto()
21
+
22
+ def __add__(self, other):
23
+ """Support + operator as bitwise OR"""
24
+ return self | other
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class ConnectionString:
29
+ """
30
+ Readonly connection string configuration
31
+
32
+ Mirrors C# readonly record struct with IParsable interface.
33
+ """
34
+ protocol: Protocol
35
+ host: Optional[str] = None
36
+ port: Optional[int] = None
37
+ path: Optional[str] = None
38
+ buffer_name: Optional[str] = None
39
+ buffer_size: int = 10485760 # 10MB default
40
+ metadata_size: int = 65536 # 64KB default
41
+ mode: str = "oneway" # "oneway" or "duplex"
42
+
43
+ @classmethod
44
+ def parse(cls, s: str, provider=None) -> 'ConnectionString':
45
+ """
46
+ Parse connection string (equivalent to IParsable<T>.Parse in C#)
47
+
48
+ Args:
49
+ s: Connection string in format protocol://host:port/path?params
50
+ provider: Not used, kept for API compatibility with C#
51
+
52
+ Returns:
53
+ ConnectionString instance
54
+
55
+ Raises:
56
+ ValueError: If connection string format is invalid
57
+ """
58
+ if not s:
59
+ raise ValueError("Connection string cannot be empty")
60
+
61
+ # Handle environment variable
62
+ if s.startswith("$"):
63
+ env_var = s[1:]
64
+ s = os.environ.get(env_var, "")
65
+ if not s:
66
+ raise ValueError(f"Environment variable {env_var} not set")
67
+
68
+ # Parse URL
69
+ parsed = urlparse(s)
70
+
71
+ # Determine protocol
72
+ protocol = Protocol.NONE
73
+ if parsed.scheme == "shm":
74
+ protocol = Protocol.SHM
75
+ elif parsed.scheme == "mjpeg+http":
76
+ protocol = Protocol.MJPEG | Protocol.HTTP
77
+ elif parsed.scheme == "mjpeg+tcp":
78
+ protocol = Protocol.MJPEG | Protocol.TCP
79
+ elif parsed.scheme == "http":
80
+ protocol = Protocol.HTTP
81
+ elif parsed.scheme == "tcp":
82
+ protocol = Protocol.TCP
83
+ else:
84
+ raise ValueError(f"Unknown protocol: {parsed.scheme}")
85
+
86
+ # Parse query parameters
87
+ params = parse_qs(parsed.query) if parsed.query else {}
88
+
89
+ # Extract components based on protocol
90
+ host = None
91
+ port = None
92
+ path = None
93
+ buffer_name = None
94
+
95
+ if protocol == Protocol.SHM:
96
+ # For SHM, the netloc or path becomes the buffer name
97
+ buffer_name = parsed.netloc or parsed.path.lstrip("/") or "default"
98
+ else:
99
+ # For network protocols
100
+ host = parsed.hostname
101
+ port = parsed.port
102
+ path = parsed.path.lstrip("/") if parsed.path else None
103
+
104
+ # Extract additional parameters
105
+ buffer_size = int(params.get("buffer_size", [10485760])[0])
106
+ metadata_size = int(params.get("metadata_size", [65536])[0])
107
+ mode = params.get("mode", ["oneway"])[0]
108
+
109
+ return cls(
110
+ protocol=protocol,
111
+ host=host,
112
+ port=port,
113
+ path=path,
114
+ buffer_name=buffer_name,
115
+ buffer_size=buffer_size,
116
+ metadata_size=metadata_size,
117
+ mode=mode
118
+ )
119
+
120
+ @classmethod
121
+ def try_parse(cls, s: str, provider=None) -> tuple[bool, Optional['ConnectionString']]:
122
+ """
123
+ Try to parse connection string (equivalent to IParsable<T>.TryParse in C#)
124
+
125
+ Args:
126
+ s: Connection string to parse
127
+ provider: Not used, kept for API compatibility
128
+
129
+ Returns:
130
+ Tuple of (success, ConnectionString or None)
131
+ """
132
+ try:
133
+ result = cls.parse(s, provider)
134
+ return True, result
135
+ except (ValueError, KeyError):
136
+ return False, None
137
+
138
+ @classmethod
139
+ def from_config(cls, config: Dict[str, Any]) -> 'ConnectionString':
140
+ """
141
+ Create from configuration dictionary (mirrors IConfiguration in C#)
142
+
143
+ Args:
144
+ config: Configuration dictionary
145
+
146
+ Returns:
147
+ ConnectionString instance
148
+ """
149
+ # Try to get connection string from various sources
150
+ connection_string = (
151
+ config.get("CONNECTION_STRING") or
152
+ config.get("RocketWelder", {}).get("ConnectionString") or
153
+ config.get("ConnectionString") or
154
+ os.environ.get("CONNECTION_STRING")
155
+ )
156
+
157
+ if connection_string:
158
+ return cls.parse(connection_string)
159
+
160
+ # Build from components
161
+ protocol_str = config.get("RocketWelder", {}).get("Protocol", "shm")
162
+ host = config.get("RocketWelder", {}).get("Host")
163
+ port = config.get("RocketWelder", {}).get("Port")
164
+ path = config.get("RocketWelder", {}).get("Path") or config.get("RocketWelder", {}).get("BufferName")
165
+
166
+ if protocol_str == "shm":
167
+ connection_string = f"shm://{path or 'default'}"
168
+ elif host:
169
+ port_part = f":{port}" if port else ""
170
+ path_part = f"/{path}" if path else ""
171
+ connection_string = f"{protocol_str}://{host}{port_part}{path_part}"
172
+ else:
173
+ connection_string = "shm://default"
174
+
175
+ return cls.parse(connection_string)
176
+
177
+ def __str__(self) -> str:
178
+ """String representation of connection string"""
179
+ if self.protocol == Protocol.SHM:
180
+ return f"shm://{self.buffer_name or 'default'}?buffer_size={self.buffer_size}&metadata_size={self.metadata_size}&mode={self.mode}"
181
+ elif self.protocol == (Protocol.MJPEG | Protocol.HTTP):
182
+ port_part = f":{self.port}" if self.port else ""
183
+ path_part = f"/{self.path}" if self.path else ""
184
+ return f"mjpeg+http://{self.host}{port_part}{path_part}"
185
+ elif self.protocol == (Protocol.MJPEG | Protocol.TCP):
186
+ port_part = f":{self.port}" if self.port else ""
187
+ path_part = f"/{self.path}" if self.path else ""
188
+ return f"mjpeg+tcp://{self.host}{port_part}{path_part}"
189
+ else:
190
+ return f"{self.protocol.name.lower()}://{self.host}:{self.port}/{self.path}"
@@ -0,0 +1,23 @@
1
+ """
2
+ Exception classes for RocketWelder SDK
3
+ """
4
+
5
+
6
+ class RocketWelderException(Exception):
7
+ """Base exception for RocketWelder SDK"""
8
+ pass
9
+
10
+
11
+ class ConnectionException(RocketWelderException):
12
+ """Exception raised for connection errors"""
13
+ pass
14
+
15
+
16
+ class ProtocolException(RocketWelderException):
17
+ """Exception raised for protocol-specific errors"""
18
+ pass
19
+
20
+
21
+ class BufferException(RocketWelderException):
22
+ """Exception raised for buffer-related errors"""
23
+ pass
@@ -0,0 +1,224 @@
1
+ """
2
+ GStreamer caps parsing for video format information
3
+
4
+ Mirrors the C# implementation with Python idioms.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Optional, Tuple
9
+ import re
10
+ import numpy as np
11
+ import cv2
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class GstCaps:
16
+ """
17
+ Readonly GStreamer caps configuration
18
+
19
+ Mirrors C# readonly record struct with IParsable interface.
20
+ """
21
+ width: int
22
+ height: int
23
+ format: str
24
+ framerate: Optional[Tuple[int, int]] = None
25
+
26
+ @classmethod
27
+ def parse(cls, s: str, provider=None) -> 'GstCaps':
28
+ """
29
+ Parse GStreamer caps string (equivalent to IParsable<T>.Parse in C#)
30
+
31
+ Args:
32
+ s: Caps string like "video/x-raw,format=RGB,width=640,height=480,framerate=30/1"
33
+ provider: Not used, kept for API compatibility with C#
34
+
35
+ Returns:
36
+ GstCaps instance
37
+
38
+ Raises:
39
+ ValueError: If caps format is invalid
40
+ """
41
+ if not s:
42
+ raise ValueError("Caps string cannot be empty")
43
+
44
+ # Remove video/x-raw prefix if present
45
+ if s.startswith("video/x-raw"):
46
+ s = s[len("video/x-raw"):].lstrip(",")
47
+
48
+ # Parse key=value pairs
49
+ params = {}
50
+ for part in s.split(","):
51
+ if "=" in part:
52
+ key, value = part.split("=", 1)
53
+ params[key.strip()] = value.strip()
54
+
55
+ # Extract required fields
56
+ if "width" not in params:
57
+ raise ValueError("Missing 'width' in caps")
58
+ if "height" not in params:
59
+ raise ValueError("Missing 'height' in caps")
60
+
61
+ # Handle GStreamer type annotations like "(int)640"
62
+ width_str = params["width"]
63
+ if width_str.startswith("(int)"):
64
+ width_str = width_str[5:]
65
+ width = int(width_str)
66
+
67
+ height_str = params["height"]
68
+ if height_str.startswith("(int)"):
69
+ height_str = height_str[5:]
70
+ height = int(height_str)
71
+ format_str = params.get("format", "RGB")
72
+
73
+ # Parse framerate if present
74
+ framerate = None
75
+ if "framerate" in params:
76
+ framerate_str = params["framerate"]
77
+ # Handle GStreamer type annotations like "(fraction)30/1"
78
+ if framerate_str.startswith("(fraction)"):
79
+ framerate_str = framerate_str[10:]
80
+ fr_match = re.match(r"(\d+)/(\d+)", framerate_str)
81
+ if fr_match:
82
+ framerate = (int(fr_match.group(1)), int(fr_match.group(2)))
83
+
84
+ return cls(
85
+ width=width,
86
+ height=height,
87
+ format=format_str,
88
+ framerate=framerate
89
+ )
90
+
91
+ @classmethod
92
+ def try_parse(cls, s: str, provider=None) -> tuple[bool, Optional['GstCaps']]:
93
+ """
94
+ Try to parse caps string (equivalent to IParsable<T>.TryParse in C#)
95
+
96
+ Args:
97
+ s: Caps string to parse
98
+ provider: Not used, kept for API compatibility
99
+
100
+ Returns:
101
+ Tuple of (success, GstCaps or None)
102
+ """
103
+ try:
104
+ result = cls.parse(s, provider)
105
+ return True, result
106
+ except (ValueError, KeyError):
107
+ return False, None
108
+
109
+ @classmethod
110
+ def from_simple(cls, width: int, height: int, format_str: str = "RGB") -> 'GstCaps':
111
+ """
112
+ Create from simple parameters
113
+
114
+ Args:
115
+ width: Frame width
116
+ height: Frame height
117
+ format_str: Pixel format (RGB, BGR, GRAY8, etc.)
118
+
119
+ Returns:
120
+ GstCaps instance
121
+ """
122
+ return cls(width=width, height=height, format=format_str)
123
+
124
+ def get_opencv_dtype(self) -> int:
125
+ """
126
+ Get OpenCV data type for the format
127
+
128
+ Returns:
129
+ OpenCV dtype constant
130
+ """
131
+ # Map GStreamer formats to OpenCV types
132
+ format_map = {
133
+ "RGB": cv2.CV_8UC3,
134
+ "BGR": cv2.CV_8UC3,
135
+ "RGBA": cv2.CV_8UC4,
136
+ "BGRA": cv2.CV_8UC4,
137
+ "GRAY8": cv2.CV_8UC1,
138
+ "GRAY16_LE": cv2.CV_16UC1,
139
+ "GRAY16_BE": cv2.CV_16UC1,
140
+ }
141
+
142
+ return format_map.get(self.format, cv2.CV_8UC3)
143
+
144
+ def get_channels(self) -> int:
145
+ """
146
+ Get number of channels for the format
147
+
148
+ Returns:
149
+ Number of channels
150
+ """
151
+ if self.format in ["RGB", "BGR"]:
152
+ return 3
153
+ elif self.format in ["RGBA", "BGRA"]:
154
+ return 4
155
+ elif self.format.startswith("GRAY"):
156
+ return 1
157
+ else:
158
+ return 3 # Default to 3 channels
159
+
160
+ def get_numpy_dtype(self) -> np.dtype:
161
+ """
162
+ Get NumPy data type for the format
163
+
164
+ Returns:
165
+ NumPy dtype
166
+ """
167
+ if "16" in self.format:
168
+ return np.dtype(np.uint16)
169
+ else:
170
+ return np.dtype(np.uint8)
171
+
172
+ def create_mat(self, data_ptr: memoryview) -> np.ndarray:
173
+ """
174
+ Create OpenCV Mat from data pointer without copying (zero-copy)
175
+
176
+ Args:
177
+ data_ptr: Memory view of the frame data
178
+
179
+ Returns:
180
+ NumPy array that wraps the data (no copy)
181
+ """
182
+ # Calculate expected size
183
+ channels = self.get_channels()
184
+ dtype = self.get_numpy_dtype()
185
+ expected_size = self.width * self.height * channels * dtype.itemsize
186
+
187
+ # Create numpy array from memoryview (zero-copy)
188
+ # The memoryview directly points to shared memory
189
+ flat_array = np.frombuffer(data_ptr, dtype=dtype, count=self.width * self.height * channels)
190
+
191
+ # Reshape to image dimensions
192
+ if channels == 1:
193
+ return flat_array.reshape((self.height, self.width))
194
+ else:
195
+ return flat_array.reshape((self.height, self.width, channels))
196
+
197
+ def create_mat_from_buffer(self, buffer: bytes) -> np.ndarray:
198
+ """
199
+ Create OpenCV Mat from byte buffer (makes a copy)
200
+
201
+ Args:
202
+ buffer: Byte buffer containing frame data
203
+
204
+ Returns:
205
+ NumPy array (copy of data)
206
+ """
207
+ channels = self.get_channels()
208
+ dtype = self.get_numpy_dtype()
209
+
210
+ # Create numpy array from bytes
211
+ flat_array = np.frombuffer(buffer, dtype=dtype)
212
+
213
+ # Reshape to image dimensions
214
+ if channels == 1:
215
+ return flat_array.reshape((self.height, self.width))
216
+ else:
217
+ return flat_array.reshape((self.height, self.width, channels))
218
+
219
+ def __str__(self) -> str:
220
+ """String representation as GStreamer caps"""
221
+ caps = f"video/x-raw,format={self.format},width={self.width},height={self.height}"
222
+ if self.framerate:
223
+ caps += f",framerate={self.framerate[0]}/{self.framerate[1]}"
224
+ return caps
@@ -0,0 +1,43 @@
1
+ """GStreamer metadata structure matching the JSON written by GStreamer plugins"""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+ import json
6
+
7
+ from .gst_caps import GstCaps
8
+
9
+
10
+ @dataclass
11
+ class GstMetadata:
12
+ """Metadata structure that matches the JSON written by GStreamer plugins"""
13
+
14
+ type: str
15
+ version: str
16
+ caps: GstCaps
17
+ element_name: str
18
+
19
+ @classmethod
20
+ def from_json(cls, json_str: str) -> 'GstMetadata':
21
+ """Deserialize from JSON string"""
22
+ data = json.loads(json_str)
23
+
24
+ # Parse caps string to GstCaps
25
+ caps = GstCaps.parse(data['caps'])
26
+
27
+ return cls(
28
+ type=data['type'],
29
+ version=data['version'],
30
+ caps=caps,
31
+ element_name=data['element_name']
32
+ )
33
+
34
+ def to_json(self) -> str:
35
+ """Serialize to JSON string"""
36
+ # Convert GstCaps back to string for JSON
37
+ data = {
38
+ 'type': self.type,
39
+ 'version': self.version,
40
+ 'caps': self.caps.caps_string if self.caps.caps_string else str(self.caps),
41
+ 'element_name': self.element_name
42
+ }
43
+ return json.dumps(data)
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rocket-welder-sdk
3
- Version: 1.0.2
4
- Summary: Client library for RocketWelder video streaming services
3
+ Version: 1.0.4
4
+ Summary: High-performance video streaming client library for RocketWelder services
5
5
  Home-page: https://github.com/modelingevolution/rocket-welder-sdk
6
- Author: RocketWelder
6
+ Author: ModelingEvolution
7
7
  Classifier: Development Status :: 3 - Alpha
8
8
  Classifier: Intended Audience :: Developers
9
9
  Classifier: Topic :: Multimedia :: Video
@@ -18,7 +18,7 @@ Requires-Python: >=3.8
18
18
  Description-Content-Type: text/markdown
19
19
  Requires-Dist: numpy>=1.20.0
20
20
  Requires-Dist: opencv-python>=4.5.0
21
- Requires-Dist: zerobuffer-ipc>=1.0.0
21
+ Requires-Dist: zerobuffer-ipc>=1.1.0
22
22
  Provides-Extra: dev
23
23
  Requires-Dist: pytest>=7.0; extra == "dev"
24
24
  Requires-Dist: black>=22.0; extra == "dev"
@@ -35,6 +35,11 @@ Dynamic: summary
35
35
 
36
36
  # Rocket Welder SDK
37
37
 
38
+ [![NuGet](https://img.shields.io/nuget/v/RocketWelder.SDK.svg)](https://www.nuget.org/packages/RocketWelder.SDK/)
39
+ [![PyPI](https://img.shields.io/pypi/v/rocket-welder-sdk.svg)](https://pypi.org/project/rocket-welder-sdk/)
40
+ [![vcpkg](https://img.shields.io/badge/vcpkg-rocket--welder--sdk-blue)](https://github.com/modelingevolution/rocket-welder-sdk-vcpkg-registry)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
42
+
38
43
  Multi-language client libraries for interacting with RocketWelder video streaming services.
39
44
 
40
45
  ## Overview
@@ -100,22 +105,98 @@ CONNECTION_STRING=shm://camera_feed?buffer_size=20MB&metadata_size=4KB
100
105
 
101
106
  ## Installation
102
107
 
103
- ### C++ (vcpkg)
108
+ ### C++ with vcpkg
104
109
 
110
+ Configure the custom registry in your `vcpkg-configuration.json`:
111
+ ```json
112
+ {
113
+ "registries": [
114
+ {
115
+ "kind": "git",
116
+ "repository": "https://github.com/modelingevolution/rocket-welder-sdk-vcpkg-registry",
117
+ "baseline": "YOUR_BASELINE_HERE",
118
+ "packages": ["rocket-welder-sdk"]
119
+ }
120
+ ]
121
+ }
122
+ ```
123
+
124
+ Then install:
105
125
  ```bash
126
+ # Install via vcpkg
106
127
  vcpkg install rocket-welder-sdk
128
+
129
+ # Or integrate with CMake
130
+ find_package(rocket-welder-sdk CONFIG REQUIRED)
131
+ target_link_libraries(your_app PRIVATE rocket-welder-sdk::rocket-welder-sdk)
107
132
  ```
108
133
 
109
- ### C# (NuGet)
134
+ ### C# with NuGet
135
+
136
+ [![NuGet Downloads](https://img.shields.io/nuget/dt/RocketWelder.SDK.svg)](https://www.nuget.org/packages/RocketWelder.SDK/)
110
137
 
111
138
  ```bash
139
+ # Package Manager Console
140
+ Install-Package RocketWelder.SDK
141
+
142
+ # .NET CLI
112
143
  dotnet add package RocketWelder.SDK
144
+
145
+ # PackageReference in .csproj
146
+ <PackageReference Include="RocketWelder.SDK" Version="1.0.*" />
113
147
  ```
114
148
 
115
- ### Python (pip)
149
+ ### Python with pip
150
+
151
+ [![PyPI Downloads](https://img.shields.io/pypi/dm/rocket-welder-sdk.svg)](https://pypi.org/project/rocket-welder-sdk/)
116
152
 
117
153
  ```bash
154
+ # Install from PyPI
118
155
  pip install rocket-welder-sdk
156
+
157
+ # Install with optional dependencies
158
+ pip install rocket-welder-sdk[opencv] # Includes OpenCV
159
+ pip install rocket-welder-sdk[all] # All optional dependencies
160
+
161
+ # Install specific version
162
+ pip install rocket-welder-sdk==1.0.0
163
+ ```
164
+
165
+ ## Quick Start
166
+
167
+ ### C++ Quick Start
168
+ ```cpp
169
+ #include <rocket_welder/client.hpp>
170
+
171
+ auto client = rocket_welder::Client::from_connection_string("shm://my-buffer");
172
+ client.on_frame([](cv::Mat& frame) {
173
+ // Process frame
174
+ });
175
+ client.start();
176
+ ```
177
+
178
+ ### C# Quick Start
179
+ ```csharp
180
+ using RocketWelder.SDK;
181
+
182
+ var client = RocketWelderClient.FromConnectionString("shm://my-buffer");
183
+ client.Start(frame => {
184
+ // Process frame
185
+ });
186
+ ```
187
+
188
+ ### Python Quick Start
189
+ ```python
190
+ import rocket_welder_sdk as rw
191
+
192
+ client = rw.Client.from_connection_string("shm://my-buffer")
193
+
194
+ @client.on_frame
195
+ def process(frame):
196
+ # Process frame
197
+ pass
198
+
199
+ client.start()
119
200
  ```
120
201
 
121
202
  ## Usage Examples
@@ -178,7 +259,7 @@ class Program
178
259
  int frameCount = 0;
179
260
 
180
261
  // Process frames as OpenCV Mat
181
- client.OnFrame((Mat frame) =>
262
+ client.Start((Mat frame) =>
182
263
  {
183
264
  // Add overlay text
184
265
  Cv2.PutText(frame, "Processing", new Point(10, 30),
@@ -188,8 +269,6 @@ class Program
188
269
  Cv2.PutText(frame, $"Frame: {frameCount++}", new Point(10, 60),
189
270
  HersheyFonts.HersheySimplex, 0.5, new Scalar(255, 255, 255), 1);
190
271
  });
191
-
192
- client.Start();
193
272
  }
194
273
  }
195
274
  ```
@@ -0,0 +1,17 @@
1
+ MANIFEST.in
2
+ logo.png
3
+ setup.py
4
+ ../README.md
5
+ rocket_welder_sdk/__init__.py
6
+ rocket_welder_sdk/client.py
7
+ rocket_welder_sdk.egg-info/PKG-INFO
8
+ rocket_welder_sdk.egg-info/SOURCES.txt
9
+ rocket_welder_sdk.egg-info/dependency_links.txt
10
+ rocket_welder_sdk.egg-info/requires.txt
11
+ rocket_welder_sdk.egg-info/top_level.txt
12
+ rocket_welder_sdk/rocket_welder_sdk/__init__.py
13
+ rocket_welder_sdk/rocket_welder_sdk/client.py
14
+ rocket_welder_sdk/rocket_welder_sdk/connection_string.py
15
+ rocket_welder_sdk/rocket_welder_sdk/exceptions.py
16
+ rocket_welder_sdk/rocket_welder_sdk/gst_caps.py
17
+ rocket_welder_sdk/rocket_welder_sdk/gst_metadata.py
@@ -1,6 +1,6 @@
1
1
  numpy>=1.20.0
2
2
  opencv-python>=4.5.0
3
- zerobuffer-ipc>=1.0.0
3
+ zerobuffer-ipc>=1.1.0
4
4
 
5
5
  [dev]
6
6
  pytest>=7.0
@@ -11,9 +11,9 @@ else:
11
11
 
12
12
  setup(
13
13
  name="rocket-welder-sdk",
14
- version="1.0.2",
15
- author="RocketWelder",
16
- description="Client library for RocketWelder video streaming services",
14
+ version="1.0.4",
15
+ author="ModelingEvolution",
16
+ description="High-performance video streaming client library for RocketWelder services",
17
17
  long_description=long_description,
18
18
  long_description_content_type="text/markdown",
19
19
  url="https://github.com/modelingevolution/rocket-welder-sdk",
@@ -34,7 +34,7 @@ setup(
34
34
  install_requires=[
35
35
  "numpy>=1.20.0",
36
36
  "opencv-python>=4.5.0",
37
- "zerobuffer-ipc>=1.0.0",
37
+ "zerobuffer-ipc>=1.1.0",
38
38
  ],
39
39
  extras_require={
40
40
  "dev": [
@@ -1,8 +0,0 @@
1
- setup.py
2
- rocket_welder_sdk/__init__.py
3
- rocket_welder_sdk/client.py
4
- rocket_welder_sdk.egg-info/PKG-INFO
5
- rocket_welder_sdk.egg-info/SOURCES.txt
6
- rocket_welder_sdk.egg-info/dependency_links.txt
7
- rocket_welder_sdk.egg-info/requires.txt
8
- rocket_welder_sdk.egg-info/top_level.txt