- Add FrameDispatcher for distributing QVideoFrames to subscribers - Implement TelemetryCollector to measure video pipeline performance metrics - Create MainWindow as the main application interface with video rendering - Develop AppMenuBar for camera selection, resolution, and FPS settings - Establish overlay system for displaying telemetry metrics - Set up project structure and configuration files - Add unit tests for FrameDispatcher and TelemetryCollector
9.4 KiB
9.4 KiB
Plan działania — MVP Camera Preview (PySide6)
Środowisko
| Element | Wartość |
|---|---|
| Python | 3.12.10 (venv: .venv-win) |
| Framework GUI | PySide6 6.11.0 |
| Dev platform | Windows 11 |
| Target platform | Mac Mini (Intel i7, macOS Ventura) |
| Kamera docelowa | ELP USB Camera |
| Narzędzia | pytest, ruff, colorama |
Fazy realizacji
Faza 0 — Projekt i scaffolding
Cel: ustalenie struktury katalogów i modułów przed napisaniem pierwszej linii logiki.
0.1 Struktura projektu
duck-preview2/
├── app/
│ ├── __init__.py
│ ├── main.py # entry point
│ ├── config.py # stałe, domyślne ustawienia
│ ├── camera/
│ │ ├── __init__.py
│ │ ├── camera_service.py # QCamera + QMediaCaptureSession
│ │ └── camera_enumerator.py # wykrywanie dostępnych kamer
│ ├── pipeline/
│ │ ├── __init__.py
│ │ └── frame_dispatcher.py # dystrybucja klatek do subskrybentów
│ ├── telemetry/
│ │ ├── __init__.py
│ │ └── telemetry_collector.py # zbieranie metryk FPS/frame time/CPU
│ ├── overlay/
│ │ ├── __init__.py
│ │ └── overlay_widget.py # przezroczysta warstwa QWidget
│ └── ui/
│ ├── __init__.py
│ ├── main_window.py # główne okno aplikacji
│ └── menu_bar.py # menu: kamera, rozdzielczość, FPS, debug
├── tests/
│ ├── __init__.py
│ ├── test_camera_enumerator.py
│ └── test_telemetry_collector.py
├── notes/
├── requirements.txt
├── requirements-dev.txt
└── pyproject.toml # konfiguracja ruff + pytest
0.2 Pliki konfiguracyjne
pyproject.toml— konfiguracja ruff (linter/formatter) i pytestrequirements.txt— zależności produkcyjne (PySide6)requirements-dev.txt— zależności deweloperskie (pytest, ruff).gitignore— aktualizacja o artefakty Pythona
Faza 1 — Camera Service
Cel: stabilne pobranie obrazu z kamery przez QtMultimedia.
1.1 Camera Enumerator
QMediaDevices.videoInputs()— lista dostępnych kamer- Zwraca listę
QCameraDevicez nazwą, id i obsługiwanymi formatami - Obsługa braku kamer (komunikat, nie crash)
- Test jednostkowy: mockowanie
QMediaDevices
1.2 Camera Service
- Opakowuje
QCamera+QMediaCaptureSession - API:
start(device: QCameraDevice)— uruchamia kameręstop()— zatrzymuje kameręset_resolution(width, height)— ustawia formatset_fps(fps)— ustawia docelowy FPSreconnect()— restart po błędzie
QVideoSinkjako punkt odbioru klatek- Sygnał
frame_ready(QVideoFrame)do Frame Dispatcher - Obsługa błędów kamery (
QCamera.errorOccurred)
1.3 Uwagi platformowe
| Aspekt | Windows 11 (dev) | macOS Ventura (target) |
|---|---|---|
| Backend | DirectShow / Media Foundation | AVFoundation |
| Kamera ELP | USB, standardowy UVC driver | USB, UVC |
| Format klatek | YUYV / MJPEG | YUYV / MJPEG |
| GPU rendering | ANGLE (OpenGL ES) | Metal |
Faza 2 — Frame Dispatcher
Cel: dystrybucja klatek do wielu odbiorców bez blokowania akwizycji.
2.1 Frame Dispatcher
- Wzorzec: publish-subscribe (lista callbacków)
subscribe(callback: Callable[[QVideoFrame], None])unsubscribe(callback)dispatch(frame: QVideoFrame)— wywołuje wszystkich subskrybentów- Klatki NIE są kopiowane — subskrybenci działają na referencji
- Subskrybenci mogą pominąć klatkę (tryb drop-if-busy)
- Wywołanie
dispatchnastępuje w wątku GUI (slot połączony zframe_ready)
2.2 Subskrybenci w Fazie 1
| Subskrybent | Działanie |
|---|---|
| Video Renderer | przekazuje klatkę do QVideoSink / QVideoWidget |
| Telemetry Collector | mierzy czas, zlicza klatki |
Faza 3 — Video Renderer
Cel: renderowanie klatki w GUI bez zbędnych kopii.
3.1 Podejście
QVideoWidgetjako główny widget podgląduQMediaCaptureSession.setVideoOutput(QVideoWidget)— ścieżka bezpośrednia, zero kopii- Alternatywnie:
QVideoSink→QGraphicsVideoItemdla przyszłych overlayów - Domyślnie:
QVideoWidget(prosta, niska latencja)
3.2 Wymagania
- Preview nie blokuje wątku GUI
- Obsługa aspect ratio (letter/pillarbox)
- Resize okna bez migotania
Faza 4 — Telemetry Collector
Cel: dokładne metryki pipeline'u wideo.
4.1 Zbierane metryki
| Metryka | Metoda pomiaru |
|---|---|
| Realtime FPS | licznik klatek / okno 1 s |
| Frame time | time.perf_counter() między klatkami |
| Frame acquisition time | timestamp wejście frame_ready → dispatch |
| Rendering time | czas QVideoWidget.update() (opcjonalnie) |
| Dropped frames | detekcja przez numerację lub timestamp gap |
| CPU usage | psutil.cpu_percent() (dodać do requirements) |
| Memory usage | psutil.virtual_memory() (opcjonalnie) |
4.2 API
TelemetryCollector— subskrybent Frame Dispatcheron_frame(frame: QVideoFrame)— rejestruje timestamp klatkiget_snapshot() -> TelemetrySnapshot— aktualny stan metryk (dataclass)update_interval_ms: int— jak często odświeżać snapshot (domyślnie 500 ms)- Sygnał
metrics_updated(TelemetrySnapshot)— emitowany coupdate_interval_ms
4.3 TelemetrySnapshot (dataclass)
@dataclass
class TelemetrySnapshot:
fps: float
frame_time_ms: float
dropped_frames: int
cpu_percent: float
memory_mb: float | None
timestamp: float
Faza 5 — Overlay System
Cel: wyświetlanie metryk na przezroczystej warstwie nad podglądem.
5.1 Architektura
OverlayWidget(QWidget)— przezroczysty widget (WA_TransparentForMouseEvents)- Pozycjonowany absolutnie nad
QVideoWidget(ten sam parent, wyższy z-index) paintEventrysuje semi-przezroczysty prostokąt + tekst z metrykami- Połączony z sygnałem
metrics_updated— odświeża tylko gdy dane się zmienią
5.2 Zawartość overlaya (MVP)
FPS: 60.0
Frame: 16.7 ms
Drop: 0
CPU: 12.3 %
5.3 Sterowalność
- Widoczność overlaya: toggle przez menu Debug
- Pozycja: lewy górny róg (stała w MVP)
- Kolor tła:
rgba(0, 0, 0, 160)
Faza 6 — GUI / Main Window
Cel: minimalne, funkcjonalne okno aplikacji.
6.1 MainWindow
QMainWindowzQVideoWidgetjako central widgetOverlayWidgetnałożony na video- Obsługa resize → reposition overlay
- Tytuł okna:
Duck Preview
6.2 MenuBar
Menu Camera:
- Lista wykrytych kamer (radio-style)
- Separator
- Reconnect
Menu Video:
- Resolution submenu (pobierane dynamicznie z
QCameraDevice.videoFormats()) - FPS submenu
Menu Debug:
- Toggle overlay metryk
- Logowanie do konsoli (toggle)
6.3 Startup flow
main.py
→ QApplication
→ CameraEnumerator.list_cameras()
→ MainWindow(cameras)
→ CameraService.start(cameras[0]) # pierwsza kamera lub ELP
→ FrameDispatcher.subscribe(telemetry, renderer)
→ app.exec()
Faza 7 — Testy i walidacja
7.1 Testy jednostkowe
| Moduł | Co testować |
|---|---|
CameraEnumerator |
lista kamer, brak kamer, format danych |
TelemetryCollector |
obliczenia FPS, wykrywanie dropów |
FrameDispatcher |
subskrypcja, odsubskrypcja, dispatch |
TelemetrySnapshot |
poprawność dataclass |
7.2 Testy manualne (Windows dev)
- Uruchomienie z kamerą laptopa / USB webcam
- Przełączanie kamer
- Zmiana rozdzielczości
- Zmiana FPS
- Toggle overlay
- Reconnect po odłączeniu kamery
7.3 Testy na Mac Mini (target)
- Wykrycie kamery ELP
- Poprawny format YUYV/MJPEG
- Wydajność AVFoundation vs DirectShow
- GPU rendering przez Metal
7.4 Kryteria sukcesu (z PRD)
- Preview stabilny i płynny
- Latencja renderowania niska
- Dane telemetrii dokładne
- GUI responsywne
- Overlay działa poprawnie
- Architektura gotowa na subskrybentów AI
Kolejność implementacji (sprint order)
Sprint 1: Faza 0 — scaffolding, pyproject.toml, requirements
Sprint 2: Faza 1 — CameraEnumerator + CameraService (bez GUI)
Sprint 3: Faza 3 — VideoRenderer + MainWindow (preview działa)
Sprint 4: Faza 2 — FrameDispatcher (refactor pipeline)
Sprint 5: Faza 4 — TelemetryCollector
Sprint 6: Faza 5 — OverlayWidget
Sprint 7: Faza 6 — MenuBar (camera/resolution/fps switch)
Sprint 8: Faza 7 — Testy, poprawki, walidacja na Mac Mini
Zależności do dodania
# requirements.txt
PySide6>=6.7
psutil>=6.0
# requirements-dev.txt
pytest>=8.0
ruff>=0.4
Uwagi cross-platform
- ELP camera — kamera UVC, powinna działać bez dodatkowych sterowników na obu platformach. Sprawdzić obsługiwane rozdzielczości i FPS przez
QCameraDevice.videoFormats(). - Ścieżki absolutne — unikać
os.pathna korzyśćpathlib.Path. - Threading — wszystkie operacje Qt muszą odbywać się w wątku GUI.
TelemetryCollectormoże używaćQTimerzamiast osobnego wątku. - Format klatek — na macOS AVFoundation preferuje
BGRAlubNV12. Konwersja powinna być leniwa i tylko gdy potrzebna (nie w hot path renderowania). - High DPI — włączyć
QApplication.setHighDpiScaleFactorRoundingPolicydla konsistencji Windows/Mac. - Testowanie bez kamery —
CameraEnumeratorpowinien umożliwiać dependency injection / mock dla środowisk CI.