feat: implement core functionality for camera preview application
- 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
This commit is contained in:
324
notes/01-mvp-plan.md
Normal file
324
notes/01-mvp-plan.md
Normal file
@@ -0,0 +1,324 @@
|
||||
# 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 pytest
|
||||
- `requirements.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ę `QCameraDevice` z 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 format
|
||||
- `set_fps(fps)` — ustawia docelowy FPS
|
||||
- `reconnect()` — restart po błędzie
|
||||
- `QVideoSink` jako 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 `dispatch` następuje w wątku GUI (slot połączony z `frame_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
|
||||
|
||||
- `QVideoWidget` jako główny widget podglądu
|
||||
- `QMediaCaptureSession.setVideoOutput(QVideoWidget)` — ścieżka bezpośrednia, zero kopii
|
||||
- Alternatywnie: `QVideoSink` → `QGraphicsVideoItem` dla 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 Dispatcher
|
||||
- `on_frame(frame: QVideoFrame)` — rejestruje timestamp klatki
|
||||
- `get_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 co `update_interval_ms`
|
||||
|
||||
#### 4.3 TelemetrySnapshot (dataclass)
|
||||
|
||||
```python
|
||||
@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)
|
||||
- `paintEvent` rysuje 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
|
||||
|
||||
- `QMainWindow` z `QVideoWidget` jako central widget
|
||||
- `OverlayWidget` nał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
|
||||
|
||||
1. **ELP camera** — kamera UVC, powinna działać bez dodatkowych sterowników na obu platformach. Sprawdzić obsługiwane rozdzielczości i FPS przez `QCameraDevice.videoFormats()`.
|
||||
2. **Ścieżki absolutne** — unikać `os.path` na korzyść `pathlib.Path`.
|
||||
3. **Threading** — wszystkie operacje Qt muszą odbywać się w wątku GUI. `TelemetryCollector` może używać `QTimer` zamiast osobnego wątku.
|
||||
4. **Format klatek** — na macOS AVFoundation preferuje `BGRA` lub `NV12`. Konwersja powinna być leniwa i tylko gdy potrzebna (nie w hot path renderowania).
|
||||
5. **High DPI** — włączyć `QApplication.setHighDpiScaleFactorRoundingPolicy` dla konsistencji Windows/Mac.
|
||||
6. **Testowanie bez kamery** — `CameraEnumerator` powinien umożliwiać dependency injection / mock dla środowisk CI.
|
||||
Reference in New Issue
Block a user