Skip to content

Commit f4a9a53

Browse files
committed
refactor: clearer APIs and doc
1 parent da354c8 commit f4a9a53

File tree

7 files changed

+131
-220
lines changed

7 files changed

+131
-220
lines changed

src/arduino/app_peripherals/camera/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@
33
# SPDX-License-Identifier: MPL-2.0
44

55
from .camera import Camera
6+
from .v4l_camera import V4LCamera
7+
from .ip_camera import IPCamera
8+
from .websocket_camera import WebSocketCamera
69
from .errors import *
710

811
__all__ = [
912
"Camera",
13+
"V4LCamera",
14+
"IPCamera",
15+
"WebSocketCamera",
1016
"CameraError",
1117
"CameraReadError",
1218
"CameraOpenError",

src/arduino/app_peripherals/camera/base_camera.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,52 +23,57 @@ class BaseCamera(ABC):
2323
providing a unified API regardless of the underlying camera protocol or type.
2424
"""
2525

26-
def __init__(self, resolution: Optional[Tuple[int, int]] = (640, 480), fps: int = 10,
27-
adjustments: Optional[Callable[[np.ndarray], np.ndarray]] = None, **kwargs):
26+
def __init__(
27+
self,
28+
resolution: Optional[Tuple[int, int]] = (640, 480),
29+
fps: int = 10,
30+
adjustments: Optional[Callable[[np.ndarray], np.ndarray]] = None,
31+
):
2832
"""
2933
Initialize the camera base.
3034
3135
Args:
3236
resolution (tuple, optional): Resolution as (width, height). None uses default resolution.
33-
fps (int): Frames per second for the camera.
37+
fps (int): Frames per second to capture from the camera.
3438
adjustments (callable, optional): Function or function pipeline to adjust frames that takes
3539
a numpy array and returns a numpy array. Default: None
36-
**kwargs: Additional camera-specific parameters.
3740
"""
3841
self.resolution = resolution
3942
self.fps = fps
4043
self.adjustments = adjustments
44+
self.logger = logger # This will be overridden by subclasses if needed
45+
46+
self._camera_lock = threading.Lock()
4147
self._is_started = False
42-
self._cap_lock = threading.Lock()
4348
self._last_capture_time = time.monotonic()
44-
self.desired_interval = 1.0 / fps if fps > 0 else 0
49+
self._desired_interval = 1.0 / fps if fps > 0 else 0
4550

4651
def start(self) -> None:
4752
"""Start the camera capture."""
48-
with self._cap_lock:
53+
with self._camera_lock:
4954
if self._is_started:
5055
return
5156

5257
try:
5358
self._open_camera()
5459
self._is_started = True
5560
self._last_capture_time = time.monotonic()
56-
logger.info(f"Successfully started {self.__class__.__name__}")
61+
self.logger.info(f"Successfully started {self.__class__.__name__}")
5762
except Exception as e:
5863
raise CameraOpenError(f"Failed to start camera: {e}")
5964

6065
def stop(self) -> None:
6166
"""Stop the camera and release resources."""
62-
with self._cap_lock:
67+
with self._camera_lock:
6368
if not self._is_started:
6469
return
6570

6671
try:
6772
self._close_camera()
6873
self._is_started = False
69-
logger.info(f"Stopped {self.__class__.__name__}")
74+
self.logger.info(f"Stopped {self.__class__.__name__}")
7075
except Exception as e:
71-
logger.warning(f"Error stopping camera: {e}")
76+
self.logger.warning(f"Error stopping camera: {e}")
7277

7378
def capture(self) -> Optional[np.ndarray]:
7479
"""
@@ -85,13 +90,13 @@ def capture(self) -> Optional[np.ndarray]:
8590
def _extract_frame(self) -> Optional[np.ndarray]:
8691
"""Extract a frame with FPS throttling and post-processing."""
8792
# FPS throttling
88-
if self.desired_interval > 0:
93+
if self._desired_interval > 0:
8994
current_time = time.monotonic()
9095
elapsed = current_time - self._last_capture_time
91-
if elapsed < self.desired_interval:
92-
time.sleep(self.desired_interval - elapsed)
96+
if elapsed < self._desired_interval:
97+
time.sleep(self._desired_interval - elapsed)
9398

94-
with self._cap_lock:
99+
with self._camera_lock:
95100
if not self._is_started:
96101
return None
97102

src/arduino/app_peripherals/camera/camera.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ class Camera:
1515
1616
This class serves as both a factory and a wrapper, automatically creating
1717
the appropriate camera implementation based on the provided configuration.
18+
19+
Supports:
20+
- V4L Cameras (local cameras connected to the system), the default
21+
- IP Cameras (network-based cameras via RTSP, HLS)
22+
- WebSocket Cameras (input streams via WebSocket client)
23+
24+
Note: constructor arguments (except source) must be provided in keyword
25+
format to forward them correctly to the specific camera implementations.
1826
"""
1927

2028
def __new__(cls, source: Union[str, int] = 0, **kwargs) -> BaseCamera:
@@ -23,22 +31,20 @@ def __new__(cls, source: Union[str, int] = 0, **kwargs) -> BaseCamera:
2331
Args:
2432
source (Union[str, int]): Camera source identifier. Supports:
2533
- int: V4L camera index (e.g., 0, 1)
26-
- str: Camera index as string (e.g., "0", "1") for V4L
27-
- str: Device path (e.g., "/dev/video0") for V4L
34+
- str: V4L camera index (e.g., "0", "1") or device path (e.g., "/dev/video0")
2835
- str: URL for IP cameras (e.g., "rtsp://...", "http://...")
29-
- str: WebSocket URL (e.g., "ws://0.0.0.0:8080")
36+
- str: WebSocket URL for input streams (e.g., "ws://0.0.0.0:8080")
3037
**kwargs: Camera-specific configuration parameters grouped by type:
3138
Common Parameters:
3239
resolution (tuple, optional): Frame resolution as (width, height).
33-
Default: None (auto)
40+
Default: (640, 480)
3441
fps (int, optional): Target frames per second. Default: 10
3542
adjustments (callable, optional): Function pipeline to adjust frames that takes a
3643
numpy array and returns a numpy array. Default: None
3744
V4L Camera Parameters:
38-
device_index (int, optional): V4L device index override
39-
capture_format (str, optional): Video capture format (e.g., 'MJPG', 'YUYV')
40-
buffer_size (int, optional): Number of frames to buffer
45+
device (int, optional): V4L device index override. Default: 0.
4146
IP Camera Parameters:
47+
url (str): Camera stream URL
4248
username (str, optional): Authentication username
4349
password (str, optional): Authentication password
4450
timeout (float, optional): Connection timeout in seconds. Default: 10.0
@@ -54,9 +60,10 @@ def __new__(cls, source: Union[str, int] = 0, **kwargs) -> BaseCamera:
5460
5561
Raises:
5662
CameraConfigError: If source type is not supported or parameters are invalid
63+
CameraOpenError: If the camera cannot be opened
5764
5865
Examples:
59-
V4L/USB Camera:
66+
V4L Camera:
6067
6168
```python
6269
camera = Camera(0, resolution=(640, 480), fps=30)
@@ -67,17 +74,16 @@ def __new__(cls, source: Union[str, int] = 0, **kwargs) -> BaseCamera:
6774
6875
```python
6976
camera = Camera("rtsp://192.168.1.100:554/stream", username="admin", password="secret", timeout=15.0)
70-
camera = Camera("http://192.168.1.100:8080/video", retry_attempts=5)
77+
camera = Camera("http://192.168.1.100:8080/video.mp4")
7178
```
7279
7380
WebSocket Camera:
7481
7582
```python
76-
camera = Camera("ws://0.0.0.0:8080", frame_format="json", max_queue_size=20)
77-
camera = Camera("ws://192.168.1.100:8080", ping_interval=30)
83+
camera = Camera("ws://0.0.0.0:8080", frame_format="json")
84+
camera = Camera("ws://192.168.1.100:8080", timeout=5)
7885
```
7986
"""
80-
# Dynamic imports to avoid circular dependencies
8187
if isinstance(source, int) or (isinstance(source, str) and source.isdigit()):
8288
# V4L Camera
8389
from .v4l_camera import V4LCamera

src/arduino/app_peripherals/camera/ip_camera.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
import cv2
66
import numpy as np
77
import requests
8-
from typing import Optional
8+
from typing import Callable, Optional, Tuple
99
from urllib.parse import urlparse
1010

1111
from arduino.app_utils import Logger
1212

1313
from .camera import BaseCamera
14-
from .errors import CameraOpenError
14+
from .errors import CameraConfigError, CameraOpenError
1515

1616
logger = Logger("IPCamera")
1717

@@ -24,44 +24,58 @@ class IPCamera(BaseCamera):
2424
Can handle authentication and various streaming protocols.
2525
"""
2626

27-
def __init__(self, url: str, username: Optional[str] = None,
28-
password: Optional[str] = None, timeout: int = 10, **kwargs):
27+
def __init__(
28+
self,
29+
url: str,
30+
username: Optional[str] = None,
31+
password: Optional[str] = None,
32+
timeout: int = 10,
33+
resolution: Optional[Tuple[int, int]] = (640, 480),
34+
fps: int = 10,
35+
adjustments: Optional[Callable[[np.ndarray], np.ndarray]] = None,
36+
):
2937
"""
3038
Initialize IP camera.
3139
3240
Args:
33-
url: Camera stream URL (rtsp://, http://, https://)
41+
url: Camera stream URL (i.e. rtsp://..., http://..., https://...)
3442
username: Optional authentication username
3543
password: Optional authentication password
3644
timeout: Connection timeout in seconds
37-
**kwargs: Additional camera parameters propagated to BaseCamera
45+
resolution (tuple, optional): Resolution as (width, height). None uses default resolution.
46+
fps (int): Frames per second to capture from the camera.
47+
adjustments (callable, optional): Function or function pipeline to adjust frames that takes
48+
a numpy array and returns a numpy array. Default: None
3849
"""
39-
super().__init__(**kwargs)
50+
super().__init__(resolution, fps, adjustments)
4051
self.url = url
4152
self.username = username
4253
self.password = password
4354
self.timeout = timeout
55+
self.logger = logger
56+
4457
self._cap = None
58+
4559
self._validate_url()
4660

4761
def _validate_url(self) -> None:
4862
"""Validate the camera URL format."""
4963
try:
5064
parsed = urlparse(self.url)
5165
if parsed.scheme not in ['http', 'https', 'rtsp']:
52-
raise CameraOpenError(f"Unsupported URL scheme: {parsed.scheme}")
66+
raise CameraConfigError(f"Unsupported URL scheme: {parsed.scheme}")
5367
except Exception as e:
54-
raise CameraOpenError(f"Invalid URL format: {e}")
68+
raise CameraConfigError(f"Invalid URL format: {e}")
5569

5670
def _open_camera(self) -> None:
5771
"""Open the IP camera connection."""
58-
auth_url = self._build_authenticated_url()
72+
url = self._build_url()
5973

6074
# Test connectivity first for HTTP streams
6175
if self.url.startswith(('http://', 'https://')):
6276
self._test_http_connectivity()
6377

64-
self._cap = cv2.VideoCapture(auth_url)
78+
self._cap = cv2.VideoCapture(url)
6579
self._cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Reduce buffer to get latest frames
6680
if not self._cap.isOpened():
6781
raise CameraOpenError(f"Failed to open IP camera: {self.url}")
@@ -75,17 +89,15 @@ def _open_camera(self) -> None:
7589

7690
logger.info(f"Opened IP camera: {self.url}")
7791

78-
def _build_authenticated_url(self) -> str:
92+
def _build_url(self) -> str:
7993
"""Build URL with authentication if credentials provided."""
94+
# If no username or password provided as parameters, return original URL
8095
if not self.username or not self.password:
8196
return self.url
8297

8398
parsed = urlparse(self.url)
84-
if parsed.username and parsed.password:
85-
# URL already has credentials
86-
return self.url
87-
88-
# Add credentials to URL
99+
100+
# Override any URL credentials if credentials are provided
89101
auth_netloc = f"{self.username}:{self.password}@{parsed.hostname}"
90102
if parsed.port:
91103
auth_netloc += f":{parsed.port}"

0 commit comments

Comments
 (0)