# 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.
