# Stan aplikacji — MVP Camera Preview Data: 2026-05-12 Środowisko dev: Windows 11, Python 3.12.10, PySide6 6.11.0 Środowisko docelowe: Mac Mini (Intel i7, macOS Ventura), kamera ELP USB --- ## Zaimplementowane moduły ### Struktura projektu ``` duck-preview2/ ├── app/ │ ├── config.py # stałe i domyślne ustawienia │ ├── main.py # entry point (python -m app.main) │ ├── camera/ │ │ ├── camera_enumerator.py # wykrywanie kamer przez QMediaDevices │ │ └── camera_service.py # zarządzanie QCamera + QMediaCaptureSession │ ├── pipeline/ │ │ └── frame_dispatcher.py # pub/sub dystrybucja QVideoFrame │ ├── telemetry/ │ │ └── telemetry_collector.py # pomiar FPS, frame time, CPU, pamięci │ ├── overlay/ │ │ ├── overlay_layer.py # interfejs IOverlayLayer (ABC) │ │ └── telemetry_overlay.py # implementacja — box z metrykami │ └── ui/ │ ├── camera_view.py # CameraView — render + kompozycja overlayów │ ├── main_window.py # główne okno, wiring komponentów │ └── menu_bar.py # menu: Camera / Video / Debug ├── tests/ │ ├── test_frame_dispatcher.py # 8 testów jednostkowych │ └── test_telemetry_collector.py # 7 testów jednostkowych ├── pyproject.toml # konfiguracja pytest + ruff ├── requirements.txt # PySide6, psutil └── requirements-dev.txt # + pytest, ruff ``` ### Opis komponentów #### CameraEnumerator (`app/camera/camera_enumerator.py`) - Wykrywa dostępne kamery przez `QMediaDevices.videoInputs()` - Zwraca listę `CameraInfo` z nazwą, id i listą obsługiwanych formatów (rozdzielczość × FPS) - Formaty deduplikowane i posortowane (największa rozdzielczość pierwsza) - Obsługa braku kamer bez crasha #### CameraService (`app/camera/camera_service.py`) - Opakowuje `QCamera` + `QMediaCaptureSession` + `QVideoSink` - API: `start(CameraInfo)`, `stop()`, `reconnect()`, `set_resolution()`, `set_fps()` - Algorytm doboru formatu: score-based (priorytet: dopasowanie rozdzielczości, potem FPS) - Sygnały: `frame_ready(QVideoFrame)`, `camera_started`, `camera_stopped`, `camera_error` #### FrameDispatcher (`app/pipeline/frame_dispatcher.py`) - Pub/sub: `subscribe(callback, drop_if_busy=True)` / `unsubscribe(callback)` - `drop_if_busy=True` — klatka pomijana jeśli subskrybent jest zajęty (render) - `drop_if_busy=False` — każda klatka dociera (telemetria) - Odporny na wyjątki w subskrybentach (jeden nie blokuje pozostałych) #### TelemetryCollector (`app/telemetry/telemetry_collector.py`) - Subskrybuje każdą klatkę (`drop_if_busy=False`) - Mierzy: FPS (okno 1 s), średni frame time (ring-buffer 120 próbek), dropped frames (heurystyka 2.5× avg) - CPU: `psutil.Process.cpu_percent()` — tylko nasz proces, inicjalizowany w `__init__` (warmup) - RAM: `memory_info().wset` (Windows private working set) lub `rss` (macOS/Linux) - Emituje `metrics_updated(TelemetrySnapshot)` co 500 ms przez `QTimer` #### IOverlayLayer (`app/overlay/overlay_layer.py`) - Abstrakcja (ABC) dla pluggable overlayów - Interface: `paint(painter: QPainter, video_rect: QRect)`, `visible: bool`, `name: str` - Nowe overlaye nie wymagają modyfikacji żadnego istniejącego kodu #### TelemetryOverlay (`app/overlay/telemetry_overlay.py`) - Implementacja `IOverlayLayer` - Rysuje semi-przezroczysty box z metrykami w lewym górnym rogu video - Slot `on_metrics_updated(TelemetrySnapshot)` — odbiera dane z TelemetryCollector - Format: FPS, Frame time, Drop count, CPU %, Mem MB #### CameraView (`app/ui/camera_view.py`) - Zwykły `QWidget` (nie `QVideoWidget`) — render przez `QPainter` w `paintEvent` - Odbiera `QVideoFrame` przez slot `on_frame()`, konwertuje do `QImage.Format_RGB32` - Letterboxing z zachowaniem aspect ratio - Rejestr overlayów: `add_overlay_layer()`, `remove_overlay_layer()`, `set_all_overlays_visible()` - W `paintEvent`: rysuje klatkę → iteruje po warstwach, każda dostaje `painter.save()/restore()` #### MainWindow (`app/ui/main_window.py`) - Wires together wszystkie komponenty - Minimalny status bar (nazwa kamery, błędy) - `closeEvent` → `CameraService.stop()` #### AppMenuBar (`app/ui/menu_bar.py`) - Menu **Camera**: lista wykrytych kamer (radio), Reconnect - Menu **Video**: Resolution submenu, FPS submenu (pobierane z `QCameraDevice.videoFormats()`) - Menu **Debug**: Show Overlay (toggle), Console Logging (toggle poziom logowania) --- ## Co działało na Windows 11 (dev) - Wykrycie wbudowanej kamery laptopa - Podgląd w czasie rzeczywistym - Przełączanie rozdzielczości i FPS z menu - Overlay z metrykami widoczny i aktualizowany - Toggle overlay przez Debug menu - Logowanie do konsoli przez Debug menu - Reconnect po zmianie kamery - Letterboxing przy resize okna --- ## Co próbowaliśmy i nie wyszło ### QVideoWidget jako renderer (porzucone) **Podejście:** Użycie `QVideoWidget` jako centralnego widgetu + nałożenie `OverlayWidget` (child lub sibling). **Problem:** Na Windows `QVideoWidget` tworzy natywne okno HWND z powierzchnią D3D (Media Foundation backend). Natywna powierzchnia jest rysowana poza hierarchią Qt i zawsze przykrywa wszystkie `QWidget` — niezależnie od: - kolejności z-order (`raise_()`) - rodzica (child/sibling) - flag okna - `WA_TranslucentBackground` / `WA_NoSystemBackground` Żaden `QWidget` nie może się wyświetlić nad `QVideoWidget` na Windows. **Próby ratowania:** 1. `OverlayWidget` jako child `QVideoWidget` — zasłonięty przez natywną powierzchnię 2. Kontener `QWidget` z `QVideoWidget` i `OverlayWidget` jako rodzeństwo — nadal zasłonięty 3. `setWindowFlags(FramelessWindowHint)` na child widget — odrywa widget od rodzica, tworzy osobne (niewidoczne) okno top-level **Rozwiązanie:** Porzucenie `QVideoWidget`. Własny `CameraView(QWidget)` odbiera klatki przez `QVideoSink`, konwertuje do `QImage` i rysuje przez `QPainter`. Overlay w tym samym `paintEvent` — brak konfliktu z natywnym renderowaniem. ### OverlayWidget jako osobny QWidget (porzucone) **Podejście:** `OverlayWidget` z `WA_TransparentForMouseEvents` i `WA_TranslucentBackground` nałożony na video. **Problem:** Poza konfliktem z `QVideoWidget`, `WA_TranslucentBackground` na child widget działa tylko gdy rodzic też jest transparentny — Qt nie komponuje dziecka z tłem rodzica innym niż własne. W praktyce overlay był niewidoczny lub zasłaniał video czarnym prostokątem. --- ## Znane ograniczenia i uwagi ### Pomiar CPU `psutil.Process.cpu_percent()` zwraca procent **względem jednego rdzenia** (np. 50% = pół rdzenia). Na wielordzeniowym procesorze 15% w Task Managerze (uśrednione po rdzeniach) może odpowiadać 50% na jednym rdzeniu. To nie jest błąd — to różna metodologia. Task Manager pokazuje `total_cpu / num_cores`, psutil pokazuje `cpu_time / wall_time`. Jeśli potrzebny jest widok "jak Task Manager": `process.cpu_percent() / psutil.cpu_count()`. ### Pomiar RAM `memory_info().wset` (Windows) = Private Working Set = to co Task Manager pokazuje w kolumnie "Pamięć". RSS zawiera też współdzielone biblioteki (Qt DLLs ~40 MB) i dlatego było zawyżone. Na macOS używane jest `rss` (tam `wset` nie istnieje). ### Wydajność konwersji klatek `QVideoFrame.toImage()` + `convertToFormat(RGB32)` wykonuje się na CPU. Przy 1080p60 to koszt ~1-3 ms/klatkę. Dla MVP akceptowalny. Przy przyszłej integracji YOLO warto rozważyć bezpośredni dostęp do danych przez `QVideoFrame.map()` i przekazywanie raw bufora do GPU. ### Brak testów integracyjnych Testy jednostkowe pokrywają `FrameDispatcher` i `TelemetryCollector` w izolacji (bez Qt event loop). Brak testów `CameraView`, `CameraService` i `MainWindow` — wymagałyby `QApplication` i mockowania urządzeń. --- ## Stan zgodności z PRD (01-mvp-preview.md) | Wymaganie | Status | |---|---| | Realtime camera preview | Zaimplementowane | | Camera switching | Zaimplementowane | | Resolution selection | Zaimplementowane | | FPS selection | Zaimplementowane | | Reconnect/restart | Zaimplementowane | | Realtime FPS metric | Zaimplementowane | | Frame time metric | Zaimplementowane | | Dropped frames detection | Zaimplementowane (heurystyka) | | CPU usage metric | Zaimplementowane | | Memory usage metric | Zaimplementowane | | Overlay system | Zaimplementowane (IOverlayLayer) | | Performance metrics display | Zaimplementowane (TelemetryOverlay) | | Minimal GUI | Zaimplementowane | | Camera menu | Zaimplementowane | | Resolution/FPS menu | Zaimplementowane | | Debug/telemetry options | Zaimplementowane | | Architecture ready for AI subscribers | Zaimplementowane (IOverlayLayer + FrameDispatcher) | | Low latency preview | Zaimplementowane (drop-if-busy w dispatcher) | | Non-blocking GUI thread | Zaimplementowane | --- ## Następne kroki (poza MVP) - Walidacja na Mac Mini z kamerą ELP (AVFoundation backend, macOS Ventura) - Snapshot / recording - Integracja YOLO: `YoloBboxOverlay(IOverlayLayer)` + worker w osobnym procesie - Integracja OCR - Optymalizacja konwersji klatek (QVideoFrame.map() → numpy → GPU) - Testy integracyjne z QApplication + mock camera