feat: implement core functionality for camera preview application
- Add FrameDispatcher for distributing QVideoFrames to subscribers - Implement TelemetryCollector to measure video pipeline performance metrics - Create MainWindow as the main application interface with video rendering - Develop AppMenuBar for camera selection, resolution, and FPS settings - Establish overlay system for displaying telemetry metrics - Set up project structure and configuration files - Add unit tests for FrameDispatcher and TelemetryCollector
This commit is contained in:
80
app/camera/camera_enumerator.py
Normal file
80
app/camera/camera_enumerator.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""Camera enumeration — discovers available video input devices."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from PySide6.QtMultimedia import QCameraDevice, QMediaDevices
|
||||
|
||||
|
||||
@dataclass
|
||||
class CameraInfo:
|
||||
"""Lightweight descriptor of a detected camera."""
|
||||
|
||||
device: QCameraDevice
|
||||
name: str
|
||||
id: str
|
||||
formats: list[tuple[int, int, float]] = field(default_factory=list)
|
||||
# formats: list of (width, height, max_fps)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name} [{self.id}]"
|
||||
|
||||
|
||||
class CameraEnumerator:
|
||||
"""Discovers available video input devices via QMediaDevices."""
|
||||
|
||||
@staticmethod
|
||||
def list_cameras() -> list[CameraInfo]:
|
||||
"""Return all available camera devices with their supported formats."""
|
||||
devices = QMediaDevices.videoInputs()
|
||||
cameras: list[CameraInfo] = []
|
||||
|
||||
for device in devices:
|
||||
formats: list[tuple[int, int, float]] = []
|
||||
for fmt in device.videoFormats():
|
||||
res = fmt.resolution()
|
||||
fps = fmt.maxFrameRate()
|
||||
formats.append((res.width(), res.height(), fps))
|
||||
|
||||
# deduplicate and sort: largest resolution first, then fps descending
|
||||
seen: set[tuple[int, int, float]] = set()
|
||||
unique_formats: list[tuple[int, int, float]] = []
|
||||
for f in sorted(formats, key=lambda x: (x[0] * x[1], x[2]), reverse=True):
|
||||
if f not in seen:
|
||||
seen.add(f)
|
||||
unique_formats.append(f)
|
||||
|
||||
cameras.append(
|
||||
CameraInfo(
|
||||
device=device,
|
||||
name=device.description(),
|
||||
id=device.id().toStdString()
|
||||
if hasattr(device.id(), "toStdString")
|
||||
else device.id().data().decode("utf-8", errors="replace"),
|
||||
formats=unique_formats,
|
||||
)
|
||||
)
|
||||
|
||||
return cameras
|
||||
|
||||
@staticmethod
|
||||
def default_camera() -> CameraInfo | None:
|
||||
"""Return the system default camera, or None if no camera is available."""
|
||||
device = QMediaDevices.defaultVideoInput()
|
||||
if device.isNull():
|
||||
return None
|
||||
|
||||
cameras = CameraEnumerator.list_cameras()
|
||||
# find by id match
|
||||
default_id = (
|
||||
device.id().toStdString()
|
||||
if hasattr(device.id(), "toStdString")
|
||||
else device.id().data().decode("utf-8", errors="replace")
|
||||
)
|
||||
for cam in cameras:
|
||||
if cam.id == default_id:
|
||||
return cam
|
||||
|
||||
# fallback: wrap directly
|
||||
return cameras[0] if cameras else None
|
||||
Reference in New Issue
Block a user