rocket-welder-sdk 1.0.3__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.
- {rocket_welder_sdk-1.0.3 → rocket_welder_sdk-1.0.4}/PKG-INFO +87 -8
- rocket_welder_sdk-1.0.4/rocket_welder_sdk/rocket_welder_sdk/__init__.py +20 -0
- rocket_welder_sdk-1.0.4/rocket_welder_sdk/rocket_welder_sdk/client.py +326 -0
- rocket_welder_sdk-1.0.4/rocket_welder_sdk/rocket_welder_sdk/connection_string.py +190 -0
- rocket_welder_sdk-1.0.4/rocket_welder_sdk/rocket_welder_sdk/exceptions.py +23 -0
- rocket_welder_sdk-1.0.4/rocket_welder_sdk/rocket_welder_sdk/gst_caps.py +224 -0
- rocket_welder_sdk-1.0.4/rocket_welder_sdk/rocket_welder_sdk/gst_metadata.py +43 -0
- {rocket_welder_sdk-1.0.3 → rocket_welder_sdk-1.0.4}/rocket_welder_sdk.egg-info/PKG-INFO +87 -8
- rocket_welder_sdk-1.0.4/rocket_welder_sdk.egg-info/SOURCES.txt +17 -0
- {rocket_welder_sdk-1.0.3 → rocket_welder_sdk-1.0.4}/rocket_welder_sdk.egg-info/requires.txt +1 -1
- {rocket_welder_sdk-1.0.3 → rocket_welder_sdk-1.0.4}/setup.py +2 -2
- rocket_welder_sdk-1.0.3/rocket_welder_sdk.egg-info/SOURCES.txt +0 -11
- {rocket_welder_sdk-1.0.3 → rocket_welder_sdk-1.0.4}/MANIFEST.in +0 -0
- {rocket_welder_sdk-1.0.3 → rocket_welder_sdk-1.0.4}/logo.png +0 -0
- {rocket_welder_sdk-1.0.3 → rocket_welder_sdk-1.0.4}/rocket_welder_sdk/__init__.py +0 -0
- {rocket_welder_sdk-1.0.3 → rocket_welder_sdk-1.0.4}/rocket_welder_sdk/client.py +0 -0
- {rocket_welder_sdk-1.0.3 → rocket_welder_sdk-1.0.4}/rocket_welder_sdk.egg-info/dependency_links.txt +0 -0
- {rocket_welder_sdk-1.0.3 → rocket_welder_sdk-1.0.4}/rocket_welder_sdk.egg-info/top_level.txt +0 -0
- {rocket_welder_sdk-1.0.3 → rocket_welder_sdk-1.0.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rocket-welder-sdk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: High-performance video streaming client library for RocketWelder services
|
|
5
5
|
Home-page: https://github.com/modelingevolution/rocket-welder-sdk
|
|
6
6
|
Author: ModelingEvolution
|
|
@@ -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.
|
|
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
|
+
[](https://www.nuget.org/packages/RocketWelder.SDK/)
|
|
39
|
+
[](https://pypi.org/project/rocket-welder-sdk/)
|
|
40
|
+
[](https://github.com/modelingevolution/rocket-welder-sdk-vcpkg-registry)
|
|
41
|
+
[](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++
|
|
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#
|
|
134
|
+
### C# with NuGet
|
|
135
|
+
|
|
136
|
+
[](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
|
|
149
|
+
### Python with pip
|
|
150
|
+
|
|
151
|
+
[](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.
|
|
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,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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rocket-welder-sdk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: High-performance video streaming client library for RocketWelder services
|
|
5
5
|
Home-page: https://github.com/modelingevolution/rocket-welder-sdk
|
|
6
6
|
Author: ModelingEvolution
|
|
@@ -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.
|
|
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
|
+
[](https://www.nuget.org/packages/RocketWelder.SDK/)
|
|
39
|
+
[](https://pypi.org/project/rocket-welder-sdk/)
|
|
40
|
+
[](https://github.com/modelingevolution/rocket-welder-sdk-vcpkg-registry)
|
|
41
|
+
[](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++
|
|
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#
|
|
134
|
+
### C# with NuGet
|
|
135
|
+
|
|
136
|
+
[](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
|
|
149
|
+
### Python with pip
|
|
150
|
+
|
|
151
|
+
[](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.
|
|
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
|
|
@@ -11,7 +11,7 @@ else:
|
|
|
11
11
|
|
|
12
12
|
setup(
|
|
13
13
|
name="rocket-welder-sdk",
|
|
14
|
-
version="1.0.
|
|
14
|
+
version="1.0.4",
|
|
15
15
|
author="ModelingEvolution",
|
|
16
16
|
description="High-performance video streaming client library for RocketWelder services",
|
|
17
17
|
long_description=long_description,
|
|
@@ -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.
|
|
37
|
+
"zerobuffer-ipc>=1.1.0",
|
|
38
38
|
],
|
|
39
39
|
extras_require={
|
|
40
40
|
"dev": [
|
|
@@ -1,11 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rocket_welder_sdk-1.0.3 → rocket_welder_sdk-1.0.4}/rocket_welder_sdk.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{rocket_welder_sdk-1.0.3 → rocket_welder_sdk-1.0.4}/rocket_welder_sdk.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|