rocket-welder-sdk 1.1.27__py3-none-any.whl → 1.1.29__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.
@@ -6,19 +6,25 @@ Main entry point for the RocketWelder SDK.
6
6
  from __future__ import annotations
7
7
 
8
8
  import logging
9
+ import queue
9
10
  import threading
10
- from typing import TYPE_CHECKING, Any, Callable
11
+ from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union
11
12
 
12
13
  import numpy as np
13
14
 
14
15
  from .connection_string import ConnectionMode, ConnectionString, Protocol
15
16
  from .controllers import DuplexShmController, IController, OneWayShmController
17
+ from .opencv_controller import OpenCvController
16
18
 
17
19
  if TYPE_CHECKING:
20
+ import numpy.typing as npt
21
+
18
22
  from .gst_metadata import GstMetadata
19
23
 
20
- # Type alias for OpenCV Mat
21
- Mat = np.ndarray[Any, Any]
24
+ # Use numpy array type for Mat - OpenCV Mat is essentially a numpy array
25
+ Mat = npt.NDArray[np.uint8]
26
+ else:
27
+ Mat = np.ndarray # type: ignore[misc]
22
28
 
23
29
  # Module logger
24
30
  logger = logging.getLogger(__name__)
@@ -31,7 +37,7 @@ class RocketWelderClient:
31
37
  Provides a unified interface for different connection types and protocols.
32
38
  """
33
39
 
34
- def __init__(self, connection: str | ConnectionString):
40
+ def __init__(self, connection: Union[str, ConnectionString]):
35
41
  """
36
42
  Initialize the RocketWelder client.
37
43
 
@@ -43,9 +49,17 @@ class RocketWelderClient:
43
49
  else:
44
50
  self._connection = connection
45
51
 
46
- self._controller: IController | None = None
52
+ self._controller: Optional[IController] = None
47
53
  self._lock = threading.Lock()
48
54
 
55
+ # Preview support
56
+ self._preview_enabled = (
57
+ self._connection.parameters.get("preview", "false").lower() == "true"
58
+ )
59
+ self._preview_queue: queue.Queue[Optional[Mat]] = queue.Queue(maxsize=2) # type: ignore[valid-type] # Small buffer
60
+ self._preview_window_name = "RocketWelder Preview"
61
+ self._original_callback: Any = None
62
+
49
63
  @property
50
64
  def connection(self) -> ConnectionString:
51
65
  """Get the connection configuration."""
@@ -57,7 +71,7 @@ class RocketWelderClient:
57
71
  with self._lock:
58
72
  return self._controller is not None and self._controller.is_running
59
73
 
60
- def get_metadata(self) -> GstMetadata | None:
74
+ def get_metadata(self) -> Optional[GstMetadata]:
61
75
  """
62
76
  Get the current GStreamer metadata.
63
77
 
@@ -71,8 +85,8 @@ class RocketWelderClient:
71
85
 
72
86
  def start(
73
87
  self,
74
- on_frame: Callable[[Mat], None] | Callable[[Mat, Mat], None],
75
- cancellation_token: threading.Event | None = None,
88
+ on_frame: Union[Callable[[Mat], None], Callable[[Mat, Mat], None]], # type: ignore[valid-type]
89
+ cancellation_token: Optional[threading.Event] = None,
76
90
  ) -> None:
77
91
  """
78
92
  Start receiving/processing video frames.
@@ -97,11 +111,55 @@ class RocketWelderClient:
97
111
  self._controller = DuplexShmController(self._connection)
98
112
  else:
99
113
  self._controller = OneWayShmController(self._connection)
114
+ elif self._connection.protocol in (Protocol.FILE, Protocol.MJPEG):
115
+ self._controller = OpenCvController(self._connection)
100
116
  else:
101
117
  raise ValueError(f"Unsupported protocol: {self._connection.protocol}")
102
118
 
119
+ # If preview is enabled, wrap the callback to capture frames
120
+ if self._preview_enabled:
121
+ self._original_callback = on_frame
122
+
123
+ # Determine if duplex or one-way
124
+ if self._connection.connection_mode == ConnectionMode.DUPLEX:
125
+
126
+ def preview_wrapper_duplex(input_frame: Mat, output_frame: Mat) -> None: # type: ignore[valid-type]
127
+ # Call original callback
128
+ on_frame(input_frame, output_frame) # type: ignore[call-arg]
129
+ # Queue the OUTPUT frame for preview
130
+ try:
131
+ self._preview_queue.put_nowait(output_frame.copy()) # type: ignore[attr-defined]
132
+ except queue.Full:
133
+ # Drop oldest frame if queue is full
134
+ try:
135
+ self._preview_queue.get_nowait()
136
+ self._preview_queue.put_nowait(output_frame.copy()) # type: ignore[attr-defined]
137
+ except queue.Empty:
138
+ pass
139
+
140
+ actual_callback = preview_wrapper_duplex
141
+ else:
142
+
143
+ def preview_wrapper_oneway(frame: Mat) -> None: # type: ignore[valid-type]
144
+ # Call original callback
145
+ on_frame(frame) # type: ignore[call-arg]
146
+ # Queue frame for preview
147
+ try:
148
+ self._preview_queue.put_nowait(frame.copy()) # type: ignore[attr-defined]
149
+ except queue.Full:
150
+ # Drop oldest frame if queue is full
151
+ try:
152
+ self._preview_queue.get_nowait()
153
+ self._preview_queue.put_nowait(frame.copy()) # type: ignore[attr-defined]
154
+ except queue.Empty:
155
+ pass
156
+
157
+ actual_callback = preview_wrapper_oneway # type: ignore[assignment]
158
+ else:
159
+ actual_callback = on_frame # type: ignore[assignment]
160
+
103
161
  # Start the controller
104
- self._controller.start(on_frame, cancellation_token) # type: ignore[arg-type]
162
+ self._controller.start(actual_callback, cancellation_token) # type: ignore[arg-type]
105
163
  logger.info("RocketWelder client started with %s", self._connection)
106
164
 
107
165
  def stop(self) -> None:
@@ -110,8 +168,83 @@ class RocketWelderClient:
110
168
  if self._controller:
111
169
  self._controller.stop()
112
170
  self._controller = None
171
+
172
+ # Signal preview to stop if enabled
173
+ if self._preview_enabled:
174
+ self._preview_queue.put(None) # Sentinel value
175
+
113
176
  logger.info("RocketWelder client stopped")
114
177
 
178
+ def show(self, cancellation_token: Optional[threading.Event] = None) -> None:
179
+ """
180
+ Display preview frames in a window (main thread only).
181
+
182
+ This method should be called from the main thread after start().
183
+ - If preview=true: blocks and displays frames until stopped or 'q' pressed
184
+ - If preview=false or not set: returns immediately
185
+
186
+ Args:
187
+ cancellation_token: Optional cancellation token to stop preview
188
+
189
+ Example:
190
+ client = RocketWelderClient("file:///video.mp4?preview=true")
191
+ client.start(process_frame)
192
+ client.show() # Blocks and shows preview
193
+ client.stop()
194
+ """
195
+ if not self._preview_enabled:
196
+ # No preview requested, return immediately
197
+ return
198
+
199
+ try:
200
+ import cv2
201
+ except ImportError:
202
+ logger.warning("OpenCV not available, cannot show preview")
203
+ return
204
+
205
+ logger.info("Starting preview display in main thread")
206
+
207
+ # Create window
208
+ cv2.namedWindow(self._preview_window_name, cv2.WINDOW_NORMAL)
209
+
210
+ try:
211
+ while True:
212
+ # Check for cancellation
213
+ if cancellation_token and cancellation_token.is_set():
214
+ break
215
+
216
+ try:
217
+ # Get frame with timeout
218
+ frame = self._preview_queue.get(timeout=0.1)
219
+
220
+ # Check for stop sentinel
221
+ if frame is None:
222
+ break
223
+
224
+ # Display frame
225
+ cv2.imshow(self._preview_window_name, frame)
226
+
227
+ # Process window events and check for 'q' key
228
+ key = cv2.waitKey(1) & 0xFF
229
+ if key == ord("q"):
230
+ logger.info("User pressed 'q', stopping preview")
231
+ break
232
+
233
+ except queue.Empty:
234
+ # No frame available, check if still running
235
+ if not self.is_running:
236
+ break
237
+ # Process window events even without new frame
238
+ if cv2.waitKey(1) & 0xFF == ord("q"):
239
+ logger.info("User pressed 'q', stopping preview")
240
+ break
241
+
242
+ finally:
243
+ # Clean up window
244
+ cv2.destroyWindow(self._preview_window_name)
245
+ cv2.waitKey(1) # Process pending events
246
+ logger.info("Preview display stopped")
247
+
115
248
  def __enter__(self) -> RocketWelderClient:
116
249
  """Context manager entry."""
117
250
  return self
@@ -120,6 +253,110 @@ class RocketWelderClient:
120
253
  """Context manager exit."""
121
254
  self.stop()
122
255
 
256
+ @classmethod
257
+ def from_connection_string(cls, connection_string: str) -> RocketWelderClient:
258
+ """
259
+ Create a client from a connection string.
260
+
261
+ Args:
262
+ connection_string: Connection string (e.g., 'shm://buffer?mode=Duplex')
263
+
264
+ Returns:
265
+ Configured RocketWelderClient instance
266
+ """
267
+ return cls(connection_string)
268
+
269
+ @classmethod
270
+ def from_args(cls, args: List[str]) -> RocketWelderClient:
271
+ """
272
+ Create a client from command line arguments.
273
+
274
+ Checks in order:
275
+ 1. First positional argument from args
276
+ 2. CONNECTION_STRING environment variable
277
+
278
+ Args:
279
+ args: Command line arguments (typically sys.argv)
280
+
281
+ Returns:
282
+ Configured RocketWelderClient instance
283
+
284
+ Raises:
285
+ ValueError: If no connection string is found
286
+ """
287
+ import os
288
+
289
+ # Check for positional argument (skip script name if present)
290
+ connection_string = None
291
+ for arg in args[1:] if len(args) > 0 and args[0].endswith(".py") else args:
292
+ if not arg.startswith("-"):
293
+ connection_string = arg
294
+ break
295
+
296
+ # Fall back to environment variable
297
+ if not connection_string:
298
+ connection_string = os.environ.get("CONNECTION_STRING")
299
+
300
+ if not connection_string:
301
+ raise ValueError(
302
+ "No connection string provided. "
303
+ "Provide as argument or set CONNECTION_STRING environment variable"
304
+ )
305
+
306
+ return cls(connection_string)
307
+
308
+ @classmethod
309
+ def from_(cls, *args: Any, **kwargs: Any) -> RocketWelderClient:
310
+ """
311
+ Create a client with automatic configuration detection.
312
+
313
+ This is the most convenient factory method that:
314
+ 1. Checks kwargs for 'args' parameter (command line arguments)
315
+ 2. Checks args for command line arguments
316
+ 3. Falls back to CONNECTION_STRING environment variable
317
+
318
+ Examples:
319
+ client = RocketWelderClient.from_() # Uses env var
320
+ client = RocketWelderClient.from_(sys.argv) # Uses command line
321
+ client = RocketWelderClient.from_(args=sys.argv) # Named param
322
+
323
+ Returns:
324
+ Configured RocketWelderClient instance
325
+
326
+ Raises:
327
+ ValueError: If no connection string is found
328
+ """
329
+ import os
330
+
331
+ # Check kwargs first
332
+ argv = kwargs.get("args")
333
+
334
+ # Then check positional args
335
+ if not argv and args:
336
+ # If first arg looks like sys.argv (list), use it
337
+ if isinstance(args[0], list):
338
+ argv = args[0]
339
+ # If first arg is a string, treat it as connection string
340
+ elif isinstance(args[0], str):
341
+ return cls(args[0])
342
+
343
+ # Try to get from command line args if provided
344
+ if argv:
345
+ try:
346
+ return cls.from_args(argv)
347
+ except ValueError:
348
+ pass # Fall through to env var check
349
+
350
+ # Fall back to environment variable
351
+ connection_string = os.environ.get("CONNECTION_STRING")
352
+ if connection_string:
353
+ return cls(connection_string)
354
+
355
+ raise ValueError(
356
+ "No connection string provided. "
357
+ "Provide as argument or set CONNECTION_STRING environment variable"
358
+ )
359
+
123
360
  @classmethod
124
361
  def create_oneway_shm(
125
362
  cls,