"""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