rocket-welder-sdk 1.1.36.dev14__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.
Files changed (40) hide show
  1. rocket_welder_sdk/__init__.py +95 -0
  2. rocket_welder_sdk/bytes_size.py +234 -0
  3. rocket_welder_sdk/connection_string.py +291 -0
  4. rocket_welder_sdk/controllers.py +831 -0
  5. rocket_welder_sdk/external_controls/__init__.py +30 -0
  6. rocket_welder_sdk/external_controls/contracts.py +100 -0
  7. rocket_welder_sdk/external_controls/contracts_old.py +105 -0
  8. rocket_welder_sdk/frame_metadata.py +138 -0
  9. rocket_welder_sdk/gst_metadata.py +411 -0
  10. rocket_welder_sdk/high_level/__init__.py +54 -0
  11. rocket_welder_sdk/high_level/client.py +235 -0
  12. rocket_welder_sdk/high_level/connection_strings.py +331 -0
  13. rocket_welder_sdk/high_level/data_context.py +169 -0
  14. rocket_welder_sdk/high_level/frame_sink_factory.py +118 -0
  15. rocket_welder_sdk/high_level/schema.py +195 -0
  16. rocket_welder_sdk/high_level/transport_protocol.py +238 -0
  17. rocket_welder_sdk/keypoints_protocol.py +642 -0
  18. rocket_welder_sdk/opencv_controller.py +278 -0
  19. rocket_welder_sdk/periodic_timer.py +303 -0
  20. rocket_welder_sdk/py.typed +2 -0
  21. rocket_welder_sdk/rocket_welder_client.py +497 -0
  22. rocket_welder_sdk/segmentation_result.py +420 -0
  23. rocket_welder_sdk/session_id.py +238 -0
  24. rocket_welder_sdk/transport/__init__.py +31 -0
  25. rocket_welder_sdk/transport/frame_sink.py +122 -0
  26. rocket_welder_sdk/transport/frame_source.py +74 -0
  27. rocket_welder_sdk/transport/nng_transport.py +197 -0
  28. rocket_welder_sdk/transport/stream_transport.py +193 -0
  29. rocket_welder_sdk/transport/tcp_transport.py +154 -0
  30. rocket_welder_sdk/transport/unix_socket_transport.py +339 -0
  31. rocket_welder_sdk/ui/__init__.py +48 -0
  32. rocket_welder_sdk/ui/controls.py +362 -0
  33. rocket_welder_sdk/ui/icons.py +21628 -0
  34. rocket_welder_sdk/ui/ui_events_projection.py +226 -0
  35. rocket_welder_sdk/ui/ui_service.py +358 -0
  36. rocket_welder_sdk/ui/value_types.py +72 -0
  37. rocket_welder_sdk-1.1.36.dev14.dist-info/METADATA +845 -0
  38. rocket_welder_sdk-1.1.36.dev14.dist-info/RECORD +40 -0
  39. rocket_welder_sdk-1.1.36.dev14.dist-info/WHEEL +5 -0
  40. rocket_welder_sdk-1.1.36.dev14.dist-info/top_level.txt +1 -0
@@ -0,0 +1,497 @@
1
+ """
2
+ Enterprise-grade RocketWelder client for video streaming.
3
+ Main entry point for the RocketWelder SDK.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import queue
10
+ import threading
11
+ from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union
12
+
13
+ import numpy as np
14
+
15
+ from .connection_string import ConnectionMode, ConnectionString, Protocol
16
+ from .controllers import DuplexShmController, IController, OneWayShmController
17
+ from .frame_metadata import FrameMetadata # noqa: TC001 - used at runtime in callbacks
18
+ from .opencv_controller import OpenCvController
19
+ from .session_id import (
20
+ get_configured_nng_urls,
21
+ get_nng_urls_from_env,
22
+ has_explicit_nng_urls,
23
+ )
24
+ from .transport.nng_transport import NngFrameSink
25
+
26
+ if TYPE_CHECKING:
27
+ import numpy.typing as npt
28
+
29
+ from .gst_metadata import GstMetadata
30
+
31
+ # Use numpy array type for Mat - OpenCV Mat is essentially a numpy array
32
+ Mat = npt.NDArray[np.uint8]
33
+ else:
34
+ Mat = np.ndarray # type: ignore[misc]
35
+
36
+ # Module logger
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class RocketWelderClient:
41
+ """
42
+ Main client for RocketWelder video streaming services.
43
+
44
+ Provides a unified interface for different connection types and protocols.
45
+ """
46
+
47
+ def __init__(self, connection: Union[str, ConnectionString]):
48
+ """
49
+ Initialize the RocketWelder client.
50
+
51
+ Args:
52
+ connection: Connection string or ConnectionString object
53
+ """
54
+ if isinstance(connection, str):
55
+ self._connection = ConnectionString.parse(connection)
56
+ else:
57
+ self._connection = connection
58
+
59
+ self._controller: Optional[IController] = None
60
+ self._lock = threading.Lock()
61
+
62
+ # NNG publishers for streaming results (auto-created if SessionId env var is set)
63
+ self._nng_publishers: dict[str, NngFrameSink] = {}
64
+
65
+ # Preview support
66
+ self._preview_enabled = (
67
+ self._connection.parameters.get("preview", "false").lower() == "true"
68
+ )
69
+ self._preview_queue: queue.Queue[Optional[Mat]] = queue.Queue(maxsize=2) # type: ignore[valid-type] # Small buffer
70
+ self._preview_window_name = "RocketWelder Preview"
71
+ self._original_callback: Any = None
72
+
73
+ @property
74
+ def connection(self) -> ConnectionString:
75
+ """Get the connection configuration."""
76
+ return self._connection
77
+
78
+ @property
79
+ def is_running(self) -> bool:
80
+ """Check if the client is running."""
81
+ with self._lock:
82
+ return self._controller is not None and self._controller.is_running
83
+
84
+ @property
85
+ def nng_publishers(self) -> dict[str, NngFrameSink]:
86
+ """Get NNG publishers for streaming results.
87
+
88
+ Returns:
89
+ Dictionary with 'segmentation', 'keypoints', 'actions' keys.
90
+ Empty if SessionId env var was not set at startup.
91
+
92
+ Example:
93
+ client.nng_publishers["segmentation"].write_frame(seg_data)
94
+ """
95
+ return self._nng_publishers
96
+
97
+ def _create_nng_publishers(self) -> None:
98
+ """Create NNG publishers for result streaming.
99
+
100
+ URLs are read from environment variables (preferred) or derived from SessionId (fallback).
101
+
102
+ Priority:
103
+ 1. Explicit URLs: SEGMENTATION_SINK_URL, KEYPOINTS_SINK_URL, ACTIONS_SINK_URL
104
+ 2. Derived from SessionId environment variable (backwards compatibility)
105
+ """
106
+ try:
107
+ urls = get_configured_nng_urls()
108
+
109
+ for name, url in urls.items():
110
+ sink = NngFrameSink.create_publisher(url)
111
+ self._nng_publishers[name] = sink
112
+ logger.info("NNG publisher ready: %s at %s", name, url)
113
+
114
+ # Log configuration summary
115
+ logger.info(
116
+ "NNG publishers configured: seg=%s, kp=%s, actions=%s",
117
+ urls.get("segmentation", "(not configured)"),
118
+ urls.get("keypoints", "(not configured)"),
119
+ urls.get("actions", "(not configured)"),
120
+ )
121
+ except ValueError as ex:
122
+ # No URLs configured - this is expected for containers that don't publish results
123
+ logger.debug("NNG publishers not configured: %s", ex)
124
+ except Exception as ex:
125
+ logger.warning("Failed to create NNG publishers: %s", ex)
126
+ # Don't fail start() - NNG is optional for backwards compatibility
127
+
128
+ def get_metadata(self) -> Optional[GstMetadata]:
129
+ """
130
+ Get the current GStreamer metadata.
131
+
132
+ Returns:
133
+ GstMetadata or None if not available
134
+ """
135
+ with self._lock:
136
+ if self._controller:
137
+ return self._controller.get_metadata()
138
+ return None
139
+
140
+ def start(
141
+ self,
142
+ on_frame: Union[Callable[[Mat], None], Callable[[Mat, Mat], None]], # type: ignore[valid-type]
143
+ cancellation_token: Optional[threading.Event] = None,
144
+ ) -> None:
145
+ """
146
+ Start receiving/processing video frames.
147
+
148
+ Args:
149
+ on_frame: Callback for frame processing.
150
+ For one-way: (input_frame) -> None
151
+ For duplex: (input_frame, output_frame) -> None
152
+ cancellation_token: Optional cancellation token
153
+
154
+ Raises:
155
+ RuntimeError: If already running
156
+ ValueError: If connection type is not supported
157
+ """
158
+ with self._lock:
159
+ if self._controller and self._controller.is_running:
160
+ raise RuntimeError("Client is already running")
161
+
162
+ # Create appropriate controller based on connection
163
+ if self._connection.protocol == Protocol.SHM:
164
+ if self._connection.connection_mode == ConnectionMode.DUPLEX:
165
+ self._controller = DuplexShmController(self._connection)
166
+ else:
167
+ self._controller = OneWayShmController(self._connection)
168
+ elif self._connection.protocol == Protocol.FILE or bool(
169
+ self._connection.protocol & Protocol.MJPEG # type: ignore[operator]
170
+ ):
171
+ self._controller = OpenCvController(self._connection)
172
+ else:
173
+ raise ValueError(f"Unsupported protocol: {self._connection.protocol}")
174
+
175
+ # Auto-create NNG publishers if URLs are configured
176
+ # (explicit URLs via SEGMENTATION_SINK_URL etc., or derived from SessionId)
177
+ if has_explicit_nng_urls():
178
+ self._create_nng_publishers()
179
+ else:
180
+ # Log that NNG is not configured (informational)
181
+ urls = get_nng_urls_from_env()
182
+ logger.info(
183
+ "NNG sink URLs not configured (this is normal if not publishing AI results). "
184
+ "seg=%s, kp=%s, actions=%s",
185
+ urls.get("segmentation") or "(not set)",
186
+ urls.get("keypoints") or "(not set)",
187
+ urls.get("actions") or "(not set)",
188
+ )
189
+
190
+ # If preview is enabled, wrap the callback to capture frames
191
+ if self._preview_enabled:
192
+ self._original_callback = on_frame
193
+
194
+ # Determine if duplex or one-way
195
+ if self._connection.connection_mode == ConnectionMode.DUPLEX:
196
+
197
+ def preview_wrapper_duplex(
198
+ metadata: FrameMetadata, input_frame: Mat, output_frame: Mat # type: ignore[valid-type]
199
+ ) -> None:
200
+ # Call original callback (ignoring FrameMetadata for backwards compatibility)
201
+ on_frame(input_frame, output_frame) # type: ignore[call-arg]
202
+ # Queue the OUTPUT frame for preview
203
+ try:
204
+ self._preview_queue.put_nowait(output_frame.copy()) # type: ignore[attr-defined]
205
+ except queue.Full:
206
+ # Drop oldest frame if queue is full
207
+ try:
208
+ self._preview_queue.get_nowait()
209
+ self._preview_queue.put_nowait(output_frame.copy()) # type: ignore[attr-defined]
210
+ except queue.Empty:
211
+ pass
212
+
213
+ actual_callback = preview_wrapper_duplex
214
+ else:
215
+
216
+ def preview_wrapper_oneway(frame: Mat) -> None: # type: ignore[valid-type]
217
+ # Call original callback
218
+ on_frame(frame) # type: ignore[call-arg]
219
+ # Queue frame for preview
220
+ try:
221
+ self._preview_queue.put_nowait(frame.copy()) # type: ignore[attr-defined]
222
+ except queue.Full:
223
+ # Drop oldest frame if queue is full
224
+ try:
225
+ self._preview_queue.get_nowait()
226
+ self._preview_queue.put_nowait(frame.copy()) # type: ignore[attr-defined]
227
+ except queue.Empty:
228
+ pass
229
+
230
+ actual_callback = preview_wrapper_oneway # type: ignore[assignment]
231
+ else:
232
+ # Wrap the callback to adapt (Mat, Mat) -> (FrameMetadata, Mat, Mat) for duplex
233
+ if self._connection.connection_mode == ConnectionMode.DUPLEX:
234
+
235
+ def metadata_adapter(
236
+ metadata: FrameMetadata, input_frame: Mat, output_frame: Mat # type: ignore[valid-type]
237
+ ) -> None:
238
+ # Call original callback (ignoring FrameMetadata for backwards compatibility)
239
+ on_frame(input_frame, output_frame) # type: ignore[call-arg]
240
+
241
+ actual_callback = metadata_adapter
242
+ else:
243
+ actual_callback = on_frame # type: ignore[assignment]
244
+
245
+ # Start the controller
246
+ self._controller.start(actual_callback, cancellation_token) # type: ignore[arg-type]
247
+ logger.info("RocketWelder client started with %s", self._connection)
248
+
249
+ def stop(self) -> None:
250
+ """Stop the client and clean up resources."""
251
+ with self._lock:
252
+ if self._controller:
253
+ self._controller.stop()
254
+ self._controller = None
255
+
256
+ # Signal preview to stop if enabled
257
+ if self._preview_enabled:
258
+ self._preview_queue.put(None) # Sentinel value
259
+
260
+ # Clean up NNG publishers
261
+ for name, sink in self._nng_publishers.items():
262
+ try:
263
+ sink.close()
264
+ logger.debug("Closed NNG publisher: %s", name)
265
+ except Exception as ex:
266
+ logger.warning("Failed to close NNG publisher %s: %s", name, ex)
267
+ self._nng_publishers.clear()
268
+
269
+ logger.info("RocketWelder client stopped")
270
+
271
+ def show(self, cancellation_token: Optional[threading.Event] = None) -> None:
272
+ """
273
+ Display preview frames in a window (main thread only).
274
+
275
+ This method should be called from the main thread after start().
276
+ - If preview=true: blocks and displays frames until stopped or 'q' pressed
277
+ - If preview=false or not set: returns immediately
278
+
279
+ Args:
280
+ cancellation_token: Optional cancellation token to stop preview
281
+
282
+ Example:
283
+ client = RocketWelderClient("file:///video.mp4?preview=true")
284
+ client.start(process_frame)
285
+ client.show() # Blocks and shows preview
286
+ client.stop()
287
+ """
288
+ if not self._preview_enabled:
289
+ # No preview requested, return immediately
290
+ return
291
+
292
+ try:
293
+ import cv2
294
+ except ImportError:
295
+ logger.warning("OpenCV not available, cannot show preview")
296
+ return
297
+
298
+ logger.info("Starting preview display in main thread")
299
+
300
+ # Create window
301
+ cv2.namedWindow(self._preview_window_name, cv2.WINDOW_NORMAL)
302
+
303
+ try:
304
+ while True:
305
+ # Check for cancellation
306
+ if cancellation_token and cancellation_token.is_set():
307
+ break
308
+
309
+ try:
310
+ # Get frame with timeout
311
+ frame = self._preview_queue.get(timeout=0.1)
312
+
313
+ # Check for stop sentinel
314
+ if frame is None:
315
+ break
316
+
317
+ # Display frame
318
+ cv2.imshow(self._preview_window_name, frame)
319
+
320
+ # Process window events and check for 'q' key
321
+ key = cv2.waitKey(1) & 0xFF
322
+ if key == ord("q"):
323
+ logger.info("User pressed 'q', stopping preview")
324
+ break
325
+
326
+ except queue.Empty:
327
+ # No frame available, check if still running
328
+ if not self.is_running:
329
+ break
330
+ # Process window events even without new frame
331
+ if cv2.waitKey(1) & 0xFF == ord("q"):
332
+ logger.info("User pressed 'q', stopping preview")
333
+ break
334
+
335
+ finally:
336
+ # Clean up window
337
+ cv2.destroyWindow(self._preview_window_name)
338
+ cv2.waitKey(1) # Process pending events
339
+ logger.info("Preview display stopped")
340
+
341
+ def __enter__(self) -> RocketWelderClient:
342
+ """Context manager entry."""
343
+ return self
344
+
345
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
346
+ """Context manager exit."""
347
+ self.stop()
348
+
349
+ @classmethod
350
+ def from_connection_string(cls, connection_string: str) -> RocketWelderClient:
351
+ """
352
+ Create a client from a connection string.
353
+
354
+ Args:
355
+ connection_string: Connection string (e.g., 'shm://buffer?mode=Duplex')
356
+
357
+ Returns:
358
+ Configured RocketWelderClient instance
359
+ """
360
+ return cls(connection_string)
361
+
362
+ @classmethod
363
+ def from_args(cls, args: List[str]) -> RocketWelderClient:
364
+ """
365
+ Create a client from command line arguments.
366
+
367
+ Checks in order:
368
+ 1. First positional argument from args
369
+ 2. CONNECTION_STRING environment variable
370
+
371
+ Args:
372
+ args: Command line arguments (typically sys.argv)
373
+
374
+ Returns:
375
+ Configured RocketWelderClient instance
376
+
377
+ Raises:
378
+ ValueError: If no connection string is found
379
+ """
380
+ import os
381
+
382
+ # Check for positional argument (skip script name if present)
383
+ connection_string = None
384
+ for arg in args[1:] if len(args) > 0 and args[0].endswith(".py") else args:
385
+ if not arg.startswith("-"):
386
+ connection_string = arg
387
+ break
388
+
389
+ # Fall back to environment variable
390
+ if not connection_string:
391
+ connection_string = os.environ.get("CONNECTION_STRING")
392
+
393
+ if not connection_string:
394
+ raise ValueError(
395
+ "No connection string provided. "
396
+ "Provide as argument or set CONNECTION_STRING environment variable"
397
+ )
398
+
399
+ return cls(connection_string)
400
+
401
+ @classmethod
402
+ def from_(cls, *args: Any, **kwargs: Any) -> RocketWelderClient:
403
+ """
404
+ Create a client with automatic configuration detection.
405
+
406
+ This is the most convenient factory method that:
407
+ 1. Checks kwargs for 'args' parameter (command line arguments)
408
+ 2. Checks args for command line arguments
409
+ 3. Falls back to CONNECTION_STRING environment variable
410
+
411
+ Examples:
412
+ client = RocketWelderClient.from_() # Uses env var
413
+ client = RocketWelderClient.from_(sys.argv) # Uses command line
414
+ client = RocketWelderClient.from_(args=sys.argv) # Named param
415
+
416
+ Returns:
417
+ Configured RocketWelderClient instance
418
+
419
+ Raises:
420
+ ValueError: If no connection string is found
421
+ """
422
+ import os
423
+
424
+ # Check kwargs first
425
+ argv = kwargs.get("args")
426
+
427
+ # Then check positional args
428
+ if not argv and args:
429
+ # If first arg looks like sys.argv (list), use it
430
+ if isinstance(args[0], list):
431
+ argv = args[0]
432
+ # If first arg is a string, treat it as connection string
433
+ elif isinstance(args[0], str):
434
+ return cls(args[0])
435
+
436
+ # Try to get from command line args if provided
437
+ if argv:
438
+ try:
439
+ return cls.from_args(argv)
440
+ except ValueError:
441
+ pass # Fall through to env var check
442
+
443
+ # Fall back to environment variable
444
+ connection_string = os.environ.get("CONNECTION_STRING")
445
+ if connection_string:
446
+ return cls(connection_string)
447
+
448
+ raise ValueError(
449
+ "No connection string provided. "
450
+ "Provide as argument or set CONNECTION_STRING environment variable"
451
+ )
452
+
453
+ @classmethod
454
+ def create_oneway_shm(
455
+ cls,
456
+ buffer_name: str,
457
+ buffer_size: str = "256MB",
458
+ metadata_size: str = "4KB",
459
+ ) -> RocketWelderClient:
460
+ """
461
+ Create a one-way shared memory client.
462
+
463
+ Args:
464
+ buffer_name: Name of the shared memory buffer
465
+ buffer_size: Size of the buffer (e.g., "256MB")
466
+ metadata_size: Size of metadata buffer (e.g., "4KB")
467
+
468
+ Returns:
469
+ Configured RocketWelderClient instance
470
+ """
471
+ connection_str = (
472
+ f"shm://{buffer_name}?size={buffer_size}&metadata={metadata_size}&mode=OneWay"
473
+ )
474
+ return cls(connection_str)
475
+
476
+ @classmethod
477
+ def create_duplex_shm(
478
+ cls,
479
+ buffer_name: str,
480
+ buffer_size: str = "256MB",
481
+ metadata_size: str = "4KB",
482
+ ) -> RocketWelderClient:
483
+ """
484
+ Create a duplex shared memory client.
485
+
486
+ Args:
487
+ buffer_name: Name of the shared memory buffer
488
+ buffer_size: Size of the buffer (e.g., "256MB")
489
+ metadata_size: Size of metadata buffer (e.g., "4KB")
490
+
491
+ Returns:
492
+ Configured RocketWelderClient instance
493
+ """
494
+ connection_str = (
495
+ f"shm://{buffer_name}?size={buffer_size}&metadata={metadata_size}&mode=Duplex"
496
+ )
497
+ return cls(connection_str)