# FASE 4 — Videochamada ## Objetivo LiveKit integrado ponta a ponta: emissão de tokens, sala por visita, vídeo no navegador do visitante, e degradação graciosa quando o SFU falha. ## Pré-requisitos FASES 0–3 concluídas. Ler `docs/05-INFRA-DOCKER.md` §3 e §4, e `docs/01-ARQUITETURA.md` §5.5. > **Faça a POC antes de escrever código de produção.** Suba `livekit-server` + `coturn` e feche uma chamada entre um navegador móvel em 4G real e um Android, medindo o tempo até o primeiro frame. É o maior risco técnico do projeto e o mais barato de derrubar cedo. Se a POC falhar, o problema quase sempre é a faixa UDP fechada no firewall. ## Tarefas ### 1. Porta de vídeo ```kotlin interface VideoCallProvider { suspend fun criarSala(visitId: UUID): RoomInfo suspend fun emitirToken(visitId: UUID, participante: Participant): String suspend fun encerrarSala(visitId: UUID) suspend fun saudavel(): Boolean } ``` Adaptador `LiveKitProvider`. A porta existe para permitir trocar por serviço gerenciado sem tocar nos apps — decisão registrada em `01-ARQUITETURA.md` ADR-003. ### 2. Emissão de tokens Token JWT do LiveKit gerado **pelo backend**, nunca pelo cliente. Escopo mínimo: - `roomJoin` apenas na sala daquela visita - `canPublish` para vídeo e áudio - `canPublishData: false` - TTL de 10 minutos, alinhado à expiração da visita `room_name` derivado determinísticamente de `visitId`, gravado em `visits.room_name`. **Um token nunca dá acesso a outra sala.** Verifique isso com teste explícito. ### 3. Ciclo de vida da sala Sala criada na transição para `EM_CHAMADA`, não antes — criar em `PENDENTE` desperdiça recurso do SFU em visitas que nunca são atendidas. Encerrada ao chegar em estado final. `empty_timeout: 120` no LiveKit é a rede de segurança para sala órfã. ### 4. Vídeo na web do visitante SDK `livekit-client` carregado por **`import()` dinâmico**, apenas quando a visita entra em `EM_CHAMADA`. Ele sozinho pesa mais que todo o resto do app — carregá-lo no bundle inicial destruiria o orçamento de 200KB da FASE 3. Vídeo remoto em tela cheia, auto-preview pequeno no canto, botão de mudo e de encerrar. Indicador de qualidade de conexão. ### 5. Degradação graciosa Circuit breaker (Resilience4j) sobre o `VideoCallProvider`. Ao abrir: ``` LiveKit fora ─► AUDIO (mesma sala, sem vídeo, muito menos banda) └─► FOTO_TEXTO (visitante manda foto, morador aprova sem chamada) └─► FILA_OPERADOR (resolução por telefone) ``` O nível vigente vai em `visits.degraded_mode`, é emitido como `DEGRADED_MODE` no WebSocket, aparece em `/actuator/health` e no painel admin. **Comunique a degradação, nunca a esconda.** Esconder faz o usuário achar que o produto quebrou; nomear faz o produto parecer resiliente — que é o que ele é. ### 6. Configuração de mídia **Limite de resolução imposto no servidor**, não no cliente: 480p a ~600kbps. 480p basta para reconhecer alguém na portaria; 1080p multiplicaria o custo de banda por quatro sem melhorar nenhuma decisão. Ver o dimensionamento em `05-INFRA-DOCKER.md` §4. Simulcast desligado (só dois participantes). `adaptiveStream` e `dynacast` ligados. TURN configurado com o segredo do coturn. ### 7. Métricas `portaria.call.time_to_first_frame` · `portaria.call.duration` · `portaria.call.failed_ratio` · `portaria.call.turn_usage_ratio` (uso alto de TURN indica problema de rede na portaria) · `portaria.call.degraded_ratio`. ## Critérios de aceite - [ ] POC validada em rede real antes do código de produção - [ ] Chamada completa entre navegador móvel em 4G e Android - [ ] Tempo até o primeiro frame < 3s em rede boa - [ ] Teste: token de uma sala é rejeitado em outra sala - [ ] Teste: com LiveKit derrubado, o sistema degrada para áudio e depois para foto+texto - [ ] Teste: sala encerrada em todos os estados finais - [ ] SDK do LiveKit **não** aparece no bundle inicial do visitante - [ ] Bundle inicial continua ≤ 200KB gzip - [ ] Chamada funciona atrás de NAT simétrico (valida o coturn) ## Não faça nesta fase - Gravação (módulo `video_recording`, FASE 8) - App do morador (FASE 5) - Mais de 2 participantes por sala