feat: add MVP application documentation with project structure and component descriptions
This commit is contained in:
194
notes/02-mvp-app.md
Normal file
194
notes/02-mvp-app.md
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user