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.
Files changed (30) hide show
  1. {rocket_welder_sdk-1.1.0/rocket_welder_sdk.egg-info → rocket_welder_sdk-1.1.24}/PKG-INFO +3 -2
  2. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/pyproject.toml +3 -2
  3. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk/__init__.py +22 -0
  4. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk/controllers.py +125 -66
  5. rocket_welder_sdk-1.1.24/rocket_welder_sdk/gst_metadata.py +411 -0
  6. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk/rocket_welder_client.py +10 -13
  7. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24/rocket_welder_sdk.egg-info}/PKG-INFO +3 -2
  8. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk.egg-info/SOURCES.txt +6 -1
  9. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk.egg-info/requires.txt +2 -1
  10. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/setup.py +9 -2
  11. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/tests/test_controllers.py +11 -11
  12. rocket_welder_sdk-1.1.24/tests/test_external_controls_serialization.py +129 -0
  13. rocket_welder_sdk-1.1.24/tests/test_external_controls_serialization_v2.py +125 -0
  14. rocket_welder_sdk-1.1.24/tests/test_gst_metadata.py +205 -0
  15. rocket_welder_sdk-1.1.24/tests/test_icons.py +96 -0
  16. rocket_welder_sdk-1.1.24/tests/test_ui_controls.py +365 -0
  17. rocket_welder_sdk-1.1.24/tests/test_ui_service_happy_path.py +422 -0
  18. rocket_welder_sdk-1.1.0/rocket_welder_sdk/gst_metadata.py +0 -240
  19. rocket_welder_sdk-1.1.0/tests/test_gst_metadata.py +0 -305
  20. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/MANIFEST.in +0 -0
  21. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/README.md +0 -0
  22. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/logo.png +0 -0
  23. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk/bytes_size.py +0 -0
  24. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk/connection_string.py +0 -0
  25. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk/py.typed +0 -0
  26. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk.egg-info/dependency_links.txt +0 -0
  27. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/rocket_welder_sdk.egg-info/top_level.txt +0 -0
  28. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/setup.cfg +0 -0
  29. {rocket_welder_sdk-1.1.0 → rocket_welder_sdk-1.1.24}/tests/test_bytes_size.py +0 -0
  30. {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.0
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.10
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
- version = "1.1.0"
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.10",
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, logger: logging.Logger | None = None):
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._logger.debug(
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, logger=self._reader_logger)
126
+ self._reader = Reader(self._connection.buffer_name, config)
128
127
 
129
- self._logger.info(
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
- self._logger.debug("Stopping controller for buffer '%s'", self._connection.buffer_name)
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
- self._logger.info("Stopped controller for buffer '%s'", self._connection.buffer_name)
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 or "WriterDead" in error_type:
200
- self._logger.info(
201
- "Writer disconnected from buffer '%s'", self._connection.buffer_name
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._logger.error(
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._logger.error(
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
- self._logger.error(
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
- self._logger.error(
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
- self._logger.error("Fatal error in frame processing loop: %s", e)
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
- self._logger.debug(
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 == '\x00' * len(metadata_str):
277
- self._logger.warning("Metadata is empty or all zeros, skipping")
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
- self._logger.warning("No JSON found in metadata: %r", metadata_str[:100])
286
+ logger.warning("No JSON found in metadata: %r", metadata_str[:100])
284
287
  continue
285
-
288
+
286
289
  if json_start > 0:
287
- self._logger.debug("Skipping %d bytes before JSON", json_start)
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
- self._logger.info(
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
- self._logger.error("Failed to parse metadata: %s", e)
307
+ logger.error("Failed to parse metadata: %s", e)
305
308
  # Log the actual metadata content for debugging
306
309
  if metadata_bytes:
307
- self._logger.debug("Metadata content: %r", metadata_bytes[:200])
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 or "WriterDead" in error_type:
329
+ if "ReaderDead" in error_type:
320
330
  self._is_running = False
321
- self._logger.info(
322
- "Writer disconnected while waiting for first frame on buffer '%s'",
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
- self._logger.error(
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
- self._logger.error(
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
- self._logger.error("Unsupported channel count: %d", channels)
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
- self._logger.error("No GstCaps available for frame conversion")
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
- self._logger.error("Failed to convert frame to Mat: %s", e)
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(format="GRAY8", width=shape[1], height=shape[0])
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(format="BGR", width=shape[1], height=shape[0])
459
+ self._gst_caps = GstCaps.from_simple(width=shape[1], height=shape[0], format="BGR")
419
460
 
420
- self._logger.info("Inferred caps from frame: %s", self._gst_caps)
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, logger: logging.Logger | None = None):
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
- self._logger.info(
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
- self._logger.info("Stopping DuplexShmController")
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
- self._logger.info("DuplexShmController stopped")
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
- self._logger.info("Received metadata: %s", self._metadata)
593
+ logger.info("Received metadata: %s", self._metadata)
544
594
  except Exception as e:
545
- self._logger.warning("Failed to parse metadata: %s", e)
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(input_mat.shape)
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
- self._logger.debug(
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
- self._logger.error("Error processing duplex frame: %s", e)
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)."""