Vídeo HTML5 com controles, legenda e busca que funciona
Player de vídeo HTML5 acessível: mantenha controls, some botões extras, legenda em WebVTT e servidor com Range para a busca funcionar. Testado no Chrome.
Uma plataforma de cursos troca o player nativo por um botão de play redondo e, na semana seguinte, chegam três reclamações: quem usa teclado não consegue pausar, a legenda sumiu e arrastar a barra de tempo volta o vídeo para o começo. Os dois primeiros problemas vêm de remover o atributo controls. O terceiro não tem nada a ver com o HTML: é o servidor.
Este post monta uma aula em vídeo com o player nativo, dois botões extras, legenda em WebVTT e um arquivo preparado para começar rápido, e mostra os testes que rodaram no Chrome 156. Se o seu caso é um vídeo decorativo que toca sozinho, sem fala, veja autoplay de vídeo bloqueado: mudo e com pausa.
Mantenha o controls e acrescente o que falta
O player nativo do elemento video já tem play, pausa, barra de tempo, volume, tela cheia e menu de legendas, tudo acessível por teclado e com nomes que o leitor de tela anuncia. Recriar isso do zero é muito trabalho para pouco ganho. O que costuma faltar numa aula são atalhos específicos, como voltar alguns segundos e mudar a velocidade, e eles podem ficar ao lado do player:
<!doctype html>
<html lang="pt-BR">
<meta charset="utf-8">
<style>
.player video { width: 100%; max-width: 960px; display: block; }
.extras { display: flex; gap: .5rem; margin-top: .5rem; }
.extras button:focus-visible { outline: 3px solid #1a73e8; outline-offset: 2px; }
</style>
<figure class="player">
<video id="aula" controls preload="metadata" poster="aula-01.jpg" width="1280" height="720">
<source src="aula-01.mp4" type="video/mp4">
<track kind="captions" src="aula-01.pt.vtt" srclang="pt-BR" label="Português" default>
<a href="aula-01.mp4">Baixar a aula 1 (MP4)</a>
</video>
<div class="extras" role="group" aria-label="Controles extras da aula">
<button type="button" id="voltar">Voltar 10 segundos</button>
<button type="button" id="velocidade">Velocidade: 1x</button>
</div>
<figcaption>Aula 1: instalando o Node.js. <a href="aula-01-transcricao.html">Ler a transcrição</a></figcaption>
</figure>
<script>
const video = document.getElementById("aula");
const velocidades = [1, 1.25, 1.5, 2];
document.getElementById("voltar").addEventListener("click", () => {
video.currentTime = Math.max(0, video.currentTime - 10);
});
const botaoVel = document.getElementById("velocidade");
botaoVel.addEventListener("click", () => {
const i = (velocidades.indexOf(video.playbackRate) + 1) % velocidades.length;
video.playbackRate = velocidades[i];
botaoVel.textContent = `Velocidade: ${velocidades[i]}x`.replace(".", ",");
});
</script>
</html>
Os botões são <button> de verdade, com texto que diz o que fazem, então funcionam com Enter e Espaço sem nenhum código de teclado. O link dentro do <video> só aparece em navegadores que não tocam vídeo, e o link da transcrição atende quem prefere ler.
A figura mostra a página aberta no Chrome, com a legenda na tela e os botões extras abaixo do player:

O teste de teclado
Um script com Puppeteer (t_aula.mjs) apertou Tab em sequência, usou Enter no botão de voltar depois de levar o vídeo para 15 s, usou Espaço no botão de velocidade e leu a faixa de legenda. A saída:
Tab: aula -> voltar -> velocidade -> a -> body -> aula Voltar 10 s (Enter) a partir de 15 s: 5 Velocidade (Espaço): [ 1.25, 'Velocidade: 1,25x' ] Legenda: ["showing",2]
O foco passa pelo player, pelos dois botões e pelo link da transcrição, nessa ordem. O voltar levou de 15 para 5 segundos, a velocidade foi para 1,25 e a faixa de legenda está ativa com as 2 falas do arquivo.
A legenda em WebVTT
A legenda é um arquivo de texto no formato WebVTT, ligado pelo elemento <track>:
WEBVTT 00:00:00.500 --> 00:00:04.000 Nesta aula você instala o Node.js e cria o primeiro projeto. 00:00:04.500 --> 00:00:09.000 Abra o terminal e confira a versão com node --version.
Três cuidados que evitam a legenda "sumida":
- O servidor deve entregar o arquivo como
text/vtt. - O
.vttprecisa estar no mesmo domínio da página, ou o servidor dele precisa liberar CORS e o vídeo precisa decrossorigin. - Abrindo a página direto do disco (
file://), o Chrome não carrega a faixa: no primeiro teste deste post, a legenda veio com 0 falas, e só apareceu ao servir a pasta por HTTP.
Para vídeo gravado com fala, legenda é o critério 1.2.2 da WCAG, de nível A. A legenda gerada automaticamente serve de rascunho, mas precisa de revisão: nome de comando e de biblioteca é onde ela mais erra.
Arrastar a barra e voltar para o zero: o servidor
Para pular para o meio do vídeo, o navegador pede só um trecho do arquivo com o cabeçalho Range, e o servidor tem de responder 206 Partial Content (veja requisições de intervalo na MDN). Servindo a mesma página com o python3 -m http.server, que responde sempre o arquivo inteiro, o teste mostrou o defeito da plataforma de cursos:
HTTP/1.0 200 OK
{ seekable: [ [ 0, 0 ] ], depois: 0 }
O vídeo não tem nenhum trecho "pulável" e o currentTime = 15 volta para 0. Com um servidor de teste que entende Range (servidor-range.mjs, em Node, sem dependências):
HTTP/1.1 206 Partial Content
Content-Type: video/mp4
Content-Length: 100
Content-Range: bytes 0-99/12123235
{ seekable: [ [ 0, 30 ] ], depois: 15 }
Nginx, Apache, IIS e os serviços de armazenamento de arquivos já respondem Range para arquivos estáticos. O problema aparece quando o vídeo passa por código seu, como uma rota que confere o login do aluno e devolve o arquivo. Nesse caso, a rota tem de tratar Range ou delegar a entrega ao servidor web. Confira também o tipo devolvido, assunto de MIME de vídeo no servidor.
O arquivo: faststart e poster
Um MP4 tem o índice (a caixa moov) e os dados (mdat). Muitos programas gravam o índice no fim, e aí o navegador precisa buscar o fim do arquivo antes de começar. A documentação do ffmpeg indica a opção +faststart para mover o índice para o começo, sem recomprimir:
ffmpeg -i aula-01-gravacao.mp4 -c copy -movflags +faststart aula-01.mp4
Um script curto em Python (atomos.py) lista a ordem das caixas e mostra a diferença no mesmo vídeo de teste:
sem-faststart.mp4 ftyp free mdat moov aula-01.mp4 ftyp moov free mdat
O poster sai do próprio vídeo, num quadro que represente a aula, e evita a tela preta enquanto os metadados carregam:
ffmpeg -ss 1 -i aula-01.mp4 -frames:v 1 -q:v 4 aula-01.jpg
Para gerar miniaturas sem baixar o vídeo inteiro, há um caminho em miniatura de vídeo sem baixar.
Quando vale um player todo seu
Quando o produto precisa de algo que o nativo não faz, como capítulos clicáveis, qualidade adaptativa (HLS ou DASH) ou marca d'água, uma biblioteca de player madura costuma sair mais barata que um player do zero. Em qualquer caso, refaça o teste de teclado e de legenda deste post no player novo antes de trocar.
Os arquivos de teste (página, legenda, servidor e scripts) rodaram no Chrome 156 headless, com um vídeo de padrão de cores gerado pelo ffmpeg. Safari e Firefox não foram testados aqui.
Referência: http://msdn.microsoft.com/pt-br/library/hh924823(v=vs.85).aspx