rocket-welder-sdk 1.1.0__tar.gz → 1.1.24__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.1.0/rocket_welder_sdk.egg-info → rocket_welder_sdk-1.1.24}/PKG-INFO +3 -2
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/pyproject.toml +3 -2
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk/__init__.py +22 -0
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk/controllers.py +125 -66
- rocket_welder_sdk-1.1.24/rocket_welder_sdk/gst_metadata.py +411 -0
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk/rocket_welder_client.py +10 -13
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24/rocket_welder_sdk.egg-info}/PKG-INFO +3 -2
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk.egg-info/SOURCES.txt +6 -1
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk.egg-info/requires.txt +2 -1
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/setup.py +9 -2
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/tests/test_controllers.py +11 -11
- rocket_welder_sdk-1.1.24/tests/test_external_controls_serialization.py +129 -0
- rocket_welder_sdk-1.1.24/tests/test_external_controls_serialization_v2.py +125 -0
- rocket_welder_sdk-1.1.24/tests/test_gst_metadata.py +205 -0
- rocket_welder_sdk-1.1.24/tests/test_icons.py +96 -0
- rocket_welder_sdk-1.1.24/tests/test_ui_controls.py +365 -0
- rocket_welder_sdk-1.1.24/tests/test_ui_service_happy_path.py +422 -0
- rocket_welder_sdk-1.1.0/rocket_welder_sdk/gst_metadata.py +0 -240
- rocket_welder_sdk-1.1.0/tests/test_gst_metadata.py +0 -305
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/MANIFEST.in +0 -0
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/README.md +0 -0
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/logo.png +0 -0
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk/bytes_size.py +0 -0
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk/connection_string.py +0 -0
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk/py.typed +0 -0
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk.egg-info/dependency_links.txt +0 -0
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk.egg-info/top_level.txt +0 -0
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/setup.cfg +0 -0
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/tests/test_bytes_size.py +0 -0
- {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/tests/test_connection_string.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rocket-welder-sdk
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.24
|
|
4
4
|
Summary: High-performance video streaming SDK for RocketWelder services using ZeroBuffer IPC
|
|
5
5
|
Home-page: https://github.com/modelingevolution/rocket-welder-sdk
|
|
6
6
|
Author: ModelingEvolution
|
|
@@ -27,7 +27,8 @@ Requires-Python: >=3.8
|
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
Requires-Dist: numpy>=1.20.0
|
|
29
29
|
Requires-Dist: opencv-python>=4.5.0
|
|
30
|
-
Requires-Dist: zerobuffer-ipc>=1.1.
|
|
30
|
+
Requires-Dist: zerobuffer-ipc>=1.1.17
|
|
31
|
+
Requires-Dist: pydantic>=2.5.0
|
|
31
32
|
Provides-Extra: dev
|
|
32
33
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
33
34
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rocket-welder-sdk"
|
|
7
|
-
|
|
7
|
+
dynamic = ["version"]
|
|
8
8
|
description = "High-performance video streaming SDK for RocketWelder services using ZeroBuffer IPC"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -34,7 +34,8 @@ classifiers = [
|
|
|
34
34
|
dependencies = [
|
|
35
35
|
"numpy>=1.20.0",
|
|
36
36
|
"opencv-python>=4.5.0",
|
|
37
|
-
"zerobuffer-ipc>=1.1.
|
|
37
|
+
"zerobuffer-ipc>=1.1.17",
|
|
38
|
+
"pydantic>=2.5.0",
|
|
38
39
|
]
|
|
39
40
|
|
|
40
41
|
[project.optional-dependencies]
|
|
@@ -4,6 +4,9 @@ RocketWelder SDK - Enterprise-grade Python client library for video streaming se
|
|
|
4
4
|
High-performance video streaming using shared memory (ZeroBuffer) for zero-copy operations.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
|
|
7
10
|
from .bytes_size import BytesSize
|
|
8
11
|
from .connection_string import ConnectionMode, ConnectionString, Protocol
|
|
9
12
|
from .controllers import DuplexShmController, IController, OneWayShmController
|
|
@@ -15,6 +18,25 @@ Client = RocketWelderClient
|
|
|
15
18
|
|
|
16
19
|
__version__ = "1.1.0"
|
|
17
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
|
|
39
|
+
|
|
18
40
|
__all__ = [
|
|
19
41
|
# Core types
|
|
20
42
|
"BytesSize",
|
|
@@ -14,6 +14,7 @@ from typing import Any, Callable
|
|
|
14
14
|
import numpy as np
|
|
15
15
|
from zerobuffer import BufferConfig, Frame, Reader, Writer
|
|
16
16
|
from zerobuffer.duplex import DuplexChannelFactory, IImmutableDuplexServer
|
|
17
|
+
from zerobuffer.exceptions import WriterDeadException
|
|
17
18
|
|
|
18
19
|
from .connection_string import ConnectionMode, ConnectionString, Protocol
|
|
19
20
|
from .gst_metadata import GstCaps, GstMetadata
|
|
@@ -21,6 +22,9 @@ from .gst_metadata import GstCaps, GstMetadata
|
|
|
21
22
|
# Type alias for OpenCV Mat
|
|
22
23
|
Mat = np.ndarray[Any, Any]
|
|
23
24
|
|
|
25
|
+
# Module logger
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
24
28
|
|
|
25
29
|
class IController(ABC):
|
|
26
30
|
"""Abstract base class for controllers."""
|
|
@@ -63,13 +67,12 @@ class OneWayShmController(IController):
|
|
|
63
67
|
as a zerosink, allowing zero-copy frame reception.
|
|
64
68
|
"""
|
|
65
69
|
|
|
66
|
-
def __init__(self, connection: ConnectionString
|
|
70
|
+
def __init__(self, connection: ConnectionString):
|
|
67
71
|
"""
|
|
68
72
|
Initialize the one-way controller.
|
|
69
73
|
|
|
70
74
|
Args:
|
|
71
75
|
connection: Connection string configuration
|
|
72
|
-
logger: Optional logger instance
|
|
73
76
|
"""
|
|
74
77
|
if connection.protocol != Protocol.SHM:
|
|
75
78
|
raise ValueError(
|
|
@@ -77,8 +80,6 @@ class OneWayShmController(IController):
|
|
|
77
80
|
)
|
|
78
81
|
|
|
79
82
|
self._connection = connection
|
|
80
|
-
self._logger = logger or logging.getLogger(__name__)
|
|
81
|
-
self._reader_logger = logging.getLogger(f"{__name__}.Reader")
|
|
82
83
|
self._reader: Reader | None = None
|
|
83
84
|
self._gst_caps: GstCaps | None = None
|
|
84
85
|
self._metadata: GstMetadata | None = None
|
|
@@ -108,9 +109,7 @@ class OneWayShmController(IController):
|
|
|
108
109
|
if self._is_running:
|
|
109
110
|
raise RuntimeError("Controller is already running")
|
|
110
111
|
|
|
111
|
-
self.
|
|
112
|
-
"Starting OneWayShmController for buffer '%s'", self._connection.buffer_name
|
|
113
|
-
)
|
|
112
|
+
logger.debug("Starting OneWayShmController for buffer '%s'", self._connection.buffer_name)
|
|
114
113
|
self._is_running = True
|
|
115
114
|
self._cancellation_token = cancellation_token
|
|
116
115
|
|
|
@@ -124,9 +123,9 @@ class OneWayShmController(IController):
|
|
|
124
123
|
# Pass logger to Reader for better debugging
|
|
125
124
|
if not self._connection.buffer_name:
|
|
126
125
|
raise ValueError("Buffer name is required for shared memory connection")
|
|
127
|
-
self._reader = Reader(self._connection.buffer_name, config
|
|
126
|
+
self._reader = Reader(self._connection.buffer_name, config)
|
|
128
127
|
|
|
129
|
-
|
|
128
|
+
logger.info(
|
|
130
129
|
"Created shared memory buffer '%s' with size %s and metadata %s",
|
|
131
130
|
self._connection.buffer_name,
|
|
132
131
|
self._connection.buffer_size,
|
|
@@ -146,7 +145,7 @@ class OneWayShmController(IController):
|
|
|
146
145
|
if not self._is_running:
|
|
147
146
|
return
|
|
148
147
|
|
|
149
|
-
|
|
148
|
+
logger.debug("Stopping controller for buffer '%s'", self._connection.buffer_name)
|
|
150
149
|
self._is_running = False
|
|
151
150
|
|
|
152
151
|
# Wait for worker thread to finish
|
|
@@ -160,7 +159,7 @@ class OneWayShmController(IController):
|
|
|
160
159
|
self._reader = None
|
|
161
160
|
|
|
162
161
|
self._worker_thread = None
|
|
163
|
-
|
|
162
|
+
logger.info("Stopped controller for buffer '%s'", self._connection.buffer_name)
|
|
164
163
|
|
|
165
164
|
def _process_frames(self, on_frame: Callable[[Mat], None]) -> None:
|
|
166
165
|
"""
|
|
@@ -193,35 +192,39 @@ class OneWayShmController(IController):
|
|
|
193
192
|
if mat is not None:
|
|
194
193
|
on_frame(mat)
|
|
195
194
|
|
|
195
|
+
except WriterDeadException:
|
|
196
|
+
# Writer has disconnected gracefully
|
|
197
|
+
logger.info(
|
|
198
|
+
"Writer disconnected gracefully from buffer '%s'",
|
|
199
|
+
self._connection.buffer_name,
|
|
200
|
+
)
|
|
201
|
+
self._is_running = False
|
|
202
|
+
break
|
|
196
203
|
except Exception as e:
|
|
197
204
|
# Log specific error types like C#
|
|
198
205
|
error_type = type(e).__name__
|
|
199
|
-
if "ReaderDead" in error_type
|
|
200
|
-
|
|
201
|
-
"
|
|
206
|
+
if "ReaderDead" in error_type:
|
|
207
|
+
logger.info(
|
|
208
|
+
"Reader disconnected from buffer '%s'", self._connection.buffer_name
|
|
202
209
|
)
|
|
203
210
|
self._is_running = False
|
|
204
211
|
break
|
|
205
212
|
elif "BufferFull" in error_type:
|
|
206
|
-
self.
|
|
207
|
-
"Buffer full on '%s': %s", self._connection.buffer_name, e
|
|
208
|
-
)
|
|
213
|
+
logger.error("Buffer full on '%s': %s", self._connection.buffer_name, e)
|
|
209
214
|
if not self._is_running:
|
|
210
215
|
break
|
|
211
216
|
elif "FrameTooLarge" in error_type:
|
|
212
|
-
self.
|
|
213
|
-
"Frame too large on '%s': %s", self._connection.buffer_name, e
|
|
214
|
-
)
|
|
217
|
+
logger.error("Frame too large on '%s': %s", self._connection.buffer_name, e)
|
|
215
218
|
if not self._is_running:
|
|
216
219
|
break
|
|
217
220
|
elif "ZeroBuffer" in error_type:
|
|
218
|
-
|
|
221
|
+
logger.error(
|
|
219
222
|
"ZeroBuffer error on '%s': %s", self._connection.buffer_name, e
|
|
220
223
|
)
|
|
221
224
|
if not self._is_running:
|
|
222
225
|
break
|
|
223
226
|
else:
|
|
224
|
-
|
|
227
|
+
logger.error(
|
|
225
228
|
"Unexpected error processing frame from buffer '%s': %s",
|
|
226
229
|
self._connection.buffer_name,
|
|
227
230
|
e,
|
|
@@ -230,7 +233,7 @@ class OneWayShmController(IController):
|
|
|
230
233
|
break
|
|
231
234
|
|
|
232
235
|
except Exception as e:
|
|
233
|
-
|
|
236
|
+
logger.error("Fatal error in frame processing loop: %s", e)
|
|
234
237
|
self._is_running = False
|
|
235
238
|
|
|
236
239
|
def _on_first_frame(self, on_frame: Callable[[Mat], None]) -> None:
|
|
@@ -258,53 +261,53 @@ class OneWayShmController(IController):
|
|
|
258
261
|
if metadata_bytes:
|
|
259
262
|
try:
|
|
260
263
|
# Log raw metadata for debugging
|
|
261
|
-
|
|
264
|
+
logger.debug(
|
|
262
265
|
"Raw metadata: %d bytes, type=%s, first 100 bytes: %s",
|
|
263
266
|
len(metadata_bytes),
|
|
264
267
|
type(metadata_bytes),
|
|
265
|
-
bytes(metadata_bytes[:min(100, len(metadata_bytes))]),
|
|
268
|
+
bytes(metadata_bytes[: min(100, len(metadata_bytes))]),
|
|
266
269
|
)
|
|
267
|
-
|
|
270
|
+
|
|
268
271
|
# Convert memoryview to bytes if needed
|
|
269
272
|
if isinstance(metadata_bytes, memoryview):
|
|
270
273
|
metadata_bytes = bytes(metadata_bytes)
|
|
271
|
-
|
|
274
|
+
|
|
272
275
|
# Decode UTF-8
|
|
273
276
|
metadata_str = metadata_bytes.decode("utf-8")
|
|
274
|
-
|
|
277
|
+
|
|
275
278
|
# Check if metadata is empty or all zeros
|
|
276
|
-
if not metadata_str or metadata_str ==
|
|
277
|
-
|
|
279
|
+
if not metadata_str or metadata_str == "\x00" * len(metadata_str):
|
|
280
|
+
logger.warning("Metadata is empty or all zeros, skipping")
|
|
278
281
|
continue
|
|
279
|
-
|
|
282
|
+
|
|
280
283
|
# Find the start of JSON (skip any null bytes at the beginning)
|
|
281
|
-
json_start = metadata_str.find(
|
|
284
|
+
json_start = metadata_str.find("{")
|
|
282
285
|
if json_start == -1:
|
|
283
|
-
|
|
286
|
+
logger.warning("No JSON found in metadata: %r", metadata_str[:100])
|
|
284
287
|
continue
|
|
285
|
-
|
|
288
|
+
|
|
286
289
|
if json_start > 0:
|
|
287
|
-
|
|
290
|
+
logger.debug("Skipping %d bytes before JSON", json_start)
|
|
288
291
|
metadata_str = metadata_str[json_start:]
|
|
289
|
-
|
|
292
|
+
|
|
290
293
|
# Find the end of JSON (handle null padding)
|
|
291
|
-
json_end = metadata_str.rfind(
|
|
294
|
+
json_end = metadata_str.rfind("}")
|
|
292
295
|
if json_end != -1 and json_end < len(metadata_str) - 1:
|
|
293
|
-
metadata_str = metadata_str[:json_end + 1]
|
|
294
|
-
|
|
296
|
+
metadata_str = metadata_str[: json_end + 1]
|
|
297
|
+
|
|
295
298
|
metadata_json = json.loads(metadata_str)
|
|
296
299
|
self._metadata = GstMetadata.from_json(metadata_json)
|
|
297
300
|
self._gst_caps = self._metadata.caps
|
|
298
|
-
|
|
301
|
+
logger.info(
|
|
299
302
|
"Received metadata from buffer '%s': %s",
|
|
300
303
|
self._connection.buffer_name,
|
|
301
304
|
self._gst_caps,
|
|
302
305
|
)
|
|
303
306
|
except Exception as e:
|
|
304
|
-
|
|
307
|
+
logger.error("Failed to parse metadata: %s", e)
|
|
305
308
|
# Log the actual metadata content for debugging
|
|
306
309
|
if metadata_bytes:
|
|
307
|
-
|
|
310
|
+
logger.debug("Metadata content: %r", metadata_bytes[:200])
|
|
308
311
|
# Don't continue without metadata
|
|
309
312
|
continue
|
|
310
313
|
|
|
@@ -314,17 +317,24 @@ class OneWayShmController(IController):
|
|
|
314
317
|
on_frame(mat)
|
|
315
318
|
return # Successfully processed first frame
|
|
316
319
|
|
|
320
|
+
except WriterDeadException:
|
|
321
|
+
self._is_running = False
|
|
322
|
+
logger.info(
|
|
323
|
+
"Writer disconnected gracefully while waiting for first frame on buffer '%s'",
|
|
324
|
+
self._connection.buffer_name,
|
|
325
|
+
)
|
|
326
|
+
raise
|
|
317
327
|
except Exception as e:
|
|
318
328
|
error_type = type(e).__name__
|
|
319
|
-
if "ReaderDead" in error_type
|
|
329
|
+
if "ReaderDead" in error_type:
|
|
320
330
|
self._is_running = False
|
|
321
|
-
|
|
322
|
-
"
|
|
331
|
+
logger.info(
|
|
332
|
+
"Reader disconnected while waiting for first frame on buffer '%s'",
|
|
323
333
|
self._connection.buffer_name,
|
|
324
334
|
)
|
|
325
335
|
raise
|
|
326
336
|
else:
|
|
327
|
-
|
|
337
|
+
logger.error(
|
|
328
338
|
"Error waiting for first frame on buffer '%s': %s",
|
|
329
339
|
self._connection.buffer_name,
|
|
330
340
|
e,
|
|
@@ -367,7 +377,7 @@ class OneWayShmController(IController):
|
|
|
367
377
|
# Check data size matches expected
|
|
368
378
|
expected_size = height * width * channels
|
|
369
379
|
if len(data) != expected_size:
|
|
370
|
-
|
|
380
|
+
logger.error(
|
|
371
381
|
"Data size mismatch. Expected %d bytes for %dx%d with %d channels, got %d",
|
|
372
382
|
expected_size,
|
|
373
383
|
width,
|
|
@@ -386,17 +396,48 @@ class OneWayShmController(IController):
|
|
|
386
396
|
elif channels == 4:
|
|
387
397
|
mat = data.reshape((height, width, 4))
|
|
388
398
|
else:
|
|
389
|
-
|
|
399
|
+
logger.error("Unsupported channel count: %d", channels)
|
|
390
400
|
return None
|
|
391
401
|
|
|
392
402
|
return mat # type: ignore[no-any-return]
|
|
393
403
|
|
|
394
|
-
# No caps available
|
|
395
|
-
|
|
404
|
+
# No caps available - try to infer from frame size
|
|
405
|
+
logger.warning("No GstCaps available, attempting to infer from frame size")
|
|
406
|
+
|
|
407
|
+
# Try common resolutions
|
|
408
|
+
frame_size = len(frame.data)
|
|
409
|
+
common_resolutions = [
|
|
410
|
+
(640, 480, 3), # VGA RGB
|
|
411
|
+
(640, 480, 4), # VGA RGBA
|
|
412
|
+
(1280, 720, 3), # 720p RGB
|
|
413
|
+
(1920, 1080, 3), # 1080p RGB
|
|
414
|
+
(640, 480, 1), # VGA Grayscale
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
for width, height, channels in common_resolutions:
|
|
418
|
+
if frame_size == width * height * channels:
|
|
419
|
+
logger.info(f"Inferred resolution: {width}x{height} with {channels} channels")
|
|
420
|
+
|
|
421
|
+
# Create caps for future use
|
|
422
|
+
format_str = "RGB" if channels == 3 else "RGBA" if channels == 4 else "GRAY8"
|
|
423
|
+
self._gst_caps = GstCaps.from_simple(
|
|
424
|
+
width=width, height=height, format=format_str
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
# Create Mat
|
|
428
|
+
data = np.frombuffer(frame.data, dtype=np.uint8)
|
|
429
|
+
if channels == 3:
|
|
430
|
+
return data.reshape((height, width, 3)) # type: ignore[no-any-return]
|
|
431
|
+
elif channels == 1:
|
|
432
|
+
return data.reshape((height, width)) # type: ignore[no-any-return]
|
|
433
|
+
elif channels == 4:
|
|
434
|
+
return data.reshape((height, width, 4)) # type: ignore[no-any-return]
|
|
435
|
+
|
|
436
|
+
logger.error(f"Could not infer resolution for frame size {frame_size}")
|
|
396
437
|
return None
|
|
397
438
|
|
|
398
439
|
except Exception as e:
|
|
399
|
-
|
|
440
|
+
logger.error("Failed to convert frame to Mat: %s", e)
|
|
400
441
|
return None
|
|
401
442
|
|
|
402
443
|
def _infer_caps_from_frame(self, mat: Mat) -> None:
|
|
@@ -412,12 +453,12 @@ class OneWayShmController(IController):
|
|
|
412
453
|
shape = mat.shape
|
|
413
454
|
if len(shape) == 2:
|
|
414
455
|
# Grayscale
|
|
415
|
-
self._gst_caps = GstCaps(
|
|
456
|
+
self._gst_caps = GstCaps.from_simple(width=shape[1], height=shape[0], format="GRAY8")
|
|
416
457
|
elif len(shape) == 3:
|
|
417
458
|
# Color image
|
|
418
|
-
self._gst_caps = GstCaps(
|
|
459
|
+
self._gst_caps = GstCaps.from_simple(width=shape[1], height=shape[0], format="BGR")
|
|
419
460
|
|
|
420
|
-
|
|
461
|
+
logger.info("Inferred caps from frame: %s", self._gst_caps)
|
|
421
462
|
|
|
422
463
|
|
|
423
464
|
class DuplexShmController(IController):
|
|
@@ -428,13 +469,12 @@ class DuplexShmController(IController):
|
|
|
428
469
|
sending processed frames to another buffer.
|
|
429
470
|
"""
|
|
430
471
|
|
|
431
|
-
def __init__(self, connection: ConnectionString
|
|
472
|
+
def __init__(self, connection: ConnectionString):
|
|
432
473
|
"""
|
|
433
474
|
Initialize the duplex controller.
|
|
434
475
|
|
|
435
476
|
Args:
|
|
436
477
|
connection: Connection string configuration
|
|
437
|
-
logger: Optional logger instance
|
|
438
478
|
"""
|
|
439
479
|
if connection.protocol != Protocol.SHM:
|
|
440
480
|
raise ValueError(
|
|
@@ -447,7 +487,6 @@ class DuplexShmController(IController):
|
|
|
447
487
|
)
|
|
448
488
|
|
|
449
489
|
self._connection = connection
|
|
450
|
-
self._logger = logger or logging.getLogger(__name__)
|
|
451
490
|
self._duplex_server: IImmutableDuplexServer | None = None
|
|
452
491
|
self._gst_caps: GstCaps | None = None
|
|
453
492
|
self._metadata: GstMetadata | None = None
|
|
@@ -498,7 +537,7 @@ class DuplexShmController(IController):
|
|
|
498
537
|
self._connection.buffer_name, config, timeout_seconds
|
|
499
538
|
)
|
|
500
539
|
|
|
501
|
-
|
|
540
|
+
logger.info(
|
|
502
541
|
"Starting duplex server for channel '%s' with size %s and metadata %s",
|
|
503
542
|
self._connection.buffer_name,
|
|
504
543
|
self._connection.buffer_size,
|
|
@@ -514,7 +553,7 @@ class DuplexShmController(IController):
|
|
|
514
553
|
if not self._is_running:
|
|
515
554
|
return
|
|
516
555
|
|
|
517
|
-
|
|
556
|
+
logger.info("Stopping DuplexShmController")
|
|
518
557
|
self._is_running = False
|
|
519
558
|
|
|
520
559
|
# Stop the duplex server
|
|
@@ -522,7 +561,7 @@ class DuplexShmController(IController):
|
|
|
522
561
|
self._duplex_server.stop()
|
|
523
562
|
self._duplex_server = None
|
|
524
563
|
|
|
525
|
-
|
|
564
|
+
logger.info("DuplexShmController stopped")
|
|
526
565
|
|
|
527
566
|
def _on_metadata(self, metadata_bytes: bytes | memoryview) -> None:
|
|
528
567
|
"""
|
|
@@ -531,18 +570,29 @@ class DuplexShmController(IController):
|
|
|
531
570
|
Args:
|
|
532
571
|
metadata_bytes: Raw metadata bytes or memoryview
|
|
533
572
|
"""
|
|
573
|
+
logger.debug(
|
|
574
|
+
"_on_metadata called with %d bytes", len(metadata_bytes) if metadata_bytes else 0
|
|
575
|
+
)
|
|
534
576
|
try:
|
|
535
577
|
# Convert memoryview to bytes if needed
|
|
536
578
|
if isinstance(metadata_bytes, memoryview):
|
|
537
579
|
metadata_bytes = bytes(metadata_bytes)
|
|
538
|
-
|
|
580
|
+
|
|
581
|
+
# Log raw bytes for debugging
|
|
582
|
+
logger.debug(
|
|
583
|
+
"Raw metadata bytes (first 100): %r",
|
|
584
|
+
metadata_bytes[: min(100, len(metadata_bytes))],
|
|
585
|
+
)
|
|
586
|
+
|
|
539
587
|
metadata_str = metadata_bytes.decode("utf-8")
|
|
588
|
+
logger.debug("Decoded metadata string: %r", metadata_str[: min(200, len(metadata_str))])
|
|
589
|
+
|
|
540
590
|
metadata_json = json.loads(metadata_str)
|
|
541
591
|
self._metadata = GstMetadata.from_json(metadata_json)
|
|
542
592
|
self._gst_caps = self._metadata.caps
|
|
543
|
-
|
|
593
|
+
logger.info("Received metadata: %s", self._metadata)
|
|
544
594
|
except Exception as e:
|
|
545
|
-
|
|
595
|
+
logger.error("Failed to parse metadata: %s", e, exc_info=True)
|
|
546
596
|
|
|
547
597
|
def _process_duplex_frame(self, request_frame: Frame, response_writer: Writer) -> None:
|
|
548
598
|
"""
|
|
@@ -552,8 +602,14 @@ class DuplexShmController(IController):
|
|
|
552
602
|
request_frame: Input frame from the request
|
|
553
603
|
response_writer: Writer for the response frame
|
|
554
604
|
"""
|
|
605
|
+
logger.debug(
|
|
606
|
+
"_process_duplex_frame called, frame_count=%d, has_gst_caps=%s",
|
|
607
|
+
self._frame_count,
|
|
608
|
+
self._gst_caps is not None,
|
|
609
|
+
)
|
|
555
610
|
try:
|
|
556
611
|
if not self._on_frame_callback:
|
|
612
|
+
logger.warning("No frame callback set")
|
|
557
613
|
return
|
|
558
614
|
|
|
559
615
|
self._frame_count += 1
|
|
@@ -561,6 +617,7 @@ class DuplexShmController(IController):
|
|
|
561
617
|
# Convert input frame to Mat
|
|
562
618
|
input_mat = self._frame_to_mat(request_frame)
|
|
563
619
|
if input_mat is None:
|
|
620
|
+
logger.error("Failed to convert frame to Mat, gst_caps=%s", self._gst_caps)
|
|
564
621
|
return
|
|
565
622
|
|
|
566
623
|
# Get buffer for output frame - use context manager for RAII
|
|
@@ -585,7 +642,9 @@ class DuplexShmController(IController):
|
|
|
585
642
|
)
|
|
586
643
|
else:
|
|
587
644
|
# Use same shape as input
|
|
588
|
-
output_mat = np.frombuffer(output_buffer, dtype=np.uint8).reshape(
|
|
645
|
+
output_mat = np.frombuffer(output_buffer, dtype=np.uint8).reshape(
|
|
646
|
+
input_mat.shape
|
|
647
|
+
)
|
|
589
648
|
|
|
590
649
|
# Call user's processing function
|
|
591
650
|
self._on_frame_callback(input_mat, output_mat)
|
|
@@ -593,7 +652,7 @@ class DuplexShmController(IController):
|
|
|
593
652
|
# Commit the response frame after buffer is released
|
|
594
653
|
response_writer.commit_frame()
|
|
595
654
|
|
|
596
|
-
|
|
655
|
+
logger.debug(
|
|
597
656
|
"Processed duplex frame %d (%dx%d)",
|
|
598
657
|
self._frame_count,
|
|
599
658
|
input_mat.shape[1],
|
|
@@ -601,7 +660,7 @@ class DuplexShmController(IController):
|
|
|
601
660
|
)
|
|
602
661
|
|
|
603
662
|
except Exception as e:
|
|
604
|
-
|
|
663
|
+
logger.error("Error processing duplex frame: %s", e)
|
|
605
664
|
|
|
606
665
|
def _frame_to_mat(self, frame: Frame) -> Mat | None:
|
|
607
666
|
"""Convert frame to OpenCV Mat (reuse from OneWayShmController)."""
|