rocket-welder-sdk 1.1.27__tar.gz → 1.1.28__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 (42) hide show
  1. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/PKG-INFO +130 -1
  2. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/README.md +128 -0
  3. rocket_welder_sdk-1.1.28/VERSION +1 -0
  4. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/pyproject.toml +9 -4
  5. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk/__init__.py +7 -1
  6. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk/connection_string.py +51 -0
  7. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk/controllers.py +32 -24
  8. rocket_welder_sdk-1.1.28/rocket_welder_sdk/opencv_controller.py +278 -0
  9. rocket_welder_sdk-1.1.28/rocket_welder_sdk/periodic_timer.py +303 -0
  10. rocket_welder_sdk-1.1.28/rocket_welder_sdk/rocket_welder_client.py +404 -0
  11. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk.egg-info/PKG-INFO +130 -1
  12. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk.egg-info/SOURCES.txt +2 -0
  13. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/tests/test_ui_service_happy_path.py +3 -4
  14. rocket_welder_sdk-1.1.27/VERSION +0 -1
  15. rocket_welder_sdk-1.1.27/rocket_welder_sdk/rocket_welder_client.py +0 -167
  16. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/MANIFEST.in +0 -0
  17. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/logo.png +0 -0
  18. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk/bytes_size.py +0 -0
  19. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk/external_controls/__init__.py +0 -0
  20. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk/external_controls/contracts.py +0 -0
  21. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk/external_controls/contracts_old.py +0 -0
  22. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk/gst_metadata.py +0 -0
  23. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk/py.typed +0 -0
  24. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk/ui/__init__.py +0 -0
  25. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk/ui/controls.py +0 -0
  26. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk/ui/icons.py +0 -0
  27. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk/ui/ui_events_projection.py +0 -0
  28. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk/ui/ui_service.py +0 -0
  29. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk/ui/value_types.py +0 -0
  30. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk.egg-info/dependency_links.txt +0 -0
  31. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk.egg-info/requires.txt +0 -0
  32. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/rocket_welder_sdk.egg-info/top_level.txt +0 -0
  33. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/setup.cfg +0 -0
  34. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/setup.py +0 -0
  35. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/tests/test_bytes_size.py +0 -0
  36. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/tests/test_connection_string.py +0 -0
  37. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/tests/test_controllers.py +0 -0
  38. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/tests/test_external_controls_serialization.py +0 -0
  39. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/tests/test_external_controls_serialization_v2.py +0 -0
  40. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/tests/test_gst_metadata.py +0 -0
  41. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/tests/test_icons.py +0 -0
  42. {rocket_welder_sdk-1.1.27 → rocket_welder_sdk-1.1.28}/tests/test_ui_controls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rocket-welder-sdk
3
- Version: 1.1.27
3
+ Version: 1.1.28
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
@@ -17,6 +17,7 @@ Classifier: Topic :: Multimedia :: Video
17
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
18
  Classifier: License :: OSI Approved :: MIT License
19
19
  Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.8
20
21
  Classifier: Programming Language :: Python :: 3.9
21
22
  Classifier: Programming Language :: Python :: 3.10
22
23
  Classifier: Programming Language :: Python :: 3.11
@@ -93,6 +94,18 @@ shm://<buffer_name>?mode=duplex&buffer_size=10MB
93
94
  - `buffer_size`: Size of the data buffer (default: 20MB, supports units: B, KB, MB, GB)
94
95
  - `metadata_size`: Size of the metadata buffer (default: 4KB, supports units: B, KB, MB)
95
96
 
97
+ #### File (Video File Playback)
98
+ ```
99
+ file:///path/to/video.mp4
100
+ file:///path/to/video.mp4?loop=true
101
+ file:///path/to/video.mp4?preview=true
102
+ file:///path/to/video.mp4?loop=true&preview=true
103
+ ```
104
+
105
+ **Optional Parameters:**
106
+ - `loop`: Loop video playback when end is reached (`true` or `false`; default: `false`)
107
+ - `preview`: Enable preview window display (`true` or `false`; default: `false`)
108
+
96
109
  #### MJPEG over HTTP
97
110
  ```
98
111
  mjpeg+http://192.168.1.100:8080
@@ -318,6 +331,44 @@ for frame in client.frames():
318
331
  print(f"Received frame: {frame.shape}")
319
332
  ```
320
333
 
334
+ ## Preview Display
335
+
336
+ When using the `file://` protocol with `preview=true` parameter, you can display frames in a window. The `Show()` method must be called from the main thread:
337
+
338
+ ### C# Preview
339
+ ```csharp
340
+ var client = RocketWelderClient.FromConnectionString(
341
+ "file:///path/to/video.mp4?preview=true&loop=true"
342
+ );
343
+
344
+ // Start processing in background
345
+ client.Start(frame => {
346
+ // Process frame
347
+ });
348
+
349
+ // Show preview window in main thread (blocks until 'q' pressed)
350
+ client.Show();
351
+ ```
352
+
353
+ ### Python Preview
354
+ ```python
355
+ client = rw.Client.from_connection_string(
356
+ "file:///path/to/video.mp4?preview=true&loop=true"
357
+ )
358
+
359
+ # Start processing in background
360
+ client.start(lambda frame: process_frame(frame))
361
+
362
+ # Show preview window in main thread (blocks until 'q' pressed)
363
+ client.show()
364
+ ```
365
+
366
+ **Note**: The `Show()` method:
367
+ - Blocks when `preview=true` is set in the connection string
368
+ - Returns immediately when `preview` is not set or `false`
369
+ - Must be called from the main thread (X11/GUI requirement)
370
+ - Stops when 'q' key is pressed in the preview window
371
+
321
372
  ## Docker Integration
322
373
 
323
374
  ### C++ Dockerfile
@@ -385,6 +436,75 @@ COPY . .
385
436
  CMD ["python", "app.py"]
386
437
  ```
387
438
 
439
+ ### Running Docker with X11 Display Support (Preview)
440
+
441
+ When using the `preview=true` parameter with file protocol, you need to enable X11 forwarding for Docker containers to display the preview window.
442
+
443
+ #### Linux
444
+
445
+ ```bash
446
+ # Allow X server connections from Docker
447
+ xhost +local:docker
448
+
449
+ # Run container with display support
450
+ docker run --rm \
451
+ -e DISPLAY=$DISPLAY \
452
+ -e CONNECTION_STRING="file:///data/video.mp4?preview=true&loop=true" \
453
+ -v /tmp/.X11-unix:/tmp/.X11-unix:rw \
454
+ -v /path/to/video.mp4:/data/video.mp4:ro \
455
+ --network host \
456
+ your-image:latest
457
+
458
+ # Restore X server security after running
459
+ xhost -local:docker
460
+ ```
461
+
462
+ #### Windows WSL2
463
+
464
+ WSL2 includes WSLg which provides automatic X11 support:
465
+
466
+ ```bash
467
+ # WSLg sets DISPLAY automatically, just verify it's set
468
+ echo $DISPLAY # Should show :0 or similar
469
+
470
+ # Allow X server connections
471
+ xhost +local:docker 2>/dev/null || xhost +local: 2>/dev/null
472
+
473
+ # Run container with display support (same as Linux)
474
+ docker run --rm \
475
+ -e DISPLAY=$DISPLAY \
476
+ -e CONNECTION_STRING="file:///data/video.mp4?preview=true&loop=true" \
477
+ -v /tmp/.X11-unix:/tmp/.X11-unix:rw \
478
+ -v /mnt/c/path/to/video.mp4:/data/video.mp4:ro \
479
+ --network host \
480
+ your-image:latest
481
+
482
+ # Restore X server security
483
+ xhost -local:docker 2>/dev/null || xhost -local: 2>/dev/null
484
+ ```
485
+
486
+ #### Helper Scripts
487
+
488
+ The SDK includes helper scripts for easy testing:
489
+
490
+ ```bash
491
+ # Build Docker images with sample clients
492
+ ./build_docker_samples.sh
493
+
494
+ # Test Python client with preview
495
+ ./run_docker_x11.sh python
496
+
497
+ # Test C# client with preview
498
+ ./run_docker_x11.sh csharp
499
+ ```
500
+
501
+ These scripts automatically:
502
+ - Configure X11 display forwarding
503
+ - Use a test video from the repository's data folder
504
+ - Mount the video into the container
505
+ - Set up the connection string with preview enabled
506
+ - Clean up X server permissions after running
507
+
388
508
  ## Protocol Details
389
509
 
390
510
  ### Shared Memory Protocol (shm://)
@@ -394,6 +514,15 @@ High-performance local data transfer between processes:
394
514
  - **Performance**: Minimal latency, maximum throughput
395
515
  - **Use Cases**: Local processing, multi-container applications on same host
396
516
 
517
+ ### File Protocol (file://)
518
+
519
+ Local video file playback with OpenCV:
520
+
521
+ - **Performance**: Controlled playback speed based on video FPS
522
+ - **Features**: Loop playback, preview window, frame-accurate timing
523
+ - **Use Cases**: Testing, development, offline processing, demos
524
+ - **Supported Formats**: All formats supported by OpenCV (MP4, AVI, MOV, etc.)
525
+
397
526
  ### MJPEG over HTTP (mjpeg+http://)
398
527
 
399
528
  Motion JPEG streaming over HTTP:
@@ -49,6 +49,18 @@ shm://<buffer_name>?mode=duplex&buffer_size=10MB
49
49
  - `buffer_size`: Size of the data buffer (default: 20MB, supports units: B, KB, MB, GB)
50
50
  - `metadata_size`: Size of the metadata buffer (default: 4KB, supports units: B, KB, MB)
51
51
 
52
+ #### File (Video File Playback)
53
+ ```
54
+ file:///path/to/video.mp4
55
+ file:///path/to/video.mp4?loop=true
56
+ file:///path/to/video.mp4?preview=true
57
+ file:///path/to/video.mp4?loop=true&preview=true
58
+ ```
59
+
60
+ **Optional Parameters:**
61
+ - `loop`: Loop video playback when end is reached (`true` or `false`; default: `false`)
62
+ - `preview`: Enable preview window display (`true` or `false`; default: `false`)
63
+
52
64
  #### MJPEG over HTTP
53
65
  ```
54
66
  mjpeg+http://192.168.1.100:8080
@@ -274,6 +286,44 @@ for frame in client.frames():
274
286
  print(f"Received frame: {frame.shape}")
275
287
  ```
276
288
 
289
+ ## Preview Display
290
+
291
+ When using the `file://` protocol with `preview=true` parameter, you can display frames in a window. The `Show()` method must be called from the main thread:
292
+
293
+ ### C# Preview
294
+ ```csharp
295
+ var client = RocketWelderClient.FromConnectionString(
296
+ "file:///path/to/video.mp4?preview=true&loop=true"
297
+ );
298
+
299
+ // Start processing in background
300
+ client.Start(frame => {
301
+ // Process frame
302
+ });
303
+
304
+ // Show preview window in main thread (blocks until 'q' pressed)
305
+ client.Show();
306
+ ```
307
+
308
+ ### Python Preview
309
+ ```python
310
+ client = rw.Client.from_connection_string(
311
+ "file:///path/to/video.mp4?preview=true&loop=true"
312
+ )
313
+
314
+ # Start processing in background
315
+ client.start(lambda frame: process_frame(frame))
316
+
317
+ # Show preview window in main thread (blocks until 'q' pressed)
318
+ client.show()
319
+ ```
320
+
321
+ **Note**: The `Show()` method:
322
+ - Blocks when `preview=true` is set in the connection string
323
+ - Returns immediately when `preview` is not set or `false`
324
+ - Must be called from the main thread (X11/GUI requirement)
325
+ - Stops when 'q' key is pressed in the preview window
326
+
277
327
  ## Docker Integration
278
328
 
279
329
  ### C++ Dockerfile
@@ -341,6 +391,75 @@ COPY . .
341
391
  CMD ["python", "app.py"]
342
392
  ```
343
393
 
394
+ ### Running Docker with X11 Display Support (Preview)
395
+
396
+ When using the `preview=true` parameter with file protocol, you need to enable X11 forwarding for Docker containers to display the preview window.
397
+
398
+ #### Linux
399
+
400
+ ```bash
401
+ # Allow X server connections from Docker
402
+ xhost +local:docker
403
+
404
+ # Run container with display support
405
+ docker run --rm \
406
+ -e DISPLAY=$DISPLAY \
407
+ -e CONNECTION_STRING="file:///data/video.mp4?preview=true&loop=true" \
408
+ -v /tmp/.X11-unix:/tmp/.X11-unix:rw \
409
+ -v /path/to/video.mp4:/data/video.mp4:ro \
410
+ --network host \
411
+ your-image:latest
412
+
413
+ # Restore X server security after running
414
+ xhost -local:docker
415
+ ```
416
+
417
+ #### Windows WSL2
418
+
419
+ WSL2 includes WSLg which provides automatic X11 support:
420
+
421
+ ```bash
422
+ # WSLg sets DISPLAY automatically, just verify it's set
423
+ echo $DISPLAY # Should show :0 or similar
424
+
425
+ # Allow X server connections
426
+ xhost +local:docker 2>/dev/null || xhost +local: 2>/dev/null
427
+
428
+ # Run container with display support (same as Linux)
429
+ docker run --rm \
430
+ -e DISPLAY=$DISPLAY \
431
+ -e CONNECTION_STRING="file:///data/video.mp4?preview=true&loop=true" \
432
+ -v /tmp/.X11-unix:/tmp/.X11-unix:rw \
433
+ -v /mnt/c/path/to/video.mp4:/data/video.mp4:ro \
434
+ --network host \
435
+ your-image:latest
436
+
437
+ # Restore X server security
438
+ xhost -local:docker 2>/dev/null || xhost -local: 2>/dev/null
439
+ ```
440
+
441
+ #### Helper Scripts
442
+
443
+ The SDK includes helper scripts for easy testing:
444
+
445
+ ```bash
446
+ # Build Docker images with sample clients
447
+ ./build_docker_samples.sh
448
+
449
+ # Test Python client with preview
450
+ ./run_docker_x11.sh python
451
+
452
+ # Test C# client with preview
453
+ ./run_docker_x11.sh csharp
454
+ ```
455
+
456
+ These scripts automatically:
457
+ - Configure X11 display forwarding
458
+ - Use a test video from the repository's data folder
459
+ - Mount the video into the container
460
+ - Set up the connection string with preview enabled
461
+ - Clean up X server permissions after running
462
+
344
463
  ## Protocol Details
345
464
 
346
465
  ### Shared Memory Protocol (shm://)
@@ -350,6 +469,15 @@ High-performance local data transfer between processes:
350
469
  - **Performance**: Minimal latency, maximum throughput
351
470
  - **Use Cases**: Local processing, multi-container applications on same host
352
471
 
472
+ ### File Protocol (file://)
473
+
474
+ Local video file playback with OpenCV:
475
+
476
+ - **Performance**: Controlled playback speed based on video FPS
477
+ - **Features**: Loop playback, preview window, frame-accurate timing
478
+ - **Use Cases**: Testing, development, offline processing, demos
479
+ - **Supported Formats**: All formats supported by OpenCV (MP4, AVI, MOV, etc.)
480
+
353
481
  ### MJPEG over HTTP (mjpeg+http://)
354
482
 
355
483
  Motion JPEG streaming over HTTP:
@@ -0,0 +1 @@
1
+ 1.1.28
@@ -7,7 +7,7 @@ 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
- requires-python = ">=3.9"
10
+ requires-python = ">=3.8.10"
11
11
  license = {text = "MIT"}
12
12
  authors = [
13
13
  {name = "ModelingEvolution", email = "info@modelingevolution.com"}
@@ -23,6 +23,7 @@ classifiers = [
23
23
  "Topic :: Software Development :: Libraries :: Python Modules",
24
24
  "License :: OSI Approved :: MIT License",
25
25
  "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.8",
26
27
  "Programming Language :: Python :: 3.9",
27
28
  "Programming Language :: Python :: 3.10",
28
29
  "Programming Language :: Python :: 3.11",
@@ -66,7 +67,7 @@ include = ["rocket_welder_sdk*"]
66
67
  rocket_welder_sdk = ["py.typed"]
67
68
 
68
69
  [tool.mypy]
69
- python_version = "3.9"
70
+ python_version = "3.9" # mypy 1.0+ requires 3.9, but code supports 3.8
70
71
  strict = true
71
72
  warn_return_any = true
72
73
  warn_unused_configs = true
@@ -80,6 +81,7 @@ pretty = true
80
81
  module = [
81
82
  "cv2",
82
83
  "cv2.*",
84
+ "cv2.typing",
83
85
  "zerobuffer",
84
86
  "zerobuffer.*",
85
87
  "py_micro_plumberd",
@@ -91,12 +93,12 @@ ignore_missing_imports = true
91
93
 
92
94
  [tool.black]
93
95
  line-length = 100
94
- target-version = ['py39', 'py310', 'py311', 'py312']
96
+ target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
95
97
  include = '\.pyi?$'
96
98
 
97
99
  [tool.ruff]
98
100
  line-length = 100
99
- target-version = "py39"
101
+ target-version = "py38"
100
102
 
101
103
  [tool.ruff.lint]
102
104
  select = [
@@ -116,6 +118,9 @@ select = [
116
118
  ignore = [
117
119
  "E501", # line too long (handled by black)
118
120
  "B008", # do not perform function calls in argument defaults
121
+ "UP006", # Keep List[X] for Python 3.8 compatibility
122
+ "UP007", # Keep Union[X, Y] for Python 3.8 compatibility
123
+ "UP045", # Keep Optional[X] for Python 3.8 compatibility
119
124
  "C901", # too complex
120
125
  ]
121
126
 
@@ -11,9 +11,11 @@ from .bytes_size import BytesSize
11
11
  from .connection_string import ConnectionMode, ConnectionString, Protocol
12
12
  from .controllers import DuplexShmController, IController, OneWayShmController
13
13
  from .gst_metadata import GstCaps, GstMetadata
14
+ from .opencv_controller import OpenCvController
15
+ from .periodic_timer import PeriodicTimer, PeriodicTimerSync
14
16
  from .rocket_welder_client import RocketWelderClient
15
17
 
16
- # Alias for backward compatibility
18
+ # Alias for backward compatibility and README examples
17
19
  Client = RocketWelderClient
18
20
 
19
21
  __version__ = "1.1.0"
@@ -50,6 +52,10 @@ __all__ = [
50
52
  # Controllers
51
53
  "IController",
52
54
  "OneWayShmController",
55
+ "OpenCvController",
56
+ # Timers
57
+ "PeriodicTimer",
58
+ "PeriodicTimerSync",
53
59
  "Protocol",
54
60
  # Main client
55
61
  "RocketWelderClient",
@@ -21,6 +21,7 @@ class Protocol(Flag):
21
21
  MJPEG = auto() # Motion JPEG
22
22
  HTTP = auto() # HTTP protocol
23
23
  TCP = auto() # TCP protocol
24
+ FILE = auto() # File protocol
24
25
 
25
26
 
26
27
  class ConnectionMode(Enum):
@@ -43,12 +44,15 @@ class ConnectionString:
43
44
  - shm://buffer_name?size=256MB&metadata=4KB&mode=Duplex
44
45
  - mjpeg://192.168.1.100:8080
45
46
  - mjpeg+http://camera.local:80
47
+ - file:///path/to/video.mp4?loop=true
46
48
  """
47
49
 
48
50
  protocol: Protocol
49
51
  host: str | None = None
50
52
  port: int | None = None
51
53
  buffer_name: str | None = None
54
+ file_path: str | None = None
55
+ parameters: dict[str, str] = field(default_factory=dict)
52
56
  buffer_size: BytesSize = field(default_factory=lambda: BytesSize.parse("256MB"))
53
57
  metadata_size: BytesSize = field(default_factory=lambda: BytesSize.parse("4KB"))
54
58
  connection_mode: ConnectionMode = ConnectionMode.ONE_WAY
@@ -82,6 +86,8 @@ class ConnectionString:
82
86
  # Parse based on protocol type
83
87
  if protocol == Protocol.SHM:
84
88
  return cls._parse_shm(protocol, remainder)
89
+ elif protocol == Protocol.FILE:
90
+ return cls._parse_file(protocol, remainder)
85
91
  elif bool(protocol & Protocol.MJPEG): # type: ignore[operator]
86
92
  return cls._parse_mjpeg(protocol, remainder)
87
93
  else:
@@ -110,6 +116,7 @@ class ConnectionString:
110
116
  "mjpeg": Protocol.MJPEG,
111
117
  "http": Protocol.HTTP,
112
118
  "tcp": Protocol.TCP,
119
+ "file": Protocol.FILE,
113
120
  }
114
121
 
115
122
  protocol = protocol_map.get(protocol_str, Protocol.NONE)
@@ -157,6 +164,43 @@ class ConnectionString:
157
164
  timeout_ms=timeout_ms,
158
165
  )
159
166
 
167
+ @classmethod
168
+ def _parse_file(cls, protocol: Protocol, remainder: str) -> ConnectionString:
169
+ """Parse file protocol connection string."""
170
+ # Split file path and query parameters
171
+ if "?" in remainder:
172
+ file_path, query_string = remainder.split("?", 1)
173
+ params = cls._parse_query_params(query_string)
174
+ else:
175
+ file_path = remainder
176
+ params = {}
177
+
178
+ # Handle file:///absolute/path and file://relative/path
179
+ if not file_path.startswith("/"):
180
+ file_path = "/" + file_path
181
+
182
+ # Parse common parameters
183
+ connection_mode = ConnectionMode.ONE_WAY
184
+ timeout_ms = 5000
185
+
186
+ if "mode" in params:
187
+ mode_str = params["mode"].upper()
188
+ if mode_str == "DUPLEX":
189
+ connection_mode = ConnectionMode.DUPLEX
190
+ elif mode_str in ("ONEWAY", "ONE_WAY"):
191
+ connection_mode = ConnectionMode.ONE_WAY
192
+ if "timeout" in params:
193
+ with contextlib.suppress(ValueError):
194
+ timeout_ms = int(params["timeout"])
195
+
196
+ return cls(
197
+ protocol=protocol,
198
+ file_path=file_path,
199
+ parameters=params,
200
+ connection_mode=connection_mode,
201
+ timeout_ms=timeout_ms,
202
+ )
203
+
160
204
  @classmethod
161
205
  def _parse_mjpeg(cls, protocol: Protocol, remainder: str) -> ConnectionString:
162
206
  """Parse MJPEG connection string."""
@@ -195,6 +239,8 @@ class ConnectionString:
195
239
  protocol_parts = []
196
240
  if self.protocol & Protocol.SHM:
197
241
  protocol_parts.append("shm")
242
+ if self.protocol & Protocol.FILE:
243
+ protocol_parts.append("file")
198
244
  if self.protocol & Protocol.MJPEG:
199
245
  protocol_parts.append("mjpeg")
200
246
  if self.protocol & Protocol.HTTP:
@@ -215,6 +261,11 @@ class ConnectionString:
215
261
  params.append(f"timeout={self.timeout_ms}")
216
262
 
217
263
  return f"{protocol_str}://{self.buffer_name}?{'&'.join(params)}"
264
+ elif self.protocol == Protocol.FILE:
265
+ query_string = ""
266
+ if self.parameters:
267
+ query_string = "?" + "&".join(f"{k}={v}" for k, v in self.parameters.items())
268
+ return f"{protocol_str}://{self.file_path}{query_string}"
218
269
  else:
219
270
  return f"{protocol_str}://{self.host}:{self.port}"
220
271
 
@@ -9,7 +9,7 @@ import json
9
9
  import logging
10
10
  import threading
11
11
  from abc import ABC, abstractmethod
12
- from typing import Any, Callable
12
+ from typing import TYPE_CHECKING, Callable, Optional
13
13
 
14
14
  import numpy as np
15
15
  from zerobuffer import BufferConfig, Frame, Reader, Writer
@@ -19,8 +19,12 @@ from zerobuffer.exceptions import WriterDeadException
19
19
  from .connection_string import ConnectionMode, ConnectionString, Protocol
20
20
  from .gst_metadata import GstCaps, GstMetadata
21
21
 
22
- # Type alias for OpenCV Mat
23
- Mat = np.ndarray[Any, Any]
22
+ if TYPE_CHECKING:
23
+ import numpy.typing as npt
24
+
25
+ Mat = npt.NDArray[np.uint8]
26
+ else:
27
+ Mat = np.ndarray # type: ignore[misc]
24
28
 
25
29
  # Module logger
26
30
  logger = logging.getLogger(__name__)
@@ -36,13 +40,15 @@ class IController(ABC):
36
40
  ...
37
41
 
38
42
  @abstractmethod
39
- def get_metadata(self) -> GstMetadata | None:
43
+ def get_metadata(self) -> Optional[GstMetadata]:
40
44
  """Get the current GStreamer metadata."""
41
45
  ...
42
46
 
43
47
  @abstractmethod
44
48
  def start(
45
- self, on_frame: Callable[[Mat], None], cancellation_token: threading.Event | None = None
49
+ self,
50
+ on_frame: Callable[[Mat], None], # type: ignore[valid-type]
51
+ cancellation_token: Optional[threading.Event] = None,
46
52
  ) -> None:
47
53
  """
48
54
  Start the controller with a frame callback.
@@ -80,24 +86,26 @@ class OneWayShmController(IController):
80
86
  )
81
87
 
82
88
  self._connection = connection
83
- self._reader: Reader | None = None
84
- self._gst_caps: GstCaps | None = None
85
- self._metadata: GstMetadata | None = None
89
+ self._reader: Optional[Reader] = None
90
+ self._gst_caps: Optional[GstCaps] = None
91
+ self._metadata: Optional[GstMetadata] = None
86
92
  self._is_running = False
87
- self._worker_thread: threading.Thread | None = None
88
- self._cancellation_token: threading.Event | None = None
93
+ self._worker_thread: Optional[threading.Thread] = None
94
+ self._cancellation_token: Optional[threading.Event] = None
89
95
 
90
96
  @property
91
97
  def is_running(self) -> bool:
92
98
  """Check if the controller is running."""
93
99
  return self._is_running
94
100
 
95
- def get_metadata(self) -> GstMetadata | None:
101
+ def get_metadata(self) -> Optional[GstMetadata]:
96
102
  """Get the current GStreamer metadata."""
97
103
  return self._metadata
98
104
 
99
105
  def start(
100
- self, on_frame: Callable[[Mat], None], cancellation_token: threading.Event | None = None
106
+ self,
107
+ on_frame: Callable[[Mat], None], # type: ignore[valid-type]
108
+ cancellation_token: Optional[threading.Event] = None,
101
109
  ) -> None:
102
110
  """
103
111
  Start receiving frames from shared memory.
@@ -161,7 +169,7 @@ class OneWayShmController(IController):
161
169
  self._worker_thread = None
162
170
  logger.info("Stopped controller for buffer '%s'", self._connection.buffer_name)
163
171
 
164
- def _process_frames(self, on_frame: Callable[[Mat], None]) -> None:
172
+ def _process_frames(self, on_frame: Callable[[Mat], None]) -> None: # type: ignore[valid-type]
165
173
  """
166
174
  Process frames from shared memory.
167
175
 
@@ -236,7 +244,7 @@ class OneWayShmController(IController):
236
244
  logger.error("Fatal error in frame processing loop: %s", e)
237
245
  self._is_running = False
238
246
 
239
- def _on_first_frame(self, on_frame: Callable[[Mat], None]) -> None:
247
+ def _on_first_frame(self, on_frame: Callable[[Mat], None]) -> None: # type: ignore[valid-type]
240
248
  """
241
249
  Process the first frame and extract metadata.
242
250
  Matches C# OnFirstFrame behavior - loops until valid frame received.
@@ -342,7 +350,7 @@ class OneWayShmController(IController):
342
350
  if not self._is_running:
343
351
  break
344
352
 
345
- def _create_mat_from_frame(self, frame: Frame) -> Mat | None:
353
+ def _create_mat_from_frame(self, frame: Frame) -> Optional[Mat]: # type: ignore[valid-type]
346
354
  """
347
355
  Create OpenCV Mat from frame data using GstCaps.
348
356
  Matches C# CreateMat behavior - creates Mat wrapping the data.
@@ -440,7 +448,7 @@ class OneWayShmController(IController):
440
448
  logger.error("Failed to convert frame to Mat: %s", e)
441
449
  return None
442
450
 
443
- def _infer_caps_from_frame(self, mat: Mat) -> None:
451
+ def _infer_caps_from_frame(self, mat: Mat) -> None: # type: ignore[valid-type]
444
452
  """
445
453
  Infer GStreamer caps from OpenCV Mat.
446
454
 
@@ -487,11 +495,11 @@ class DuplexShmController(IController):
487
495
  )
488
496
 
489
497
  self._connection = connection
490
- self._duplex_server: IImmutableDuplexServer | None = None
491
- self._gst_caps: GstCaps | None = None
492
- self._metadata: GstMetadata | None = None
498
+ self._duplex_server: Optional[IImmutableDuplexServer] = None
499
+ self._gst_caps: Optional[GstCaps] = None
500
+ self._metadata: Optional[GstMetadata] = None
493
501
  self._is_running = False
494
- self._on_frame_callback: Callable[[Mat, Mat], None] | None = None
502
+ self._on_frame_callback: Optional[Callable[[Mat, Mat], None]] = None # type: ignore[valid-type]
495
503
  self._frame_count = 0
496
504
 
497
505
  @property
@@ -499,14 +507,14 @@ class DuplexShmController(IController):
499
507
  """Check if the controller is running."""
500
508
  return self._is_running
501
509
 
502
- def get_metadata(self) -> GstMetadata | None:
510
+ def get_metadata(self) -> Optional[GstMetadata]:
503
511
  """Get the current GStreamer metadata."""
504
512
  return self._metadata
505
513
 
506
514
  def start(
507
515
  self,
508
- on_frame: Callable[[Mat, Mat], None], # type: ignore[override]
509
- cancellation_token: threading.Event | None = None,
516
+ on_frame: Callable[[Mat, Mat], None], # type: ignore[override,valid-type]
517
+ cancellation_token: Optional[threading.Event] = None,
510
518
  ) -> None:
511
519
  """
512
520
  Start duplex frame processing.
@@ -662,7 +670,7 @@ class DuplexShmController(IController):
662
670
  except Exception as e:
663
671
  logger.error("Error processing duplex frame: %s", e)
664
672
 
665
- def _frame_to_mat(self, frame: Frame) -> Mat | None:
673
+ def _frame_to_mat(self, frame: Frame) -> Optional[Mat]: # type: ignore[valid-type]
666
674
  """Convert frame to OpenCV Mat (reuse from OneWayShmController)."""
667
675
  # Implementation is same as OneWayShmController
668
676
  return OneWayShmController._create_mat_from_frame(self, frame) # type: ignore[arg-type]