133 lines
4.6 KiB
Python
133 lines
4.6 KiB
Python
"""Camera enumeration — discovers available video input devices."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
|
|
from PySide6.QtMultimedia import QCameraDevice, QMediaDevices, QVideoFrameFormat
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Map Qt pixel format enum → human-readable string
|
|
_PIXEL_FORMAT_NAMES: dict[QVideoFrameFormat.PixelFormat, str] = {
|
|
QVideoFrameFormat.PixelFormat.Format_BGRA8888: "BGRA",
|
|
QVideoFrameFormat.PixelFormat.Format_BGRX8888: "BGRX",
|
|
QVideoFrameFormat.PixelFormat.Format_ABGR8888: "ABGR",
|
|
QVideoFrameFormat.PixelFormat.Format_ARGB8888: "ARGB",
|
|
QVideoFrameFormat.PixelFormat.Format_RGBA8888: "RGBA",
|
|
QVideoFrameFormat.PixelFormat.Format_RGBX8888: "RGBX",
|
|
QVideoFrameFormat.PixelFormat.Format_YUV420P: "YUV420P",
|
|
QVideoFrameFormat.PixelFormat.Format_YUV422P: "YUV422P",
|
|
QVideoFrameFormat.PixelFormat.Format_NV12: "NV12",
|
|
QVideoFrameFormat.PixelFormat.Format_NV21: "NV21",
|
|
QVideoFrameFormat.PixelFormat.Format_UYVY: "UYVY",
|
|
QVideoFrameFormat.PixelFormat.Format_YUYV: "YUY2",
|
|
QVideoFrameFormat.PixelFormat.Format_Jpeg: "MJPG",
|
|
QVideoFrameFormat.PixelFormat.Format_Y8: "GRAY8",
|
|
QVideoFrameFormat.PixelFormat.Format_Y16: "GRAY16",
|
|
}
|
|
|
|
|
|
def pixel_format_name(fmt: QVideoFrameFormat.PixelFormat) -> str:
|
|
"""Return a short human-readable name for a Qt pixel format."""
|
|
return _PIXEL_FORMAT_NAMES.get(fmt, f"fmt_{fmt.value if hasattr(fmt, 'value') else fmt}")
|
|
|
|
|
|
@dataclass
|
|
class CameraFormat:
|
|
"""One supported video format entry for a camera device."""
|
|
|
|
width: int
|
|
height: int
|
|
max_fps: float
|
|
pixel_format: str # e.g. "MJPG", "YUY2", "NV12", "BGRA"
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.pixel_format:6s} {self.width}x{self.height} @ {self.max_fps:.1f} fps"
|
|
|
|
|
|
@dataclass
|
|
class CameraInfo:
|
|
"""Lightweight descriptor of a detected camera."""
|
|
|
|
device: QCameraDevice
|
|
name: str
|
|
id: str
|
|
formats: list[CameraFormat] = field(default_factory=list)
|
|
|
|
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[CameraFormat] = []
|
|
for qfmt in device.videoFormats():
|
|
res = qfmt.resolution()
|
|
formats.append(CameraFormat(
|
|
width=res.width(),
|
|
height=res.height(),
|
|
max_fps=qfmt.maxFrameRate(),
|
|
pixel_format=pixel_format_name(qfmt.pixelFormat()),
|
|
))
|
|
|
|
# deduplicate; sort: largest area first, then fps descending
|
|
seen: set[tuple[int, int, float, str]] = set()
|
|
unique: list[CameraFormat] = []
|
|
for f in sorted(
|
|
formats,
|
|
key=lambda x: (x.width * x.height, x.max_fps),
|
|
reverse=True,
|
|
):
|
|
key = (f.width, f.height, f.max_fps, f.pixel_format)
|
|
if key not in seen:
|
|
seen.add(key)
|
|
unique.append(f)
|
|
|
|
cam_id = CameraEnumerator._device_id(device)
|
|
cameras.append(CameraInfo(
|
|
device=device,
|
|
name=device.description(),
|
|
id=cam_id,
|
|
formats=unique,
|
|
))
|
|
|
|
logger.info("Cameras found: %d", len(cameras))
|
|
for idx, cam in enumerate(cameras):
|
|
logger.info(" [%d] %s (id: %s)", idx, cam.name, cam.id)
|
|
for fmt in cam.formats:
|
|
logger.info(" %s", fmt)
|
|
|
|
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
|
|
|
|
default_id = CameraEnumerator._device_id(device)
|
|
for cam in CameraEnumerator.list_cameras():
|
|
if cam.id == default_id:
|
|
return cam
|
|
|
|
cameras = CameraEnumerator.list_cameras()
|
|
return cameras[0] if cameras else None
|
|
|
|
@staticmethod
|
|
def _device_id(device: QCameraDevice) -> str:
|
|
raw = device.id()
|
|
if hasattr(raw, "toStdString"):
|
|
return raw.toStdString()
|
|
return raw.data().decode("utf-8", errors="replace")
|