# 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: `QVideoSink` → `QGraphicsVideoItem` 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) ```python @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 kamery** — `CameraEnumerator` powinien umożliwiać dependency injection / mock dla środowisk CI.