refractor capture to a module

This commit is contained in:
ykcheungab
2026-08-23 15:53:02 +08:00
parent 4583dc3a40
commit 8a7760f12b
30 changed files with 1992 additions and 332 deletions
+158
View File
@@ -0,0 +1,158 @@
"""Capture configuration, health state, and reconnect policy."""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Optional, Sequence, Tuple, Union
MAX_SIMULTANEOUS_CAPTURES = 10
MAX_QUEUE_SIZE = 10
DEFAULT_QUEUE_SIZE = 1
class CaptureMode(str, Enum):
"""Frame delivery mode for a live capture worker."""
LATEST = "latest"
QUEUE = "queue"
class OverflowPolicy(str, Enum):
"""Behavior when a bounded queue is full."""
DROP_OLDEST = "drop_oldest"
DROP_NEWEST = "drop_newest"
BLOCK = "block"
class CaptureHealth(str, Enum):
"""Lifecycle / health state for a capture source."""
CLOSED = "closed"
CONNECTING = "connecting"
CONNECTED = "connected"
RECONNECTING = "reconnecting"
FAILED = "failed"
SourceType = Union[int, str]
@dataclass(frozen=True)
class ReconnectPolicy:
"""Auto-reconnect settings for live cameras.
Parameters
----------
enabled:
When False, failed reads do not attempt reopen.
max_attempts:
Maximum reopen attempts per disconnect episode. ``None`` means unlimited.
initial_backoff_s:
First wait before a reconnect attempt.
max_backoff_s:
Upper bound for exponential backoff.
fail_threshold:
Consecutive failed reads that trigger reconnect.
stale_timeout_s:
If no frame arrives for this many seconds while connected, treat as stale
and reconnect. ``None`` disables stale detection.
"""
enabled: bool = False
max_attempts: Optional[int] = None
initial_backoff_s: float = 0.25
max_backoff_s: float = 5.0
fail_threshold: int = 5
stale_timeout_s: Optional[float] = 2.0
def next_backoff(self, attempt: int) -> float:
"""Exponential backoff capped at ``max_backoff_s`` (attempt is 0-based)."""
delay = self.initial_backoff_s * (2 ** max(0, attempt))
return min(delay, self.max_backoff_s)
@dataclass
class CaptureConfig:
"""Configuration for opening and running a capture source."""
source: SourceType
backend: str = "auto"
width: Optional[int] = None
height: Optional[int] = None
fps: Optional[float] = None
fourcc: Optional[str] = "MJPG"
mode: CaptureMode = CaptureMode.LATEST
queue_size: int = DEFAULT_QUEUE_SIZE
overflow: OverflowPolicy = OverflowPolicy.DROP_OLDEST
reconnect: ReconnectPolicy = field(default_factory=ReconnectPolicy)
start_worker: bool = True
verbose: bool = False
# Optional discovery filters (used by list/open helpers, not open itself).
name_include: Optional[Sequence[str]] = None
name_exclude: Optional[Sequence[str]] = None
vid_pid_allowlist: Optional[Sequence[Tuple[int, int]]] = None
min_width: Optional[int] = None
min_height: Optional[int] = None
def __post_init__(self) -> None:
if isinstance(self.mode, str):
self.mode = CaptureMode(self.mode)
if isinstance(self.overflow, str):
self.overflow = OverflowPolicy(self.overflow)
if not isinstance(self.reconnect, ReconnectPolicy):
raise TypeError("reconnect must be a ReconnectPolicy")
size = int(self.queue_size)
if size < 1 or size > MAX_QUEUE_SIZE:
raise ValueError(f"queue_size must be in 1..{MAX_QUEUE_SIZE}, got {size}")
self.queue_size = size
if self.mode == CaptureMode.LATEST:
# Latest mode keeps only one published frame regardless of queue_size.
self.queue_size = 1
@classmethod
def from_camera_dict(
cls,
source: SourceType,
camera: dict[str, Any],
*,
mode: CaptureMode | str = CaptureMode.LATEST,
reconnect: Optional[ReconnectPolicy] = None,
verbose: bool = False,
**kwargs: Any,
) -> "CaptureConfig":
"""Build config from a sensor JSON ``camera`` section."""
res = camera.get("resolution_raw") or [None, None]
width = int(res[0]) if res and res[0] else None
height = int(res[1]) if res and res[1] else None
fps = camera.get("fps")
return cls(
source=source,
width=width,
height=height,
fps=float(fps) if fps is not None else None,
mode=mode,
reconnect=reconnect or ReconnectPolicy(),
verbose=verbose,
**kwargs,
)
@dataclass
class CaptureStatus:
"""Snapshot of capture health for diagnostics / multi-camera UIs."""
health: CaptureHealth = CaptureHealth.CLOSED
source: Optional[SourceType] = None
last_error: Optional[str] = None
last_frame_time: Optional[float] = None
last_seq: int = 0
dropped_frames: int = 0
reconnect_attempts: int = 0
consecutive_failures: int = 0
width: int = 0
height: int = 0
fps: float = 0.0
backend_name: str = ""
fourcc: str = ""