6.3 KiB
6.3 KiB
Plan — macOS fix + QVideoWidget + QVideoSink dual output
Cel
Przywrócić działanie kamery na macOS (Elgato) przez:
- Przejście z manualnego
QPainterrenderowania na natywnyQVideoWidget - Dodanie
QVideoSinkjako drugiego wyjścia (frame access dla telemetrii) - Obsługa
QCameraPermission(Qt 6.5+) - Dokumentacja packagingu przez
pyside6-deployna macOS
Kolejność implementacji
1. camera/service.py
class CameraService(QObject):
error_occurred = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._session = QMediaCaptureSession()
self._camera: QCamera | None = None
def set_video_widget(self, widget: QVideoWidget) -> None:
self._session.setVideoOutput(widget)
def set_video_sink(self, sink: QVideoSink) -> None:
self._session.setVideoSink(sink)
@property
def session(self) -> QMediaCaptureSession:
return self._session
@staticmethod
def available_cameras() -> list[QCameraDevice]:
return QMediaDevices.videoInputs()
def is_active(self) -> bool:
return self._camera is not None and self._camera.isActive()
def start(self, device: QCameraDevice) -> None:
self.stop()
self._camera = QCamera(device, self)
self._camera.errorOccurred.connect(self._on_error)
self._session.setCamera(self._camera)
self._camera.start()
def stop(self) -> None:
if self._camera is not None:
self._camera.stop()
self._session.setCamera(None)
self._camera.deleteLater()
self._camera = None
def set_camera_format(self, fmt: QCameraFormat) -> None:
if self._camera is not None:
self._camera.setCameraFormat(fmt)
def _on_error(self, error, error_string):
self.error_occurred.emit(error_string)
Zmiany:
- Usunięto
self._sinkz__init__ - Usunięto
self._session.setVideoOutput(self._sink) - Dodano
set_video_widget(widget)→session.setVideoOutput(widget) - Dodano
set_video_sink(sink)→session.setVideoSink(sink)
2. rendering/video_widget.py
from __future__ import annotations
from PySide6.QtMultimediaWidgets import QVideoWidget
class VideoWidget(QVideoWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setMinimumSize(320, 240)
Zmiany:
- Dziedziczy po
QVideoWidgetzamiastQWidget - Usunięto
on_frame(frame)— nie potrzeba, renderowanie natywne - Usunięto
paintEvent— nie potrzeba, robi to Qt - Usunięto
QImage,QPainter,SmoothPixmapTransform
3. rendering/overlay.py
font = QFont("monospace", 11) # zamiast "Consolas"
Dodatkowo: obsługa błędu w paintEvent:
- Jeśli
self._metricsma klucz"error"→ pomiń normalne metryki, narysuj czerwony komunikat błędu - Użyj
QColor(180, 40, 40)zamiast zielonego
4. app.py — nowe DI wiring
def main():
app = QApplication(sys.argv)
app.setApplicationName("Duck Preview")
camera = CameraService()
video_sink = QVideoSink()
dispatcher = FrameDispatcher()
telemetry = TelemetryCollector()
video_widget = VideoWidget()
overlay = OverlayWidget()
camera.set_video_widget(video_widget)
camera.set_video_sink(video_sink)
video_sink.videoFrameChanged.connect(dispatcher.on_frame)
dispatcher.subscribe(telemetry.on_frame)
window = MainWindow(camera, video_widget, overlay)
# Wire error → overlay
camera.error_occurred.connect(
lambda msg: overlay.set_metrics({"error": msg})
)
# Poll telemetry → overlay
metrics_timer = QTimer()
metrics_timer.timeout.connect(
lambda: overlay.set_metrics(telemetry.metrics())
)
metrics_timer.start(200)
window.show()
sys.exit(app.exec())
Zmiany:
- Tworzy
QVideoSinkjawnie camera.set_video_widget(video_widget)+camera.set_video_sink(video_sink)video_sink.videoFrameChanged → dispatcher.on_frame- Usunięto
camera.sink(property nie istnieje) camera.error_occurred → overlay.set_metrics({"error": msg})
5. main_window.py — permission flow
# Importy
from PySide6.QtCore import QCameraPermission, Qt
from PySide6.QtWidgets import QApplication
# W _on_camera_selected:
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)
6. pyside6-deploy.toml
[app]
script = "duck_preview/__main__.py"
name = "Duck Preview"
bundle_identifier = "com.bartool.duck-preview"
categories = "public.app-category.photography"
platforms = ["macos"]
7. Po zmianach
ruff check .— czystyruff format .— sformatowanypytest -q— 5 passed
Podsumowanie architektury końcowej
CameraService
├─ session.setVideoOutput(VideoWidget) → natywne GPU rendering
└─ session.setVideoSink(VideoSink) → frame access
└─ videoFrameChanged
→ FrameDispatcher
├─ TelemetryCollector
└─ [future AI]
MainWindow
├─ _on_camera_selected
│ → QCameraPermission check (Undetermined/Denied/Granted)
│ → CameraService.start(device)
├─ error_occurred → OverlayWidget.set_metrics({"error": ...})
└─ QTimer 200ms → TelemetryCollector.metrics() → OverlayWidget
OverlayWidget
├─ normal: zielone metryki (FPS, frame time, frame count)
└─ error: czerwony komunikat (np. brak permisji)