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

9.3 KiB
Raw Permalink Blame History

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)
  • closeEventCameraService.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