9.3 KiB
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ę
CameraInfoz 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) lubrss(macOS/Linux) - Emituje
metrics_updated(TelemetrySnapshot)co 500 ms przezQTimer
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(nieQVideoWidget) — render przezQPainterwpaintEvent - Odbiera
QVideoFrameprzez sloton_frame(), konwertuje doQImage.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 dostajepainter.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:
OverlayWidgetjako childQVideoWidget— zasłonięty przez natywną powierzchnię- Kontener
QWidgetzQVideoWidgetiOverlayWidgetjako rodzeństwo — nadal zasłonięty 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