4.2 KiB
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+coturne 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
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:
roomJoinapenas na sala daquela visitacanPublishpara vídeo e áudiocanPublishData: 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