rocket-welder-sdk 1.0.5__py3-none-any.whl → 1.1.25__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rocket_welder_sdk/__init__.py +52 -4
- rocket_welder_sdk/bytes_size.py +234 -0
- rocket_welder_sdk/connection_string.py +232 -0
- rocket_welder_sdk/controllers.py +668 -0
- rocket_welder_sdk/gst_metadata.py +411 -0
- rocket_welder_sdk/py.typed +2 -0
- rocket_welder_sdk/rocket_welder_client.py +167 -0
- rocket_welder_sdk-1.1.25.dist-info/METADATA +497 -0
- rocket_welder_sdk-1.1.25.dist-info/RECORD +11 -0
- rocket_welder_sdk/client.py +0 -183
- rocket_welder_sdk/rocket_welder_sdk/__init__.py +0 -20
- rocket_welder_sdk/rocket_welder_sdk/client.py +0 -326
- rocket_welder_sdk/rocket_welder_sdk/connection_string.py +0 -190
- rocket_welder_sdk/rocket_welder_sdk/exceptions.py +0 -23
- rocket_welder_sdk/rocket_welder_sdk/gst_caps.py +0 -224
- rocket_welder_sdk/rocket_welder_sdk/gst_metadata.py +0 -43
- rocket_welder_sdk-1.0.5.dist-info/METADATA +0 -36
- rocket_welder_sdk-1.0.5.dist-info/RECORD +0 -12
- {rocket_welder_sdk-1.0.5.dist-info → rocket_welder_sdk-1.1.25.dist-info}/WHEEL +0 -0
- {rocket_welder_sdk-1.0.5.dist-info → rocket_welder_sdk-1.1.25.dist-info}/top_level.txt +0 -0
rocket_welder_sdk/__init__.py
CHANGED
|
@@ -1,8 +1,56 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
RocketWelder SDK - Enterprise-grade Python client library for video streaming services.
|
|
3
|
+
|
|
4
|
+
High-performance video streaming using shared memory (ZeroBuffer) for zero-copy operations.
|
|
3
5
|
"""
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from .bytes_size import BytesSize
|
|
11
|
+
from .connection_string import ConnectionMode, ConnectionString, Protocol
|
|
12
|
+
from .controllers import DuplexShmController, IController, OneWayShmController
|
|
13
|
+
from .gst_metadata import GstCaps, GstMetadata
|
|
14
|
+
from .rocket_welder_client import RocketWelderClient
|
|
15
|
+
|
|
16
|
+
# Alias for backward compatibility
|
|
17
|
+
Client = RocketWelderClient
|
|
18
|
+
|
|
19
|
+
__version__ = "1.1.0"
|
|
20
|
+
|
|
21
|
+
# Configure library logger with NullHandler (best practice for libraries)
|
|
22
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
23
|
+
|
|
24
|
+
# Configure from environment variable and propagate to zerobuffer
|
|
25
|
+
_log_level = os.environ.get("ROCKET_WELDER_LOG_LEVEL")
|
|
26
|
+
if _log_level:
|
|
27
|
+
try:
|
|
28
|
+
# Set rocket-welder-sdk log level
|
|
29
|
+
logging.getLogger(__name__).setLevel(getattr(logging, _log_level.upper()))
|
|
30
|
+
|
|
31
|
+
# Propagate to zerobuffer if not already set
|
|
32
|
+
if not os.environ.get("ZEROBUFFER_LOG_LEVEL"):
|
|
33
|
+
os.environ["ZEROBUFFER_LOG_LEVEL"] = _log_level
|
|
34
|
+
# Also configure zerobuffer logger if already imported
|
|
35
|
+
zerobuffer_logger = logging.getLogger("zerobuffer")
|
|
36
|
+
zerobuffer_logger.setLevel(getattr(logging, _log_level.upper()))
|
|
37
|
+
except AttributeError:
|
|
38
|
+
pass # Invalid log level, ignore
|
|
6
39
|
|
|
7
|
-
|
|
8
|
-
|
|
40
|
+
__all__ = [
|
|
41
|
+
# Core types
|
|
42
|
+
"BytesSize",
|
|
43
|
+
"Client", # Backward compatibility
|
|
44
|
+
"ConnectionMode",
|
|
45
|
+
"ConnectionString",
|
|
46
|
+
"DuplexShmController",
|
|
47
|
+
# GStreamer metadata
|
|
48
|
+
"GstCaps",
|
|
49
|
+
"GstMetadata",
|
|
50
|
+
# Controllers
|
|
51
|
+
"IController",
|
|
52
|
+
"OneWayShmController",
|
|
53
|
+
"Protocol",
|
|
54
|
+
# Main client
|
|
55
|
+
"RocketWelderClient",
|
|
56
|
+
]
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enterprise-grade Bytes size representation with parsing support.
|
|
3
|
+
Matches C# Bytes struct functionality.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import ClassVar
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class BytesSize:
|
|
15
|
+
"""
|
|
16
|
+
Immutable representation of byte sizes with human-readable formatting.
|
|
17
|
+
|
|
18
|
+
Supports parsing from strings like "256MB", "4KB", "1.5GB" etc.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
_value: int
|
|
22
|
+
_precision: int = 0
|
|
23
|
+
|
|
24
|
+
def __init__(self, value: int, precision: int = 0) -> None:
|
|
25
|
+
"""Initialize BytesSize with value and optional precision."""
|
|
26
|
+
object.__setattr__(self, "_value", value)
|
|
27
|
+
object.__setattr__(self, "_precision", precision)
|
|
28
|
+
|
|
29
|
+
# Size multipliers
|
|
30
|
+
_SUFFIXES: ClassVar[dict[str, int]] = {
|
|
31
|
+
"B": 1,
|
|
32
|
+
"K": 1024,
|
|
33
|
+
"KB": 1024,
|
|
34
|
+
"M": 1024 * 1024,
|
|
35
|
+
"MB": 1024 * 1024,
|
|
36
|
+
"G": 1024 * 1024 * 1024,
|
|
37
|
+
"GB": 1024 * 1024 * 1024,
|
|
38
|
+
"T": 1024 * 1024 * 1024 * 1024,
|
|
39
|
+
"TB": 1024 * 1024 * 1024 * 1024,
|
|
40
|
+
"P": 1024 * 1024 * 1024 * 1024 * 1024,
|
|
41
|
+
"PB": 1024 * 1024 * 1024 * 1024 * 1024,
|
|
42
|
+
"E": 1024 * 1024 * 1024 * 1024 * 1024 * 1024,
|
|
43
|
+
"EB": 1024 * 1024 * 1024 * 1024 * 1024 * 1024,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Pattern for parsing size strings
|
|
47
|
+
_PATTERN: ClassVar[re.Pattern[str]] = re.compile(r"^([\d.,]+)\s*([KMGTPE]?B?)$", re.IGNORECASE)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def value(self) -> int:
|
|
51
|
+
"""Get the raw byte value."""
|
|
52
|
+
return self._value
|
|
53
|
+
|
|
54
|
+
def __int__(self) -> int:
|
|
55
|
+
"""Convert to integer."""
|
|
56
|
+
return self._value
|
|
57
|
+
|
|
58
|
+
def __float__(self) -> float:
|
|
59
|
+
"""Convert to float."""
|
|
60
|
+
return float(self._value)
|
|
61
|
+
|
|
62
|
+
def __str__(self) -> str:
|
|
63
|
+
"""Format as human-readable string."""
|
|
64
|
+
return self._format_size(self._value, self._precision)
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
"""Developer-friendly representation."""
|
|
68
|
+
return f"BytesSize({self._value}, precision={self._precision})"
|
|
69
|
+
|
|
70
|
+
def __eq__(self, other: object) -> bool:
|
|
71
|
+
"""Equality comparison."""
|
|
72
|
+
if isinstance(other, BytesSize):
|
|
73
|
+
return self._value == other._value
|
|
74
|
+
if isinstance(other, (int, float)):
|
|
75
|
+
return self._value == other
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
def __lt__(self, other: BytesSize | int | float) -> bool:
|
|
79
|
+
"""Less than comparison."""
|
|
80
|
+
if isinstance(other, BytesSize):
|
|
81
|
+
return self._value < other._value
|
|
82
|
+
if isinstance(other, (int, float)):
|
|
83
|
+
return self._value < other
|
|
84
|
+
return NotImplemented
|
|
85
|
+
|
|
86
|
+
def __le__(self, other: BytesSize | int | float) -> bool:
|
|
87
|
+
"""Less than or equal comparison."""
|
|
88
|
+
if isinstance(other, BytesSize):
|
|
89
|
+
return self._value <= other._value
|
|
90
|
+
if isinstance(other, (int, float)):
|
|
91
|
+
return self._value <= other
|
|
92
|
+
return NotImplemented
|
|
93
|
+
|
|
94
|
+
def __gt__(self, other: BytesSize | int | float) -> bool:
|
|
95
|
+
"""Greater than comparison."""
|
|
96
|
+
if isinstance(other, BytesSize):
|
|
97
|
+
return self._value > other._value
|
|
98
|
+
if isinstance(other, (int, float)):
|
|
99
|
+
return self._value > other
|
|
100
|
+
return NotImplemented
|
|
101
|
+
|
|
102
|
+
def __ge__(self, other: BytesSize | int | float) -> bool:
|
|
103
|
+
"""Greater than or equal comparison."""
|
|
104
|
+
if isinstance(other, BytesSize):
|
|
105
|
+
return self._value >= other._value
|
|
106
|
+
if isinstance(other, (int, float)):
|
|
107
|
+
return self._value >= other
|
|
108
|
+
return NotImplemented
|
|
109
|
+
|
|
110
|
+
def __add__(self, other: BytesSize | int) -> BytesSize:
|
|
111
|
+
"""Add byte sizes."""
|
|
112
|
+
if isinstance(other, BytesSize):
|
|
113
|
+
return BytesSize(self._value + other._value, self._precision)
|
|
114
|
+
if isinstance(other, int):
|
|
115
|
+
return BytesSize(self._value + other, self._precision)
|
|
116
|
+
return NotImplemented
|
|
117
|
+
|
|
118
|
+
def __sub__(self, other: BytesSize | int) -> BytesSize:
|
|
119
|
+
"""Subtract byte sizes."""
|
|
120
|
+
if isinstance(other, BytesSize):
|
|
121
|
+
return BytesSize(self._value - other._value, self._precision)
|
|
122
|
+
if isinstance(other, int):
|
|
123
|
+
return BytesSize(self._value - other, self._precision)
|
|
124
|
+
return NotImplemented
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def parse(cls, value: str | int | float | BytesSize) -> BytesSize:
|
|
128
|
+
"""
|
|
129
|
+
Parse a string, number, or BytesSize into a BytesSize object.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
value: Value to parse (e.g., "256MB", 1024, "4.5GB")
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
BytesSize instance
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValueError: If the value cannot be parsed
|
|
139
|
+
"""
|
|
140
|
+
if isinstance(value, BytesSize):
|
|
141
|
+
return value
|
|
142
|
+
|
|
143
|
+
if isinstance(value, (int, float)):
|
|
144
|
+
return cls(int(value))
|
|
145
|
+
|
|
146
|
+
if not isinstance(value, str):
|
|
147
|
+
raise ValueError(f"Cannot parse {type(value).__name__} as BytesSize")
|
|
148
|
+
|
|
149
|
+
# Clean the string
|
|
150
|
+
value = value.strip()
|
|
151
|
+
if not value:
|
|
152
|
+
raise ValueError("Cannot parse empty string as BytesSize")
|
|
153
|
+
|
|
154
|
+
# Try to match the pattern
|
|
155
|
+
match = cls._PATTERN.match(value)
|
|
156
|
+
if not match:
|
|
157
|
+
raise ValueError(f"Invalid byte size format: '{value}'")
|
|
158
|
+
|
|
159
|
+
number_str, suffix = match.groups()
|
|
160
|
+
|
|
161
|
+
# Parse the number part (handle different locales)
|
|
162
|
+
number_str = number_str.replace(",", "") # Remove thousands separators
|
|
163
|
+
try:
|
|
164
|
+
number = float(number_str)
|
|
165
|
+
except ValueError as e:
|
|
166
|
+
raise ValueError(f"Invalid number in byte size: '{number_str}'") from e
|
|
167
|
+
|
|
168
|
+
# Get the multiplier
|
|
169
|
+
suffix = suffix.upper() if suffix else "B"
|
|
170
|
+
multiplier = cls._SUFFIXES.get(suffix, 0)
|
|
171
|
+
|
|
172
|
+
if multiplier == 0:
|
|
173
|
+
raise ValueError(f"Unknown size suffix: '{suffix}'")
|
|
174
|
+
|
|
175
|
+
# Calculate the bytes
|
|
176
|
+
bytes_value = int(number * multiplier)
|
|
177
|
+
return cls(bytes_value)
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
def try_parse(cls, value: str | int | float | BytesSize) -> BytesSize | None:
|
|
181
|
+
"""
|
|
182
|
+
Try to parse a value into BytesSize, returning None on failure.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
value: Value to parse
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
BytesSize instance or None if parsing failed
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
return cls.parse(value)
|
|
192
|
+
except (ValueError, TypeError):
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def _format_size(bytes_value: int, precision: int = 0) -> str:
|
|
197
|
+
"""
|
|
198
|
+
Format bytes as human-readable string.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
bytes_value: Number of bytes
|
|
202
|
+
precision: Decimal precision for formatting
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Formatted string (e.g., "256MB", "1.5GB")
|
|
206
|
+
"""
|
|
207
|
+
if bytes_value == 0:
|
|
208
|
+
return "0B"
|
|
209
|
+
|
|
210
|
+
# Determine the appropriate unit
|
|
211
|
+
units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]
|
|
212
|
+
unit_index = 0
|
|
213
|
+
size = float(bytes_value)
|
|
214
|
+
|
|
215
|
+
while size >= 1024 and unit_index < len(units) - 1:
|
|
216
|
+
size /= 1024
|
|
217
|
+
unit_index += 1
|
|
218
|
+
|
|
219
|
+
# Format based on precision
|
|
220
|
+
if precision > 0:
|
|
221
|
+
return f"{size:.{precision}f}{units[unit_index]}"
|
|
222
|
+
elif size == int(size):
|
|
223
|
+
return f"{int(size)}{units[unit_index]}"
|
|
224
|
+
else:
|
|
225
|
+
# Auto precision (up to 2 decimal places, remove trailing zeros)
|
|
226
|
+
return f"{size:.2f}".rstrip("0").rstrip(".") + units[unit_index]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# Convenience constants
|
|
230
|
+
ZERO = BytesSize(0)
|
|
231
|
+
KB = BytesSize(1024)
|
|
232
|
+
MB = BytesSize(1024 * 1024)
|
|
233
|
+
GB = BytesSize(1024 * 1024 * 1024)
|
|
234
|
+
TB = BytesSize(1024 * 1024 * 1024 * 1024)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enterprise-grade Connection String implementation for RocketWelder SDK.
|
|
3
|
+
Matches C# ConnectionString struct functionality.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import contextlib
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum, Flag, auto
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .bytes_size import BytesSize
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Protocol(Flag):
|
|
17
|
+
"""Protocol flags for connection types."""
|
|
18
|
+
|
|
19
|
+
NONE = 0
|
|
20
|
+
SHM = auto() # Shared memory
|
|
21
|
+
MJPEG = auto() # Motion JPEG
|
|
22
|
+
HTTP = auto() # HTTP protocol
|
|
23
|
+
TCP = auto() # TCP protocol
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConnectionMode(Enum):
|
|
27
|
+
"""Connection mode for duplex/one-way communication."""
|
|
28
|
+
|
|
29
|
+
ONE_WAY = "OneWay"
|
|
30
|
+
DUPLEX = "Duplex"
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
"""String representation."""
|
|
34
|
+
return self.value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class ConnectionString:
|
|
39
|
+
"""
|
|
40
|
+
Immutable connection string representation for RocketWelder SDK.
|
|
41
|
+
|
|
42
|
+
Supports parsing connection strings like:
|
|
43
|
+
- shm://buffer_name?size=256MB&metadata=4KB&mode=Duplex
|
|
44
|
+
- mjpeg://192.168.1.100:8080
|
|
45
|
+
- mjpeg+http://camera.local:80
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
protocol: Protocol
|
|
49
|
+
host: str | None = None
|
|
50
|
+
port: int | None = None
|
|
51
|
+
buffer_name: str | None = None
|
|
52
|
+
buffer_size: BytesSize = field(default_factory=lambda: BytesSize.parse("256MB"))
|
|
53
|
+
metadata_size: BytesSize = field(default_factory=lambda: BytesSize.parse("4KB"))
|
|
54
|
+
connection_mode: ConnectionMode = ConnectionMode.ONE_WAY
|
|
55
|
+
timeout_ms: int = 5000
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def parse(cls, connection_string: str) -> ConnectionString:
|
|
59
|
+
"""
|
|
60
|
+
Parse a connection string into a ConnectionString object.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
connection_string: Connection string to parse
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
ConnectionString instance
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: If the connection string format is invalid
|
|
70
|
+
"""
|
|
71
|
+
if not connection_string:
|
|
72
|
+
raise ValueError("Connection string cannot be empty")
|
|
73
|
+
|
|
74
|
+
# Handle special protocols
|
|
75
|
+
if "://" not in connection_string:
|
|
76
|
+
raise ValueError(f"Invalid connection string format: {connection_string}")
|
|
77
|
+
|
|
78
|
+
# Split protocol and remainder
|
|
79
|
+
protocol_str, remainder = connection_string.split("://", 1)
|
|
80
|
+
protocol = cls._parse_protocol(protocol_str)
|
|
81
|
+
|
|
82
|
+
# Parse based on protocol type
|
|
83
|
+
if protocol == Protocol.SHM:
|
|
84
|
+
return cls._parse_shm(protocol, remainder)
|
|
85
|
+
elif bool(protocol & Protocol.MJPEG): # type: ignore[operator]
|
|
86
|
+
return cls._parse_mjpeg(protocol, remainder)
|
|
87
|
+
else:
|
|
88
|
+
raise ValueError(f"Unsupported protocol: {protocol_str}")
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def _parse_protocol(cls, protocol_str: str) -> Protocol:
|
|
92
|
+
"""Parse protocol string into Protocol flags."""
|
|
93
|
+
protocol_str = protocol_str.lower()
|
|
94
|
+
|
|
95
|
+
# Handle combined protocols (e.g., mjpeg+http)
|
|
96
|
+
if "+" in protocol_str:
|
|
97
|
+
parts = protocol_str.split("+")
|
|
98
|
+
result = Protocol.NONE
|
|
99
|
+
for part in parts:
|
|
100
|
+
result |= cls._get_single_protocol(part)
|
|
101
|
+
return result
|
|
102
|
+
else:
|
|
103
|
+
return cls._get_single_protocol(protocol_str)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def _get_single_protocol(cls, protocol_str: str) -> Protocol:
|
|
107
|
+
"""Get a single protocol from string."""
|
|
108
|
+
protocol_map = {
|
|
109
|
+
"shm": Protocol.SHM,
|
|
110
|
+
"mjpeg": Protocol.MJPEG,
|
|
111
|
+
"http": Protocol.HTTP,
|
|
112
|
+
"tcp": Protocol.TCP,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
protocol = protocol_map.get(protocol_str, Protocol.NONE)
|
|
116
|
+
if protocol == Protocol.NONE:
|
|
117
|
+
raise ValueError(f"Unknown protocol: {protocol_str}")
|
|
118
|
+
return protocol
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def _parse_shm(cls, protocol: Protocol, remainder: str) -> ConnectionString:
|
|
122
|
+
"""Parse shared memory connection string."""
|
|
123
|
+
# Split buffer name and query parameters
|
|
124
|
+
if "?" in remainder:
|
|
125
|
+
buffer_name, query_string = remainder.split("?", 1)
|
|
126
|
+
params = cls._parse_query_params(query_string)
|
|
127
|
+
else:
|
|
128
|
+
buffer_name = remainder
|
|
129
|
+
params = {}
|
|
130
|
+
|
|
131
|
+
# Parse parameters
|
|
132
|
+
buffer_size = BytesSize.parse("256MB")
|
|
133
|
+
metadata_size = BytesSize.parse("4KB")
|
|
134
|
+
connection_mode = ConnectionMode.ONE_WAY
|
|
135
|
+
timeout_ms = 5000
|
|
136
|
+
|
|
137
|
+
if "size" in params:
|
|
138
|
+
buffer_size = BytesSize.parse(params["size"])
|
|
139
|
+
if "metadata" in params:
|
|
140
|
+
metadata_size = BytesSize.parse(params["metadata"])
|
|
141
|
+
if "mode" in params:
|
|
142
|
+
mode_str = params["mode"].upper()
|
|
143
|
+
if mode_str == "DUPLEX":
|
|
144
|
+
connection_mode = ConnectionMode.DUPLEX
|
|
145
|
+
elif mode_str == "ONEWAY" or mode_str == "ONE_WAY":
|
|
146
|
+
connection_mode = ConnectionMode.ONE_WAY
|
|
147
|
+
if "timeout" in params:
|
|
148
|
+
with contextlib.suppress(ValueError):
|
|
149
|
+
timeout_ms = int(params["timeout"])
|
|
150
|
+
|
|
151
|
+
return cls(
|
|
152
|
+
protocol=protocol,
|
|
153
|
+
buffer_name=buffer_name,
|
|
154
|
+
buffer_size=buffer_size,
|
|
155
|
+
metadata_size=metadata_size,
|
|
156
|
+
connection_mode=connection_mode,
|
|
157
|
+
timeout_ms=timeout_ms,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def _parse_mjpeg(cls, protocol: Protocol, remainder: str) -> ConnectionString:
|
|
162
|
+
"""Parse MJPEG connection string."""
|
|
163
|
+
# Parse host:port format
|
|
164
|
+
if ":" in remainder:
|
|
165
|
+
host, port_str = remainder.rsplit(":", 1)
|
|
166
|
+
try:
|
|
167
|
+
port = int(port_str)
|
|
168
|
+
except ValueError as e:
|
|
169
|
+
raise ValueError(f"Invalid port number: {port_str}") from e
|
|
170
|
+
else:
|
|
171
|
+
host = remainder
|
|
172
|
+
# Default ports based on protocol
|
|
173
|
+
port = 80 if Protocol.HTTP in protocol else 8080
|
|
174
|
+
|
|
175
|
+
return cls(protocol=protocol, host=host, port=port)
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def _parse_query_params(cls, query_string: str) -> dict[str, str]:
|
|
179
|
+
"""Parse query parameters from string."""
|
|
180
|
+
params: dict[str, str] = {}
|
|
181
|
+
if not query_string:
|
|
182
|
+
return params
|
|
183
|
+
|
|
184
|
+
pairs = query_string.split("&")
|
|
185
|
+
for pair in pairs:
|
|
186
|
+
if "=" in pair:
|
|
187
|
+
key, value = pair.split("=", 1)
|
|
188
|
+
params[key.lower()] = value
|
|
189
|
+
|
|
190
|
+
return params
|
|
191
|
+
|
|
192
|
+
def __str__(self) -> str:
|
|
193
|
+
"""Convert to connection string format."""
|
|
194
|
+
# Format protocol
|
|
195
|
+
protocol_parts = []
|
|
196
|
+
if self.protocol & Protocol.SHM:
|
|
197
|
+
protocol_parts.append("shm")
|
|
198
|
+
if self.protocol & Protocol.MJPEG:
|
|
199
|
+
protocol_parts.append("mjpeg")
|
|
200
|
+
if self.protocol & Protocol.HTTP:
|
|
201
|
+
protocol_parts.append("http")
|
|
202
|
+
if self.protocol & Protocol.TCP:
|
|
203
|
+
protocol_parts.append("tcp")
|
|
204
|
+
|
|
205
|
+
protocol_str = "+".join(protocol_parts)
|
|
206
|
+
|
|
207
|
+
# Format based on protocol type
|
|
208
|
+
if self.protocol == Protocol.SHM:
|
|
209
|
+
params = [
|
|
210
|
+
f"size={self.buffer_size}",
|
|
211
|
+
f"metadata={self.metadata_size}",
|
|
212
|
+
f"mode={self.connection_mode.value}",
|
|
213
|
+
]
|
|
214
|
+
if self.timeout_ms != 5000:
|
|
215
|
+
params.append(f"timeout={self.timeout_ms}")
|
|
216
|
+
|
|
217
|
+
return f"{protocol_str}://{self.buffer_name}?{'&'.join(params)}"
|
|
218
|
+
else:
|
|
219
|
+
return f"{protocol_str}://{self.host}:{self.port}"
|
|
220
|
+
|
|
221
|
+
def to_dict(self) -> dict[str, Any]:
|
|
222
|
+
"""Convert to dictionary representation."""
|
|
223
|
+
return {
|
|
224
|
+
"protocol": str(self.protocol),
|
|
225
|
+
"host": self.host,
|
|
226
|
+
"port": self.port,
|
|
227
|
+
"buffer_name": self.buffer_name,
|
|
228
|
+
"buffer_size": str(self.buffer_size),
|
|
229
|
+
"metadata_size": str(self.metadata_size),
|
|
230
|
+
"connection_mode": self.connection_mode.value,
|
|
231
|
+
"timeout_ms": self.timeout_ms,
|
|
232
|
+
}
|