# Gravação de videochamadas — implementação e evidências de 19/09/2026 Esta mudança acrescenta captura opcional de vídeo e geração de MP4 após a chamada. O WAV e o contrato existente de chamadas permanecem independentes. Os ensaios iniciais usaram somente material sintético. A ativação autorizada na VPS 1 está registrada ao final deste documento. Android/iPhone, chamadas prolongadas e capacidade de produção continuam pendentes de homologação. ## Comportamento implementado Com `ATENDROZAP_CALLS_VIDEO_RECORDING_ENABLED=true`, vídeo e gravação de áudio habilitados e FFmpeg/ffprobe disponíveis, a chamada captura as unidades H.264 recebidas e as efetivamente enviadas ao relay. A imagem Docker padrão `api` e a configuração padrão continuam com a gravação de vídeo desligada. O observador pertence à chamada, preservando a captura durante ausência, substituição e liberação do attachment do navegador. Frames incompletos ou escritas parciais no relay não viram frames enviados na gravação. Timestamps RTP, orientação e eventos de câmera acompanham o H.264 em uma fila limitada, sem escrita em disco nos goroutines de mídia. Depois do encerramento, o arquivo final é composto em MP4 H.264 1280×360 a 25 fps, com atendente à esquerda, contato à direita e áudio AAC estéreo derivado do WAV. Câmera desligada deixa a área correspondente preta. O WAV continua disponível separadamente. A saída só pode ser anunciada como `ready` após inspeção e decode integral; a composição tem orçamento de fila, disco, decodificação e tempo. Reordenação de apresentação H.264 não suportada gera falha explícita. `call_video_recordings` mantém o estado independente: `recording`, `processing`, `ready`, `failed` ou `expired`. O primeiro resultado terminal e seu webhook são persistidos juntos. O WAV necessário a uma captura ou composição pendente é preservado fisicamente até esse trabalho terminar; a expiração continua impedindo seu download quando o prazo já venceu. Reinício marca captura/composição pendente como `failed/worker_restarted`, sem retomada automática do trabalho. O histórico e `GET /call/{id}` acrescentam `videoRecording` somente quando existe captura. O endpoint `GET /call/{id}/recording/video` exige header `token`, mantém isolamento de instância/worker, aceita `Range` e usa cache privado desabilitado. O console faz proxy por sessão autenticada e apresenta player, download e estados de processamento, falha e expiração. Os subeventos `video_recording_ready`/`video_recording_failed` usam o envelope existente `EventType=call`. Atualizam apenas o vídeo: não são `ended`, não mudam motivo de encerramento, não liberam vaga e não substituem o objeto `recording` do áudio. Guia e prompt de integração: [Gravação de videochamadas](https://wpp.atendro.cloud/docs/guias/captura-video.md). ## Evidências RED/GREEN Os exemplos abaixo usam dados sintéticos e um PostgreSQL descartável. Nenhum dado privado ou endereço de banco de produção foi utilizado. “RED” descreve execução que falhou antes do respectivo ajuste; “GREEN” é o resultado executado depois. Testes de recursos inteiramente novos também são listados como cobertura, sem inventar uma execução anterior. | Camada e teste | Falha observada antes do ajuste | Resultado após o ajuste | |---|---|---| | HTTP — `TestVideoRecordingRouteRequiresHeaderCredential` | Rota de MP4 devolvia 404, quando o teste exigia 401 para credencial apenas na query | Passou: autenticação exclusivamente pelo header | | Serviço — `TestVideoRecordingSurvivesAbsentReplacedAndReleasedBridges` | Nenhum dos quatro frames esperados era observado (`0`, esperado `4`) | Passou: captura mantém o mesmo ciclo durante ausência, substituição e liberação da ponte | | Motor — `TestVideoRecordingOnlyObservesCompletelySentAccessUnits` | Nenhuma das duas access units esperadas era observada (`0`, esperado `2`) | Passou: só captura unidades aceitas e completamente enviadas | | Motor — `TestVideoRecordingReceivesAuthenticatedFramesWithoutBrowser` | Timeout de um segundo aguardando captura sem navegador | Passou: recebe H.264 após autenticação e remontagem SRTP mesmo sem sink do navegador | | Motor — `TestVideoRecordingExcludesPartiallyWrittenFrames` | Em escrita curta, continuava até quatro escritas, quando deveria parar na segunda | Passou: interrompe envio e não captura unidade incompleta | | Retenção — `TestRecordingCleanupPinsWAVUntilItsVideoJobEnds` | Quatro subcasos `recording/processing → ready/failed` removiam WAV ainda necessário | Passaram: mantém o WAV pendente e libera limpeza após resultado final do vídeo | | Gravador — `TestNewSourceAfterCameraOffDoesNotReuseOldParameterSets` | Nova câmera reutilizava SPS/PPS de outro SSRC | Passou: nova fonte precisa de seus próprios parâmetros antes de decodificar | | Validação MP4 — `TestVideoValidationRejectsTruncatedMP4WithIntactMetadata` | Arquivo truncado com metadados intactos era aceito como válido | Passou: decode integral recusa a saída corrompida | | H.264 adversarial — `TestEveryParameterSetIsValidatedBeforeCapture` | Nove subcasos aceitavam SPS fora dos limites escondido antes de outro SPS, conjuntos distintos na mesma AU ou extensões de NAL não suportadas | Passaram: valida todos os SPS e rejeita conjuntos ambíguos e extensões não suportadas; repetição idêntica continua permitida | Os testes de observação foram escritos após adicionar as interfaces e executados antes de ligá-las ao serviço/motor; portanto, suas falhas acima foram comportamentais, não apenas erros de compilação. Os dois testes do gravador foram reproduzidos em RED por `go test -overlay`, removendo somente as respectivas correções sem alterar o workspace. Sem o overlay, os mesmos testes passaram. A evidência mostra o efeito desses ajustes específicos, sem afirmar que toda a gravação existia no commit anterior. ## Cobertura executada por camada - **Motor e serviço:** recepção SRTP fragmentada, SPS/PPS, wrap do timestamp `0xfffffff0`, orientação CVO `3`, câmera desligada, filtro até keyframe, callback removido, registro tardio após encerramento e engine sem suporte. `go test -race ./internal/calls` passou. No módulo do fork, `go test -race .` e `go vet .` passaram. Os novos testes `TestVideoRecording` foram repetidos vinte vezes com detector de races. - **Persistência:** `TestVideoRecordingLifecycleAndFirstTerminalEvent`, `TestVideoRecordingCleanupRecoveryAndRetentionAreIndependent` e `TestVideoRecordingResultAndOutboxRollbackTogether` passaram. Exercitam atraso de eventos, rollback de resultado/outbox, preservação do WAV, isolamento de worker, expiração e ausência de republicação após poda do evento. `go test -race ./internal/storage` passou no banco descartável, incluindo a proteção do WAV durante processamento e `TestRecordingCleanupReleasesVideoPinsWhenRecoveringStoppedWorker`. - **Captura e composição:** `ATENDROZAP_TEST_VIDEO_TOOLS_REQUIRED=1 go test -race ./internal/callrecording` passou usando FFmpeg/ffprobe reais e conteúdo sintético. As verificações incluem pixels nas rotações 0/90/180/270°, preto com câmera desligada, início remoto atrasado, mudança de resolução no mesmo SSRC, áudio AAC com energia nos dois canais e PTS `0`, `0.2`, `1.5` segundos preservados com wrap RTP. `TestWaitDeadlineCancelsAndReapsRunningTranscoder` verifica cancelamento e espera de um processo filho real, sem shell. A revisão adversarial final acrescentou nove subcasos RED/GREEN e foi seguida de nova execução completa do pacote com `-race`, ferramentas obrigatórias e `vet`, todas aprovadas. - **HTTP:** testes focados de histórico, WAV, MP4, autenticação, Range, estados indisponíveis e limites de instância/worker passaram com `-race`. `TestOpenAPI` passou depois de acrescentar a operação à enumeração manual do teste de roteamento. Essa enumeração faltante foi corrigida; a rota já respondia nos testes HTTP específicos. - **Console:** `TestConsoleCallHistoryAndRecording` passou com `-race`. Verifica player, download, processamento, proxy Range, exigência de sessão e ausência de tokens no HTML e nos headers de resposta ao navegador. - **Documentação:** `python3 scripts/sync_api_docs.py`, a verificação `--check` e os três testes de `test_build_docs.py` passaram; o catálogo agora contém 69 operações. ## Verificação global e imagem opcional Verificações globais concluídas nesta implementação: - `go test -race ./...` passou com PostgreSQL descartável e `ATENDROZAP_TEST_VIDEO_TOOLS_REQUIRED=1`, incluindo os testes reais de FFmpeg/ffprobe com material sintético. A execução global foi repetida e passou após a última revisão adversarial de todos os SPS/PPS. - `go vet ./...` e `go build ./cmd/atendrozap` passaram, também repetidos depois dessa revisão final. - Testes com detector de races de `github.com/purpshell/meowcaller/...` e o `vet` do fork passaram. - Revisão automatizada do inventário: quatro testes Python e verificação de 60 dependências passaram. Os dezoito testes Node também passaram. - `python3 scripts/check_video_tools.py` passou: revisões e checksums das fontes nativas, avisos de licença e configuração revisada do FFmpeg foram conferidos. Essa verificação não substitui a execução da imagem pronta. A primeira execução global expôs uma espera incompleta no teste de sessão `TestServerErrorReceiptFailsSentMessage`: o status persistido já podia ser `failed` antes de o evento assíncrono aparecer no outbox, causando `unexpected events: []` na fixture em memória. O teste passou a aguardar as duas condições, sem mudar o código de produção. A execução focada repetida cem vezes passou, seguida de nova execução global verde. A imagem padrão `api` foi construída com sucesso e `scripts/test_engine_offline.sh` passou. O target `video-test`, derivado de `api-video` e usando exatamente seus executáveis de FFmpeg 8.1.2/libx264 fixados, também foi construído e executado com sucesso: ```sh docker build --target video-test -t atendrozap-video-test:local . docker run --rm --network none --read-only \ --tmpfs /tmp:rw,nosuid,nodev,size=512m,mode=1777 \ --cpus 2 --memory 1g atendrozap-video-test:local ``` Passaram vinte testes principais, além dos subcasos, incluindo composição MP4, decode integral, pixels, orientação, mudança de fonte e energia dos dois canais de áudio. A execução foi local em `linux/arm64`, com rede desligada e sistema de arquivos somente leitura, exceto `/tmp` limitado a 512 MiB. Os limites de duas CPUs e 1 GiB de memória descrevem o ensaio; não medem a capacidade de chamadas de produção. Na primeira execução desse container, a extração de áudio no helper de teste solicitava o muxer bruto `s16le`, ausente do runtime mínimo. A composição MP4/AAC e a validação do arquivo já funcionavam. O helper passou a extrair WAV e ler seu chunk PCM, preservando as verificações de amostras e energia dos dois canais. Nenhum codec de produção precisou ser acrescentado. Após esse ajuste exclusivamente de teste, o container passou e `go test -race ./...` com banco descartável/ferramentas obrigatórias, `go vet ./...` e `go build ./cmd/atendrozap` foram repetidos e passaram. O ensaio de CI em `linux/amd64` ocorrerá após o push; não é declarado verde neste relatório. Revisão e inventário das ferramentas nativas estão em `docs/LICENSE_REVIEW.md` e `third_party/video-tools/manifest.json`. ## Limites e validação física pendente Padrões: um render ativo e oito esperando por worker, fila de captura de 16 MiB por chamada, limite de 1 GiB para captura e, separadamente, para MP4, timeout de 30 minutos incluindo espera. Segmentos temporários, WAV e MP4 coexistem: esses limites não representam uma cota total de disco. O guia documenta limites de segmentos, geometria, referências e NALs. A composição usa CPU adicional e a fila reside no processo da API. Ainda não há dado para prometer quantidade de chamadas ou renders simultâneos numa VPS específica. A retenção deve considerar mídia, temporários e espaço livre; o banco guarda metadados, não os arquivos. A rotação segue a interpretação CVO atual. Os testes sintéticos verificam a aplicação do valor, não comprovam que Android/iPhone emitam a orientação esperada. Permanecem pendentes: gravação de voz/vídeo prolongada com Android/iPhone, comparação visual e A/V nos dois sentidos, câmera desligada/religada, troca de aba, Wi-Fi interrompido por dez segundos e medição de CPU/memória/disco sob carga representativa. A estabilidade anterior de uma chamada real não constitui homologação desta gravação. Preparar uma imagem pelo fluxo do repositório não conclui essas etapas e não implica ativação da opção. A publicação autorizada abaixo não substitui a homologação física pendente. ## Ativação autorizada na VPS 1 — 19/09/2026 Às 22:34:50 BRT (`2026-09-20T01:34:50Z`), API e console passaram da revisão `0df3b65` para `4f7e63f`, com a API construída pelo target `api-video`. FFmpeg e ffprobe 8.1.2 foram conferidos dentro do container em execução. Configuração global do worker: ```sh ATENDROZAP_CALLS_VIDEO_RECORDING_ENABLED=true ATENDROZAP_CALLS_VIDEO_RECORDING_RETENTION_DAYS=7 ATENDROZAP_CALLS_RECORDING_RETENTION_DAYS=30 ATENDROZAP_CALLS_VIDEO_RECORDING_MAX_CONCURRENT=1 ``` Aplica-se às próximas videochamadas das instâncias `calls` existentes e futuras. O MP4 expira sete dias depois do encerramento, sem reduzir o prazo do WAV ou excluir o histórico. A API bloqueia o download na expiração; a limpeza horária remove os arquivos vencidos. Não há geração retroativa. Antes do reinício foi confirmada a ausência de chamadas em andamento. Banco e configuração anteriores ficaram em backup privado na própria VPS: `/opt/atendrozap/pre-deploy/video-recording-4f7e63f-20260920T013431Z/`. Somente API e console foram recriados. A migração 18 foi aplicada no startup. Vinte e dois testes principais do gravador, mais subcasos, passaram usando os codecs exatos da imagem publicada em `linux/amd64`, rede desativada, filesystem somente leitura, `/tmp` de 512 MiB, uma CPU e 1 GiB de memória. O binário de testes foi compilado com Go 1.26.4 para essa arquitetura. O material foi sintético; nenhum número foi pareado e nenhuma chamada real foi iniciada para a validação. Após a ativação: readiness 200; `capabilities.calls_video_recording=true`; retenções anunciadas 7/30; OpenAPI público com o prazo independente de vídeo. As duas instâncias de chamadas e a instância de mensagens anteriormente conectadas voltaram a conectadas; a outra instância permaneceu desconectada. O Atendro já usa `videoRecording.expiresAt` para mostrar a disponibilidade. O canal SSH original de build ficou sem retornar, apesar de a etapa remota ter terminado: as três imagens, binários ops, revisão e remoção do bundle foram conferidos por outra conexão, seguidos do teste isolado acima. O `ship.sh` agora usa keepalive a cada 15 segundos para que builds longos e silenciosos mantenham a conexão e falhas de transporte sejam detectadas. ## Correção publicada: referências H.264 do navegador — 20/09/2026 Publicação autorizada da revisão `10346148924f058edeb5ea445621ea0ce6d704ed` (`1034614`) na VPS 1, às `2026-09-20T02:47Z`. API e console substituíram `4f7e63f`; a API foi construída com o target `api-video`, mantendo FFmpeg/ffprobe 8.1.2. O limite de referências do SPS passou de quatro para 16, preservando os limites de geometria e orçamento do decoder. O encoder nativo do Chrome produzia SPS válidos com seis/sete referências, recusados antes do MP4. Detalhes e regressões em `docs/DECISIONS.md` e `internal/callrecording/testdata/browser-h264/README.md`. Os 23 testes principais do pacote de gravação, mais subcasos, passaram na imagem Linux/amd64 preparada na VPS, com rede desativada, filesystem somente leitura, `/tmp` de 512 MiB, uma CPU e 1 GiB de memória. As fixtures do navegador produziram MP4 nos dois lados e passaram pela decodificação estrita do FFmpeg. Nenhuma chamada real foi iniciada. Antes da ativação havia zero chamadas abertas, zero capturas de áudio e zero gravações de vídeo em captura/processamento. Backup privado do banco, configuração e informações de retorno: `/opt/atendrozap/pre-deploy/h264-references-1034614-20260920T024713Z/`. Banco, proxy e serviço de backups não foram reiniciados. Após a publicação, o status operacional confirmou a revisão completa, readiness 200 e banco disponível. As duas sessões de chamadas e a sessão de mensagens anteriores voltaram a conectadas; a quarta instância continuou desconectada. Capabilities manteve captura de vídeo habilitada, vídeo por sete dias e áudio por 30 dias. A documentação pública passou a anunciar o limite 16. O status agregado `degraded` já existia antes da publicação; as pendências de webhooks não foram alteradas por esta correção. A correção aplica-se às próximas capturas. Registros de vídeo já terminados com falha não são reprocessados, pois seus temporários foram removidos. A origem exata da falha histórica continua sem comprovação pelo log genérico; a incompatibilidade com o SPS do navegador foi reproduzida e corrigida. ## Compatibilidade de orientação do cliente publicada — 20/09/2026 Às `2026-09-20T15:20:59Z` (12:20:59 BRT), a revisão `92437cd` substituiu `1034614` na API e no console da VPS 1. A imagem usa o target `api-video`. Foi habilitada `ATENDROZAP_CALLS_VIDEO_RECORDING_INVERT_REMOTE_ORIENTATION=true` para alinhar os novos MP4 ao padrão `invert=true` do controller do Atendro. A opção troca 90°/270° apenas no lado remoto durante a composição offline; o contrato RTP/API e a orientação do atendente permanecem independentes. O relato visual mostrava o cliente de cabeça para baixo. A diferença entre as duas fórmulas foi reproduzida com um retrato sintético de topo vermelho e base azul; a regressão confere as quatro posições no MP4, alterações de orientação/resolução na mesma chamada e quadros sem novo metadado. Ela também confere que o lado local mantém a própria rotação. O print não registra os valores CVO originais, e preferências manuais do navegador continuam fora do contrato de gravação. Os 24 testes principais do pacote, além dos subcasos, passaram com os codecs da imagem Linux/amd64 pronta, uma CPU, 1 GiB, rede desativada e filesystem somente leitura fora do `/tmp` sintético. A validação local completa passou com `go test -race ./...`, PostgreSQL 17 descartável, FFmpeg/ffprobe obrigatórios, `go vet ./...` e `go build ./cmd/atendrozap`. Nenhuma chamada real foi iniciada pelo agente. Havia zero chamadas, capturas de áudio ou vídeos em processamento antes do reinício. Backup privado do banco e da configuração: `/opt/atendrozap/pre-deploy/video-orientation-92437cd-20260920T152040Z/`. Após a atualização: readiness 200, flag de compatibilidade confirmada no container, gravação habilitada e retenções 7/30. As duas sessões de chamadas e a sessão de mensagens voltaram a conectadas; as duas outras instâncias continuaram desconectadas. Nenhum erro JSON de startup foi observado. Os dois vídeos já disponíveis não foram regravados. Os temporários e a linha do tempo de orientação original são descartados após compor o MP4, portanto não se aplica uma rotação fixa em lote que possa inverter trechos corretos. A confirmação visual no aparelho real depende de nova chamada.