Files
duck-preview/notes/01-mvp-plan.md
bartool cd7f196b25 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
2026-05-12 19:49:53 +02:00

9.4 KiB

Plan działania — MVP Camera Preview (PySide6)

Środowisko

Element Wartość
Python 3.12.10 (venv: .venv-win)
Framework GUI PySide6 6.11.0
Dev platform Windows 11
Target platform Mac Mini (Intel i7, macOS Ventura)
Kamera docelowa ELP USB Camera
Narzędzia pytest, ruff, colorama

Fazy realizacji

Faza 0 — Projekt i scaffolding

Cel: ustalenie struktury katalogów i modułów przed napisaniem pierwszej linii logiki.

0.1 Struktura projektu

duck-preview2/
├── app/
│   ├── __init__.py
│   ├── main.py                  # entry point
│   ├── config.py                # stałe, domyślne ustawienia
│   ├── camera/
│   │   ├── __init__.py
│   │   ├── camera_service.py    # QCamera + QMediaCaptureSession
│   │   └── camera_enumerator.py # wykrywanie dostępnych kamer
│   ├── pipeline/
│   │   ├── __init__.py
│   │   └── frame_dispatcher.py  # dystrybucja klatek do subskrybentów
│   ├── telemetry/
│   │   ├── __init__.py
│   │   └── telemetry_collector.py # zbieranie metryk FPS/frame time/CPU
│   ├── overlay/
│   │   ├── __init__.py
│   │   └── overlay_widget.py    # przezroczysta warstwa QWidget
│   └── ui/
│       ├── __init__.py
│       ├── main_window.py       # główne okno aplikacji
│       └── menu_bar.py          # menu: kamera, rozdzielczość, FPS, debug
├── tests/
│   ├── __init__.py
│   ├── test_camera_enumerator.py
│   └── test_telemetry_collector.py
├── notes/
├── requirements.txt
├── requirements-dev.txt
└── pyproject.toml               # konfiguracja ruff + pytest

0.2 Pliki konfiguracyjne

  • pyproject.toml — konfiguracja ruff (linter/formatter) i pytest
  • requirements.txt — zależności produkcyjne (PySide6)
  • requirements-dev.txt — zależności deweloperskie (pytest, ruff)
  • .gitignore — aktualizacja o artefakty Pythona

Faza 1 — Camera Service

Cel: stabilne pobranie obrazu z kamery przez QtMultimedia.

1.1 Camera Enumerator

  • QMediaDevices.videoInputs() — lista dostępnych kamer
  • Zwraca listę QCameraDevice z nazwą, id i obsługiwanymi formatami
  • Obsługa braku kamer (komunikat, nie crash)
  • Test jednostkowy: mockowanie QMediaDevices

1.2 Camera Service

  • Opakowuje QCamera + QMediaCaptureSession
  • API:
    • start(device: QCameraDevice) — uruchamia kamerę
    • stop() — zatrzymuje kamerę
    • set_resolution(width, height) — ustawia format
    • set_fps(fps) — ustawia docelowy FPS
    • reconnect() — restart po błędzie
  • QVideoSink jako punkt odbioru klatek
  • Sygnał frame_ready(QVideoFrame) do Frame Dispatcher
  • Obsługa błędów kamery (QCamera.errorOccurred)

1.3 Uwagi platformowe

Aspekt Windows 11 (dev) macOS Ventura (target)
Backend DirectShow / Media Foundation AVFoundation
Kamera ELP USB, standardowy UVC driver USB, UVC
Format klatek YUYV / MJPEG YUYV / MJPEG
GPU rendering ANGLE (OpenGL ES) Metal

Faza 2 — Frame Dispatcher

Cel: dystrybucja klatek do wielu odbiorców bez blokowania akwizycji.

2.1 Frame Dispatcher

  • Wzorzec: publish-subscribe (lista callbacków)
  • subscribe(callback: Callable[[QVideoFrame], None])
  • unsubscribe(callback)
  • dispatch(frame: QVideoFrame) — wywołuje wszystkich subskrybentów
  • Klatki NIE są kopiowane — subskrybenci działają na referencji
  • Subskrybenci mogą pominąć klatkę (tryb drop-if-busy)
  • Wywołanie dispatch następuje w wątku GUI (slot połączony z frame_ready)

2.2 Subskrybenci w Fazie 1

Subskrybent Działanie
Video Renderer przekazuje klatkę do QVideoSink / QVideoWidget
Telemetry Collector mierzy czas, zlicza klatki

Faza 3 — Video Renderer

Cel: renderowanie klatki w GUI bez zbędnych kopii.

3.1 Podejście

  • QVideoWidget jako główny widget podglądu
  • QMediaCaptureSession.setVideoOutput(QVideoWidget) — ścieżka bezpośrednia, zero kopii
  • Alternatywnie: QVideoSinkQGraphicsVideoItem dla przyszłych overlayów
  • Domyślnie: QVideoWidget (prosta, niska latencja)

3.2 Wymagania

  • Preview nie blokuje wątku GUI
  • Obsługa aspect ratio (letter/pillarbox)
  • Resize okna bez migotania

Faza 4 — Telemetry Collector

Cel: dokładne metryki pipeline'u wideo.

4.1 Zbierane metryki

Metryka Metoda pomiaru
Realtime FPS licznik klatek / okno 1 s
Frame time time.perf_counter() między klatkami
Frame acquisition time timestamp wejście frame_ready → dispatch
Rendering time czas QVideoWidget.update() (opcjonalnie)
Dropped frames detekcja przez numerację lub timestamp gap
CPU usage psutil.cpu_percent() (dodać do requirements)
Memory usage psutil.virtual_memory() (opcjonalnie)

4.2 API

  • TelemetryCollector — subskrybent Frame Dispatcher
  • on_frame(frame: QVideoFrame) — rejestruje timestamp klatki
  • get_snapshot() -> TelemetrySnapshot — aktualny stan metryk (dataclass)
  • update_interval_ms: int — jak często odświeżać snapshot (domyślnie 500 ms)
  • Sygnał metrics_updated(TelemetrySnapshot) — emitowany co update_interval_ms

4.3 TelemetrySnapshot (dataclass)

@dataclass
class TelemetrySnapshot:
    fps: float
    frame_time_ms: float
    dropped_frames: int
    cpu_percent: float
    memory_mb: float | None
    timestamp: float

Faza 5 — Overlay System

Cel: wyświetlanie metryk na przezroczystej warstwie nad podglądem.

5.1 Architektura

  • OverlayWidget(QWidget) — przezroczysty widget (WA_TransparentForMouseEvents)
  • Pozycjonowany absolutnie nad QVideoWidget (ten sam parent, wyższy z-index)
  • paintEvent rysuje semi-przezroczysty prostokąt + tekst z metrykami
  • Połączony z sygnałem metrics_updated — odświeża tylko gdy dane się zmienią

5.2 Zawartość overlaya (MVP)

FPS:    60.0
Frame:  16.7 ms
Drop:   0
CPU:    12.3 %

5.3 Sterowalność

  • Widoczność overlaya: toggle przez menu Debug
  • Pozycja: lewy górny róg (stała w MVP)
  • Kolor tła: rgba(0, 0, 0, 160)

Faza 6 — GUI / Main Window

Cel: minimalne, funkcjonalne okno aplikacji.

6.1 MainWindow

  • QMainWindow z QVideoWidget jako central widget
  • OverlayWidget nałożony na video
  • Obsługa resize → reposition overlay
  • Tytuł okna: Duck Preview

6.2 MenuBar

Menu Camera:

  • Lista wykrytych kamer (radio-style)
  • Separator
  • Reconnect

Menu Video:

  • Resolution submenu (pobierane dynamicznie z QCameraDevice.videoFormats())
  • FPS submenu

Menu Debug:

  • Toggle overlay metryk
  • Logowanie do konsoli (toggle)

6.3 Startup flow

main.py
  → QApplication
  → CameraEnumerator.list_cameras()
  → MainWindow(cameras)
  → CameraService.start(cameras[0])  # pierwsza kamera lub ELP
  → FrameDispatcher.subscribe(telemetry, renderer)
  → app.exec()

Faza 7 — Testy i walidacja

7.1 Testy jednostkowe

Moduł Co testować
CameraEnumerator lista kamer, brak kamer, format danych
TelemetryCollector obliczenia FPS, wykrywanie dropów
FrameDispatcher subskrypcja, odsubskrypcja, dispatch
TelemetrySnapshot poprawność dataclass

7.2 Testy manualne (Windows dev)

  • Uruchomienie z kamerą laptopa / USB webcam
  • Przełączanie kamer
  • Zmiana rozdzielczości
  • Zmiana FPS
  • Toggle overlay
  • Reconnect po odłączeniu kamery

7.3 Testy na Mac Mini (target)

  • Wykrycie kamery ELP
  • Poprawny format YUYV/MJPEG
  • Wydajność AVFoundation vs DirectShow
  • GPU rendering przez Metal

7.4 Kryteria sukcesu (z PRD)

  • Preview stabilny i płynny
  • Latencja renderowania niska
  • Dane telemetrii dokładne
  • GUI responsywne
  • Overlay działa poprawnie
  • Architektura gotowa na subskrybentów AI

Kolejność implementacji (sprint order)

Sprint 1:  Faza 0  — scaffolding, pyproject.toml, requirements
Sprint 2:  Faza 1  — CameraEnumerator + CameraService (bez GUI)
Sprint 3:  Faza 3  — VideoRenderer + MainWindow (preview działa)
Sprint 4:  Faza 2  — FrameDispatcher (refactor pipeline)
Sprint 5:  Faza 4  — TelemetryCollector
Sprint 6:  Faza 5  — OverlayWidget
Sprint 7:  Faza 6  — MenuBar (camera/resolution/fps switch)
Sprint 8:  Faza 7  — Testy, poprawki, walidacja na Mac Mini

Zależności do dodania

# requirements.txt
PySide6>=6.7
psutil>=6.0

# requirements-dev.txt
pytest>=8.0
ruff>=0.4

Uwagi cross-platform

  1. ELP camera — kamera UVC, powinna działać bez dodatkowych sterowników na obu platformach. Sprawdzić obsługiwane rozdzielczości i FPS przez QCameraDevice.videoFormats().
  2. Ścieżki absolutne — unikać os.path na korzyść pathlib.Path.
  3. Threading — wszystkie operacje Qt muszą odbywać się w wątku GUI. TelemetryCollector może używać QTimer zamiast osobnego wątku.
  4. Format klatek — na macOS AVFoundation preferuje BGRA lub NV12. Konwersja powinna być leniwa i tylko gdy potrzebna (nie w hot path renderowania).
  5. High DPI — włączyć QApplication.setHighDpiScaleFactorRoundingPolicy dla konsistencji Windows/Mac.
  6. Testowanie bez kameryCameraEnumerator powinien umożliwiać dependency injection / mock dla środowisk CI.