diff --git a/notes/02-mvp-app.md b/notes/02-mvp-app.md new file mode 100644 index 0000000..3ec29b3 --- /dev/null +++ b/notes/02-mvp-app.md @@ -0,0 +1,194 @@ +# 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