195 lines
9.3 KiB
Markdown
195 lines
9.3 KiB
Markdown
# 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
|