Gravação de vídeo: evidências

Relatório de implementação, testes executados e homologação física pendente.

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.

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 testeFalha observada antes do ajusteResultado após o ajuste
HTTP — TestVideoRecordingRouteRequiresHeaderCredentialRota de MP4 devolvia 404, quando o teste exigia 401 para credencial apenas na queryPassou: autenticação exclusivamente pelo header
Serviço — TestVideoRecordingSurvivesAbsentReplacedAndReleasedBridgesNenhum 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 — TestVideoRecordingOnlyObservesCompletelySentAccessUnitsNenhuma das duas access units esperadas era observada (0, esperado 2)Passou: só captura unidades aceitas e completamente enviadas
Motor — TestVideoRecordingReceivesAuthenticatedFramesWithoutBrowserTimeout de um segundo aguardando captura sem navegadorPassou: recebe H.264 após autenticação e remontagem SRTP mesmo sem sink do navegador
Motor — TestVideoRecordingExcludesPartiallyWrittenFramesEm escrita curta, continuava até quatro escritas, quando deveria parar na segundaPassou: interrompe envio e não captura unidade incompleta
Retenção — TestRecordingCleanupPinsWAVUntilItsVideoJobEndsQuatro subcasos recording/processing → ready/failed removiam WAV ainda necessárioPassaram: mantém o WAV pendente e libera limpeza após resultado final do vídeo
Gravador — TestNewSourceAfterCameraOffDoesNotReuseOldParameterSetsNova câmera reutilizava SPS/PPS de outro SSRCPassou: nova fonte precisa de seus próprios parâmetros antes de decodificar
Validação MP4 — TestVideoValidationRejectsTruncatedMP4WithIntactMetadataArquivo truncado com metadados intactos era aceito como válidoPassou: decode integral recusa a saída corrompida
H.264 adversarial — TestEveryParameterSetIsValidatedBeforeCaptureNove subcasos aceitavam SPS fora dos limites escondido antes de outro SPS, conjuntos distintos na mesma AU ou extensões de NAL não suportadasPassaram: 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.