Files
duck-preview/notes/02-mvp-app.md

195 lines
9.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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