Hlasové a Video hovory — specifikace¶
Nahrazuje
voice-chat.md. Scope: 1:1 hovory v DM konverzacích. Skupinové hovory jsou mimo scope (vyžadují SFU server).
1. Přehled¶
Peer-to-peer audio/video komunikace integrovaná do ChatWindow. Hovor se iniciuje
tlačítky 🎙 (hlas) a 📷 (video) ve vstupní liště ConversationPanel.
Šifrování zajišťuje WebRTC (DTLS-SRTP) — žádné audio/video neprochází serverem,
pouze signalizační zprávy (SDP, ICE).
Volající Backend (FastAPI WS) Přijímající
│── /ws/voice/{conv_id} ───────►│ │
│ offer SDP + capabilities │── incoming_call ─────────►│
│ │ {caller, sdp, has_video}│
│◄── answer SDP ────────────────│◄── answer ───────────────│
│── ICE candidates ─────────────►│◄── ICE candidates ───────│
│◄──────────────────────────────│──────────────────────────►│
│ │
│ ◄══════ přímý RTP proud (P2P nebo přes TURN) ══════════► │
│ audio (Opus) + video (H.264/VP8) │
2. Hardwarové požadavky¶
| Komponenta | Minimum | Doporučeno |
|---|---|---|
| Mikrofon | jakýkoliv USB/jack | noise-cancelling headset |
| Reproduktory / sluchátka | jakékoliv | sluchátka (echo cancel) |
| Kamera | 720p 30fps | 1080p 30fps |
| CPU | 2 jádra (audio) | 4 jádra (video H.264 HW encode) |
| RAM | 200 MB volné | — |
| Síť | 100 kbps (audio) | 2 Mbps (720p video) |
3. Správa audio/video zařízení¶
3a. Enumarace zařízení¶
# Audio — sounddevice
import sounddevice as sd
devices = sd.query_devices()
inputs = [d for d in devices if d["max_input_channels"] > 0]
outputs = [d for d in devices if d["max_output_channels"] > 0]
# Vždy první možnost v každém dropdown:
"Windows výchozí zařízení" # → index = None v sounddevice
# Video — OpenCV
import cv2
cameras = []
for i in range(8): # prohledá indexy 0–7
cap = cv2.VideoCapture(i, cv2.CAP_DSHOW)
if cap.isOpened():
cameras.append({"index": i, "name": _get_camera_name(i)})
cap.release()
3b. Detekce hot-plug (připojení/odpojení za běhu)¶
Background vlákno každé 3 s porovná aktuální seznam zařízení s předchozím.
Změna → event on_devices_changed(added, removed):
- Pokud odpojeno zařízení aktivní v hovoru → varování v ActiveCallBar
„Zařízení bylo odpojeno — přepínám na výchozí"
- Pokud odpojeno uložené zařízení (mimo hovor) → badge ⚠ v záložce Zvuk nastavení
3c. Nedostupné uložené zařízení¶
Při každém startu hovoru se ověří, zda uložené zařízení existuje v aktuálním seznamu.
Situace: uloženo "Headset Pro" — není v seznamu zařízení
┌──────────────────────────────────────────────────────┐
│ ⚠ Zařízení není dostupné │
│ │
│ Uložené zařízení "Headset Pro" není připojeno. │
│ Vyberte náhradní zařízení: │
│ │
│ Mikrofon: [▼ Windows výchozí zařízení ] │
│ Reproduktory:[▼ Reproduktory (Realtek Audio) ] │
│ │
│ ☐ Zapamatovat tuto volbu │
│ │
│ [ Zrušit ] [ Pokračovat v hovoru ] │
└──────────────────────────────────────────────────────┘
4. Nastavení zařízení — záložka Zvuk¶
Rozšíření stávající sekce ZVUK v SettingsScreen.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ZVUK — HOVORY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Mikrofon
[▼ Headset Pro (USB Audio) ] [ 🎙 Test ]
Citlivost: ━━━━●━━━━━━━━━ 50 %
[ ████░░░░░░░ ] ← živý VU meter při testu
Reproduktory / sluchátka
[▼ Windows výchozí zařízení ] [ 🔊 Test ]
Hlasitost: ━━━━━━━●━━━━━ 70 %
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
VIDEO
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Kamera
[▼ Logitech C920 HD Pro Webcam ] [ 📷 Náhled ]
Rozlišení: [▼ 720p 30fps ]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
NOTIFIKACE PŘÍCHOZÍCH HOVORŮ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Vyzváněcí tón
[▼ Klasický (built-in) ] [ ▶ Přehrát ]
• Klasický
• Jemný
• Digitální
• Vlastní soubor... ← otevře file dialog (.mp3 .wav .ogg)
Hlasitost vyzvánění: ━━━━●━━━━━ 40 %
☑ Záblesk ikony v trayu
☑ Windows toast notifikace
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Uložené klíče v settings.py:
settings["audio_input"] # název zařízení nebo None (výchozí)
settings["audio_output"] # název zařízení nebo None
settings["audio_input_vol"] # 0–100
settings["audio_output_vol"] # 0–100
settings["video_device"] # index kamery nebo None
settings["video_resolution"] # "720p" | "1080p" | "480p"
settings["ringtone"] # "classic" | "soft" | "digital" | "/path/to/file"
settings["ringtone_vol"] # 0–100
settings["notif_tray_flash"] # bool
settings["notif_toast"] # bool
Test mikrofonu — 3s nahrávka → živý VU meter (CTkProgressBar), pak přehrání. Náhled kamery — malé CTkToplevel okno 320×240, live stream z OpenCV, zavírací tlačítko.
5. Architektura¶
5a. Technologie¶
| Vrstva | Knihovna | Verze |
|---|---|---|
| WebRTC (SDP, ICE, DTLS-SRTP, RTP) | aiortc |
≥ 1.9 |
| Audio capture/playback | sounddevice |
≥ 0.4.6 |
| Video capture | opencv-python |
≥ 4.9 |
| Video zobrazení v UI | Pillow (CTkImage) |
již závislost |
| Toast notifikace | winotify |
≥ 0.1.6 |
| Zvuk vyzvánění | pygame.mixer |
≥ 2.5 |
| Signalizace WS | websockets |
≥ 12.0 |
Proč
aiortc? Řeší kompletní WebRTC stack v Pythonu — SDP offer/answer, ICE gathering, DTLS handshake, SRTP šifrování, Opus a H.264/VP8 kodeky. Alternativa (ruční sounddevice + opus přes WS) by vyžadovala implementaci vlastního ICE a šifrování.
5b. Nové soubory launcheru¶
desktop/launcher2/
├── voice_call.py # VoiceVideoCall engine (aiortc + sounddevice + cv2)
├── voice_ui.py # IncomingCallDialog, ActiveCallBar, VideoWindow
├── voice_notify.py # Windows toast + tray flash + ringtone přehrávání
└── voice_devices.py # DeviceManager — enumarace, hot-plug, persistence
5c. VoiceVideoCall (voice_call.py)¶
class VoiceVideoCall:
def __init__(self, session, conv_id, with_video=False):
self.conv_id = conv_id
self.with_video = with_video
self._pc: RTCPeerConnection = None # aiortc
self._ws = None # signaling WebSocket
self._audio_track = None # SounddeviceTrack
self._video_track = None # OpenCVTrack (pokud with_video)
async def start_call(self) -> str: # → offer SDP
async def answer_call(self, sdp: str): # přijmout příchozí hovor
async def add_ice(self, candidate: dict):
def mute_mic(self, muted: bool):
def mute_speaker(self, muted: bool):
def toggle_video(self, on: bool):
async def hangup(self):
# Callbacky (volá UI)
on_remote_audio: Callable # přijatý audio frame
on_remote_video: Callable[[ndarray], None] # přijatý video frame (cv2)
on_state_change: Callable[[str], None] # "connecting"|"connected"|"ended"
on_error: Callable[[str], None]
5d. Backend — signalizace¶
Nové soubory:
- backend/app/routers/voice.py — WebSocket /ws/voice/{conv_id}
- Přidáno do main.py
WebSocket zprávy:
// Volající → backend → Přijímající
{"type": "offer", "sdp": "...", "has_video": true, "caller_name": "Jan Novák"}
{"type": "answer", "sdp": "..."}
{"type": "ice", "candidate": {"candidate": "...", "sdpMid": "0", "sdpMLineIndex": 0}}
{"type": "hangup"}
// Backend → Přijímající (push)
{"type": "incoming_call", "conv_id": "...", "caller_name": "...", "has_video": true}
// Backend → Volající (push)
{"type": "call_rejected"}
{"type": "call_busy"} // přijímající již v jiném hovoru
REST endpointy:
POST /voice/call → zahájit hovor (pošle incoming_call push)
POST /voice/hangup → ukončit
GET /voice/status/{conv_id} → { "active": bool, "participants": [...] }
5e. ICE / STUN / TURN¶
ice_servers = [
RTCIceServer(urls=["stun:stun.l.google.com:19302"]),
RTCIceServer(
urls=["turn:vm-gateway.avaxis.cz:3478"],
username="avax",
credential="<TURN_SECRET>", # z env proměnné
),
]
pc = RTCPeerConnection(RTCConfiguration(iceServers=ice_servers))
TURN server: coturn na vm-gateway. Nutný pokud jsou oba klienti za NAT/firewall.
Konfigurace coturn: infrastructure/coturn.conf.
6. Příchozí hovor — notifikace¶
Příchozí hovor detekuje chat_poll.py (polling /chat/pending) nebo WS listener.
6a. Sekvence notifikací¶
1. Přijde {"type": "incoming_call", "caller_name": "Jan", "has_video": true}
2. Paralelně:
a. 🔔 Zvuk vyzvánění (pygame.mixer, opakovaně dokud nepřijme/neodmítne)
b. 📳 Záblesk ikony v trayu (střídá normální/zvýrazněnou ikonu á 500ms)
c. 🪟 Windows Toast notifikace (winotify):
"Jan Novák volá (video)"
[Přijmout] [Odmítnout]
d. 🪟 IncomingCallDialog v launcheru (pokud je okno otevřeno)
3. Timeout 30s → automatické odmítnutí ("missed call")
6b. IncomingCallDialog¶
┌─────────────────────────────────────┐
│ │
│ 📞 Příchozí hovor │
│ │
│ ┌───┐ │
│ │ JN│ Jan Novák │
│ └───┘ Volá vás (video + audio) │
│ │
│ ⏱ 00:12 │
│ │
│ [ ❌ Odmítnout ] [ ✅ Přijmout ] │
└─────────────────────────────────────┘
- Zobrazí se vždy nad ostatními okny (
attributes("-topmost", True)) - Pokud launcher v trayu — okno se vynoří
- Časovač odpočítává 30s
- Přijmout: otevře
ActiveCallBar+ případněVideoWindow
6c. Toast notifikace (winotify)¶
from winotify import Notification, audio
notif = Notification(
app_id="AVAX Launcher",
title="Příchozí hovor",
msg=f"{caller_name} vás volá",
icon=str(ICON_PATH),
duration="long",
)
notif.set_audio(audio.Default, loop=False) # vlastní zvuk přes pygame
notif.add_actions(label="Přijmout", launch="avax://call/accept")
notif.add_actions(label="Odmítnout", launch="avax://call/reject")
notif.show()
Klik na akci v toastu pošle URI do launcheru (registrovaný URL handler avax://).
7. UI komponenty (voice_ui.py)¶
7a. ActiveCallBar¶
Zobrazí se nad vstupní lištou ConversationPanel po dobu hovoru:
┌────────────────────────────────────────────────────────────────┐
│ 🔴 HOVOR ⏱ 00:03:42 Jan Novák │
│ [ 🎙 Mikrofon ] [ 🔊 Zvuk ] [ 📷 Kamera ] [ 📞 Zavěsit ] │
└────────────────────────────────────────────────────────────────┘
- Mikrofon: toggle mute/unmute (červená = muted)
- Zvuk: toggle mute reproduktoru
- Kamera: toggle video on/off (zobrazí/skryje
VideoWindow) - Zavěsit: ukončí hovor, odstraní bar
7b. VideoWindow¶
Samostatné CTkToplevel okno, zobrazí se vedle ChatWindow:
┌──────────────────────────────────────────────────┐
│ Hovor — Jan Novák [━][□][✕] │
│ ┌──────────────────────────────────────────────┐ │
│ │ │ │
│ │ remote video (hlavní) │ │
│ │ ┌──────────────┐ │ │
│ │ │ local video │ │ │
│ │ │ (malý roh) │ │ │
│ │ └──────────────┘ │ │
│ └──────────────────────────────────────────────┘ │
│ [ 🎙 ] [ 🔊 ] [ 📷 ] [ ⛶ Celá obrazovka ] [ 📞 ] │
└──────────────────────────────────────────────────┘
Video frames z aiortc → OpenCV ndarray → PIL Image → CTkImage → CTkLabel.
Refresh: after(33, ...) = ~30 fps.
Celá obrazovka: VideoWindow.state("zoomed") + skrytí dekorace.
Lokální náhled (pip — picture-in-picture): - 160×120 px, vpravo dole v remote videu - Kliknutím přepne strany (remote ↔ local velký)
8. Šifrování a bezpečnost¶
| Vrstva | Mechanismus |
|---|---|
| Signalizace | HTTPS/WSS (TLS na ws.avaxis.cz) |
| Audio/video | DTLS-SRTP (zajišťuje aiortc automaticky) |
| TURN přihlašovací údaje | HMAC-SHA1 time-limited credentials |
| Nahrávání | zcela mimo scope (GDPR) |
9. Zpracování chybových stavů¶
| Situace | Chování |
|---|---|
| Zařízení není k dispozici při startu | Dialog výběru náhradního zařízení (viz sekce 3c) |
| Zařízení odpojeno během hovoru | Toast „Zařízení odpojeno", přepnutí na výchozí bez přerušení hovoru |
| ICE connection failed (P2P blokován) | Automatický fallback na TURN server |
| TURN selže | Hovor ukončen, chybová hláška „Nelze navázat spojení" |
| Přijímající je offline | 30s timeout → „Volání nepřijato" |
| Přijímající je v jiném hovoru | Okamžitá zpráva „Uživatel je momentálně v hovoru" (call_busy) |
| Ztráta spojení WS | Pokus o reconnect 3× á 2s, pak ukončení hovoru |
| Kamera nedostupná | Hovor pokračuje pouze s audiem, upozornění v ActiveCallBar |
10. Závislosti (requirements.txt)¶
# Stávající
sounddevice>=0.4.6
# Nové
aiortc>=1.9.0 # WebRTC: SDP, ICE, DTLS-SRTP, Opus, H.264/VP8
opencv-python>=4.9.0 # video capture (headless build — bez GUI)
winotify>=0.1.6 # Windows toast notifikace
pygame>=2.5.0 # přehrávání zvuku vyzvánění
websockets>=12.0 # WS signalizace pro voice
opencv-pythonvsopencv-python-headless: Použítopencv-python(má GUI okna cv2.imshow, ale my je nepoužijeme — frames předáme do CTkImage). Headless build je o ~10 MB menší, ale nemusí mít správné video backend drivery na Windows.
Windows: libopus.dll
aiortc na Windows potřebuje opus.dll:
1. Stáhnout z https://github.com/xiph/opus/releases (precompiled)
2. Umístit do dist/ vedle exe
3. PyInstaller spec: datas=[("opus.dll", ".")]
11. Backend — nové soubory a migrace¶
backend/app/routers/voice.py # WS /ws/voice/{conv_id} + REST /voice/*
backend/app/services/voice.py # session management (kdo s kým hovoří)
DB tabulka (volitelná — pro statistiky a missed calls):
CREATE TABLE voice_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conv_id UUID REFERENCES conversations(id),
caller_id UUID REFERENCES users(id),
callee_id UUID REFERENCES users(id),
has_video BOOLEAN DEFAULT false,
started_at TIMESTAMPTZ,
answered_at TIMESTAMPTZ,
ended_at TIMESTAMPTZ,
end_reason VARCHAR(30) -- 'hangup' | 'rejected' | 'missed' | 'error'
);
12. Integrace do ChatWindow¶
# ConversationPanel._build() — tlačítka v input baru:
self._voice_btn = CTkButton(bar, text="🎙", command=self._start_voice)
self._video_btn = CTkButton(bar, text="📷", command=self._start_video)
# (oba zatím disabled, zapnout až po implementaci)
# ConversationPanel:
def _start_voice(self):
from voice_call import VoiceVideoCall
call = VoiceVideoCall(self.session, self.conv["id"], with_video=False)
asyncio.run_coroutine_threadsafe(call.start_call(), self._loop)
self._show_active_bar(call)
def _start_video(self):
from voice_call import VoiceVideoCall
call = VoiceVideoCall(self.session, self.conv["id"], with_video=True)
...
asyncio event loop pro aiortc běží v samostatném daemonickém vlákně
(aiortc je async, tkinter je synchronní — nutná most).
13. Odhad implementace¶
| Část | Odhad |
|---|---|
voice_devices.py — DeviceManager, hot-plug, settings |
2 dny |
voice_notify.py — toast, tray, ringtone, URL handler |
1 den |
voice_call.py — VoiceVideoCall (aiortc engine) |
3 dny |
voice_ui.py — IncomingCallDialog, ActiveCallBar, VideoWindow |
2 dny |
| Backend signalizace (WS + REST) | 2 dny |
| TURN server konfigurace (coturn) | 0.5 dne |
| Nastavení zařízení (Settings screen rozšíření) | 1 den |
| Integrace do ChatWindow + testování | 2 dny |
| Celkem | ~13.5 dne |
14. Phase P2 — dokončení & hardening¶
Grounded gap-analýza 2026-05-16. P2 NENÍ greenfield —
voice_devices.py,voice_notify.py,voice_ui.py+ Settings audio/video/notif sekce jsou z velké části napsané (P0 stabilizace + P1 ICE/TURN shipped). P2 = zapojit existující nedrátované kusy + zahärdrovat in-call chyby + audit latentních P0-class bugů. Odhad ~3 dny dev (mimo user/infra-blokované).
P2.1 — In-call robustness (spec sek. 9) ✅ HOTOVO 2026-05-16 (60160d2)¶
Vše ve voice_call.py (dnes chybí — grep potvrdil 0 výskytů reconnect/
device-disconnect/camera-unavail handling):
- WS reconnect 3× á 2 s než hovor skončí. Dnes:
_ws_recv_loopvýjimka → rovnou_async_hangup. Cíl: 3 pokusy reconnect na/ws/voice/{conv}, re-send poslední SDP stav, pak teprve hangup. - Kamera nedostupná při startu → pokračuj audio-only + notice v
ActiveCallBar. Dnes:resolve_camera() < 0→ tiše bez video tracku. - Zařízení odpojeno během hovoru → switch na default bez přerušení (viz P2.2 hot-plug wiring).
- ICE failed → ověřit že TURN fallback se reálně uplatní (P1 ice-servers + coturn hotové; chybí E2E za-NAT test = user, 2 stroje).
- Akceptace: vytáhnout síť na 5 s během hovoru → po obnově hovor pokračuje (ne drop). Start bez kamery → audio hovor + hláška.
P2.2 — Device resilience wiring (sek. 3b/3c) ✅ HOTOVO 2026-05-16 (60160d2+)¶
Substitution dialog (3c): nový
voice_ui.SubstituteDeviceDialog(modální, blokující). chat_window_start_call_internal/_accept_incomingpo capability gate:!is_saved_*_available→ dialog (mic/kamera dropdown + Zapamatovat), Zrušit → abort/reject. Hot-plug (3b):voice_call._async_start→dm.start()+dm.add_listener(_on_devices_changed); aktivní zařízení odpojeno →on_notice+ hovor POKRAČUJE (NEteardown PC);_async_hangupodregistruje. Ověřeno izolovaně (notice 1×, neaktivní 0, ended-guard). DEFER: seamless switch na default mid-call (přepnutí audio capture bez výpadku) —SounddeviceAudioTracknemá live reopen API → vyžaduje audio-track refaktor (P3). Dnes: notice „odpoj → vyber v Nastavení / restartuj hovor", hovor se NEpřeruší.SubstituteDeviceDialog„Zapamatovat" persistuje vždy (ephemeral-only override = P3). Původní gap-analýza níže:
- Substitution dialog (3c):
DeviceManager.is_saved_audio_input_ available()/is_saved_camera_available()existují ale nikde se nevolají. Zapojit dochat_window._start_call_internal/_accept_incomingPŘED startem: pokud uložené zařízení chybí → nový malý dialogvoice_ui.SubstituteDeviceDialog(dropdown náhrady + „Zapamatovat") → pak teprv call. - Hot-plug během hovoru (3b):
DeviceManagermáadd_listener+_scan_loop, aleVoiceVideoCallse nepřihlašuje. Zapojit:dm.add_listener(self._on_devices_changed)v_async_start; při odpojení aktivního zařízení →ActiveCallBarvarování + přepnout track na default (sounddevicereopen) bez teardownu PC. - Akceptace: odpoj USB headset během hovoru → hláška + hovor jede dál přes default. Uložený mic chybí při startu → substitution dialog.
P2.3 — Settings polish & verify (sek. 4) ✅ HOTOVO 2026-05-16¶
Audit Settings „Zvuk — Hovory / Video / Notifikace" (screens.py): všechny 4 položky existují a jsou zadrátované —
_test_microphone(VU meter RMS),_preview_camera→_CameraPreviewDialog, ringtone „Vlastní soubor…" filedialog (.mp3/.wav/.ogg), notif toggly (notif_tray_flash / notif_toast checkboxy → settings.set).Nalezeno+opraveno:
_test_microphoneměl P0-class bug —sd.sleep(3000)+sd.playběžely na Tk threadu (button command) → UI zamrzlé 3 s, VU meter se nevykresloval live (after callbacky čekaly za blokem). → přesunuto do worker threadu (VU/reset přes self.after,_mic_vu_activeguard). Teď VU meter reálně animuje + UI neztuhne.Ověřeno čisté:
_CameraPreviewDialogmá korektní cv2 lifecycle (_runningflag,WM_DELETE_WINDOW→_close→cap.release()+destroy,after(33,…)~30fps na main threadu) — žádný handle leak._test_ speakerssd.play non-blocking OK.Vizuální chování (VU meter animace, camera náhled render) = uživatelův launcher test (avaxdev headless bez displeje/AV). Logika + threading + cv2 lifecycle ověřeny code-review + py_compile. Původní gap-analýza níže:
Settings „Zvuk — Hovory / Video / Notifikace" existuje (mic/spk/vol/
VU/kamera dropdown/ringtone+play/vol). Ověřit/dodělat:
- Camera live „Náhled" (320×240 CTkToplevel z OpenCV) — ověřit že
funguje + zavírací cleanup (uvolnit cv2.VideoCapture).
- VU meter reálně animuje při test mikrofonu (3 s capture).
- Ringtone „Vlastní soubor…" → file dialog (.mp3/.wav/.ogg).
- Notif toggly (tray flash / toast) checkboxy přítomné + uložené.
P2.4 — Audit latentních P0-class bugů ✅ HOTOVO 2026-05-16¶
Nalezeno+opraveno:
voice_notify.py—import numpybyl NEguardovaný (P0-class nekonzistence, voice_notify se při P0 vynechal) → guard + syntetický tón no-op kdyžnp is None. Pre-existingSyntaxWarning: invalid escape '\S'(docstring HKCU\Software) → raw docstring. Ověřenopython -W error::SyntaxWarningclean.Ověřeno čisté:
voice_devices.py(sd/cv2 guarded, scan thread daemon+stop(), cv2 cap.release(), bez numpy, bez Tk, bez blokujícího .result/sleep na Tk threadu).voice_ui.py(čistě ctk, set_remote/ local_frame marshallují přes after, timery přes after).voice_notifyTk-safety: nesahá na Tk;_on_timeoutvolá callback z Timer threadu, ale caller (chat_window) marshalluje přesself.after— dokumentovaný kontrakt, OK.VYŘEŠENO 2026-05-16 (commit 360ed20) — orig. DEFER → P3:
list_cameras()se v_scan_loopvolala každý HOT_PLUG_INTERVAL a (po camera-open robustness fixu) kameru reálně otevírala+read() → LED bliká i mimo hovor + kolize s aktivním video hovorem. Fix: kamery úplně mimo background scan —_scan_oncejen audio (sounddevice query nic neotevírá).list_cameras()jen on-demand (Nastavení→Video, start video hovoru). Tradeoff: žádný background camera hot-plug (audio unplug = kritický, kryt; camera-at-start řeší P2.1). Původní gap-analýza níže:
voice_devices/notify/ui vznikly PŘED P0 fixy → projít stejnou optikou:
- guard heavy importů (sounddevice/cv2/pygame/winotify) —
voice_devices má guard; ověřit voice_notify (pygame/winotify) a
že Settings sekce degraduje když chybí (ne traceback).
- žádné Tk volání z non-main threadu (_scan_loop, ringtone thread,
notifier callbacky) — vše přes root.after.
- žádné blokující .result() / time.sleep na Tk threadu.
- dead callbacky / lifecycle leaky (ringtone thread join, scan thread
stop, cv2 capture release).
P2.5 — Production TURN (sek. 5e/8) ⏳ BLOKOVÁNO user/infra¶
Dev coturn na avaxdev:3478 hotové (P1). Produkce vyžaduje (mimo dev
režii — stejný blocker jako Phase 4 docs, vm-gateway není provisioned):
coturn na vm-gateway + external-ip=<veřejná> + TLS 5349 +
anti-SSRF hardening (dnes jen loopback/multicast deny) + DNS. Až bude
vm-gateway: TURN_HOST env → vm-gateway, cert, firewall UDP 3478 +
relay range.
P2 priorita¶
P2.1 (robustness) → P2.2 (device wiring) → P2.4 (audit) → P2.3 (polish).
P2.5 čeká na vm-gateway (user/infra). E2E reálný hovor mezi 2 stroji =
user (avaxdev headless, bez AV).