from __future__ import annotations from PySide6.QtCore import QCameraPermission, Qt from PySide6.QtGui import QAction, QCloseEvent from PySide6.QtMultimedia import QCameraDevice, QMediaDevices from PySide6.QtWidgets import QApplication, QGridLayout, QMainWindow, QWidget from duck_preview.camera.service import CameraService from duck_preview.rendering.overlay import OverlayWidget from duck_preview.rendering.video_widget import VideoWidget class MainWindow(QMainWindow): def __init__( self, camera_service: CameraService, video_widget: VideoWidget, overlay_widget: OverlayWidget, parent: QWidget | None = None, ) -> None: super().__init__(parent) self._camera = camera_service self._video_widget = video_widget self._overlay = overlay_widget self._media_devices = QMediaDevices() self.setWindowTitle("Duck Preview") self.resize(1280, 720) central = QWidget() self.setCentralWidget(central) layout = QGridLayout(central) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(video_widget, 0, 0) layout.addWidget(overlay_widget, 0, 0) self._setup_menus() self._media_devices.videoInputsChanged.connect(self._rebuild_camera_menu) def _setup_menus(self) -> None: menu_bar = self.menuBar() self._camera_menu = menu_bar.addMenu("Camera") self._rebuild_camera_menu() self._resolution_menu = menu_bar.addMenu("Resolution") self._resolution_menu.setEnabled(False) self._fps_menu = menu_bar.addMenu("FPS") self._fps_menu.setEnabled(False) debug_menu = menu_bar.addMenu("Debug") toggle_overlay = QAction("Show Metrics", self) toggle_overlay.setCheckable(True) toggle_overlay.setChecked(True) toggle_overlay.triggered.connect(self._overlay.set_visible) debug_menu.addAction(toggle_overlay) def _rebuild_camera_menu(self) -> None: self._camera_menu.clear() cameras = CameraService.available_cameras() for device in cameras: action = QAction(device.description(), self) action.triggered.connect(lambda checked, d=device: self._on_camera_selected(d)) self._camera_menu.addAction(action) if not cameras: action = QAction("No cameras detected", self) action.setEnabled(False) self._camera_menu.addAction(action) def _on_camera_selected(self, device: QCameraDevice) -> None: self._request_camera_permission(device) def _request_camera_permission(self, device: QCameraDevice) -> None: perm = QCameraPermission() match QApplication.checkPermission(perm): case Qt.PermissionStatus.Undetermined: QApplication.requestPermission( perm, self, lambda: self._request_camera_permission(device) ) case Qt.PermissionStatus.Denied: self._overlay.set_metrics( { "error": ( "Camera permission denied.\n" "Grant access in System Settings > " "Privacy & Security > Camera" ) } ) case Qt.PermissionStatus.Granted: self._camera.start(device) self._rebuild_resolution_menu(device) def _rebuild_resolution_menu(self, device: QCameraDevice) -> None: self._resolution_menu.clear() self._resolution_menu.setEnabled(True) formats = device.videoFormats() seen: set[tuple[int, int]] = set() for fmt in formats: res = fmt.resolution() key = (res.width(), res.height()) if key not in seen: seen.add(key) action = QAction(f"{res.width()}x{res.height()}", self) action.triggered.connect( lambda checked, d=device, w=res.width(), h=res.height(): ( self._on_resolution_selected(d, w, h) ) ) self._resolution_menu.addAction(action) def _on_resolution_selected(self, device: QCameraDevice, width: int, height: int) -> None: self._rebuild_fps_menu(device, width, height) def _rebuild_fps_menu(self, device: QCameraDevice, width: int, height: int) -> None: self._fps_menu.clear() self._fps_menu.setEnabled(True) formats = device.videoFormats() seen_labels: set[str] = set() for fmt in formats: res = fmt.resolution() if res.width() == width and res.height() == height: min_fps = round(fmt.minFrameRate()) max_fps = round(fmt.maxFrameRate()) label = f"{max_fps} FPS" if min_fps == max_fps else f"{min_fps}-{max_fps} FPS" if label not in seen_labels: seen_labels.add(label) action = QAction(label, self) action.triggered.connect( lambda checked, f=fmt: self._camera.set_camera_format(f) ) self._fps_menu.addAction(action) def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 self._camera.stop() super().closeEvent(event)