Ir para o conteúdo
Video2Any

Blog

2026-08-23

Decodificar vídeo em paralelo dentro de uma aba

O Video2Any parte de uma restrição que decide todo o resto: o seu vídeo não sai da sua máquina. Como você não envia o arquivo, não existe servidor fazendo o trabalho por você, e é o navegador que decodifica, compara e renderiza — tudo dentro de uma aba que ainda precisa continuar respondendo.

Aqui está o que essa restrição custa de verdade, e com o que o resto do site foi construído.

Uma linha do tempo de 92 minutos dividida em quatro trechos, cada um em sua faixa de worker em paralelo
Os quatro rodam ao mesmo tempo, então o tempo total é o do trecho mais lento e não a soma deles.
Nesta página

O demuxer você escreve

O WebCodecs te dá um VideoDecoder que transforma trechos codificados em quadros, mas não te dá os trechos. O navegador tem um parser de MP4 por dentro — é assim que o <video> funciona — e nenhuma API entrega esse parser, então o contêiner você lê na mão: percorre as boxes, acha o moov, lê a tabela de amostras e passa um EncodedVideoChunk de cada vez para o decodificador.

Ler o contêiner traz uma vantagem extra. Já que você está percorrendo ele, dá para avisar que um arquivo não vai abrir antes que alguém perca cinco minutos esperando: ProRes é formato de edição que navegador nenhum decodifica, e quem conta isso é o moov, que no ProRes costuma ficar no fim do arquivo e não no começo. É por isso que a nossa sonda varre as duas pontas.

Varrer primeiro e capturar depois, por causa da memória

O óbvio seria decodificar todos os quadros e comparar. Só que uma gravação de 92 minutos a 25fps são 138.000 quadros e, em 1080p, uma aba não segura essa quantidade.

Por isso o motor trabalha em duas passadas. A primeira é a varredura: ela decodifica só os pontos de amostragem e guarda cada amostra em 160×90, um tamanho pequeno o bastante para o vídeo inteiro caber em algumas dezenas de megas de pixels e grande o bastante para distinguir um slide do seguinte. Depois que o detector escolhe, a segunda passada, a de captura, volta para pegar os quadros em resolução cheia — e só os escolhidos. A varredura não passa de 900 amostras, e desse teto sai uma conclusão que vale conhecer: um vídeo de 29 minutos e um de 92 custam quase o mesmo, porque o custo é por amostra e não por minuto.

Um worker por pedaço, cada um com seu decodificador

O motor corta o vídeo por tempo e entrega cada pedaço a um worker com seu próprio VideoDecoder. O pool é min(4, núcleos / 2): usamos metade dos núcleos porque a aba ainda tem uma interface para mexer, e paramos em quatro pelo motivo da próxima seção.

Cada pedaço começa a varrer um pouco antes do próprio intervalo, para o detector ter com o que comparar o primeiro quadro, e depois descarta o que cai fora do que é dele. Os slides vão saindo conforme cada captura termina, não todos juntos no fim.

O preço de fatiar

Fatiar custa duas coisas, e é aí que fica interessante. A primeira: cada pedaço calibra o limiar no próprio trecho, então o mesmo vídeo cortado em quatro e em oito gera apresentações diferentes, ou seja, o resultado depende de quantos núcleos a máquina tem. A segunda: cada pedaço tirava duplicatas só dentro de si, então uma cena para a qual a reunião volta o tempo todo sobrevivia uma vez em cada pedaço em que apareceu. Numa gravação de 29 minutos, três slides de nove estavam repetidos.

As duplicatas já estão resolvidas: a checagem foi para a thread principal e roda conforme cada slide chega. A calibração não. Escrevemos a versão global e o resultado piorou: na mesma reunião, onde o ffmpeg conta sete cortes reais, calibrar por pedaço acha seis e calibrar globalmente acha quatro. Por isso o pool continua em quatro: fatiar mais é uns 25% mais rápido, mas também muda a resposta.

Nada aparece antes de o vídeo ser lido inteiro

O limiar vem da distribuição das diferenças entre quadros do vídeo inteiro, então o detector não consegue escolher o primeiro slide antes de amostrar o último quadro. Numa gravação de 92 minutos a varredura leva 52 segundos e o primeiro slide aparece no segundo 50.

Isso não é problema de desempenho e nenhum grau de paralelismo resolve: é problema de informação. A gente resolveu escrevendo "Lendo o vídeo · 45:46 de 92:08" onde antes havia só uma porcentagem.

O resto da stack

A API roda em Cloudflare Workers com Hono, as contas e os metadados ficam no D1, e as poucas coisas que precisam ser guardadas vão para o R2. O front é React 19 com TanStack Router, Tailwind v4 e better-auth. Oito idiomas, cada um com seu prefixo de URL e seu basepath de router.

O site é uma SPA pré-renderizada em vez de renderizada no servidor: cada página pública é um snapshot de HTML estático gerado no deploy, e por isso as páginas de marketing são cacheáveis e rápidas enquanto o conversor funciona sem servidor algum. A gente quebra essa regra em exatamente um lugar. As páginas /app são cascas estáticas, então o editor começava sem saber nada da sua conta e precisava perguntar; como ele começava a trabalhar antes de a resposta voltar, um assinante pagante acabou com o limite de trinta minutos do plano grátis. O worker já tem a sessão quando serve essas páginas, então agora ele escreve o plano no documento e a primeira renderização já sabe. Essas respostas são privadas e nunca são cacheadas; as outras páginas mantêm o cache público.

Por que se dar ao trabalho

Um servidor faria quase tudo isso sumir. Também significaria que toda aula, toda reunião interna e toda gravação médica que alguém converte passaria pelas nossas máquinas — e o produto existe justamente para que não passem.

A restrição não atrapalha o produto: ela é o produto. Tudo o que está acima é o que pagamos por ela.

Converter um vídeoBlog