Miniatura de vídeo sem baixar o arquivo: YouTube, Vimeo e o seu próprio MP4
Use a capa que o YouTube e o Vimeo já publicam e, para vídeos enviados ao seu site, extraia um frame com ffmpeg uma única vez, num job, no momento do upload.
A plataforma de cursos fictícia Aula Aberta tem 340 aulas, e a página inicial mostra 24 delas com capa. Na primeira versão, cada capa era gerada baixando o vídeo do YouTube para o servidor e extraindo um frame. A home levava segundos para abrir e o disco enchia toda semana. A resposta curta: se o vídeo está no YouTube ou no Vimeo, a capa já existe num endereço que você obtém a partir do identificador do vídeo; guarde o id e use a imagem pronta. Só vídeo enviado ao seu próprio servidor precisa de extração, e ela acontece uma vez, num job, no upload.
Em nenhum dos casos a miniatura deve ser gerada na hora em que a página abre. Capa é uma imagem como outra qualquer: gerada ou obtida uma vez, guardada e servida com cache.
Por que baixar o vídeo para tirar um frame é o caminho longo
Um vídeo de aula de 12 minutos em 1080p passa fácil de 100 MB. Para tirar uma imagem de 50 KB, o servidor baixa o arquivo inteiro, abre com uma ferramenta de vídeo, decodifica até o ponto desejado e grava o JPEG. Se isso acontece a cada visualização, o custo cresce com o tráfego, e qualquer lentidão do provedor do vídeo vira lentidão da sua página.
Há também uma questão de termos de uso: baixar vídeos de plataformas como o YouTube fora dos meios oferecidos por elas costuma violar as regras do serviço. A capa publicada por elas existe justamente para ser exibida por quem incorpora o vídeo.
Como pegar a capa de um vídeo do YouTube
O YouTube publica miniaturas num endereço derivado do id do vídeo, aquele código de 11 caracteres que aparece em watch?v=. Os tamanhos mais usados:
https://i.ytimg.com/vi/ID/mqdefault.jpg: 320 por 180, já em 16:9, bom para listas.https://i.ytimg.com/vi/ID/hqdefault.jpg: 480 por 360, com faixas pretas em cima e embaixo nos vídeos 16:9.https://i.ytimg.com/vi/ID/maxresdefault.jpg: 1280 por 720, mas nem todo vídeo tem; quando não existe, o endereço responde com erro e uma imagem cinza genérica.
O primeiro passo é extrair o id de forma confiável, aceitando os formatos de link que as pessoas colam e recusando o resto:
function youtubeId(string $url): ?string
{
$partes = parse_url($url);
if ($partes === false || empty($partes['host'])) {
return null;
}
$host = strtolower($partes['host']);
$caminho = $partes['path'] ?? '';
if ($host === 'youtu.be') {
$id = ltrim($caminho, '/');
} elseif (in_array($host, ['youtube.com', 'www.youtube.com', 'm.youtube.com'], true)) {
if ($caminho === '/watch') {
parse_str($partes['query'] ?? '', $query);
$id = $query['v'] ?? '';
} elseif (preg_match('#^/(embed|shorts)/([^/]+)#', $caminho, $m)) {
$id = $m[2];
} else {
return null;
}
} else {
return null;
}
return preg_match('/^[A-Za-z0-9_-]{11}$/', $id) ? $id : null;
}
$id = youtubeId('https://youtu.be/aB3dE5fG7hJ?t=42');
// "aB3dE5fG7hJ"
$capa = 'https://i.ytimg.com/vi/' . $id . '/mqdefault.jpg';
Guarde o id no banco, não a URL colada. A validação final, 11 caracteres entre letras, números, hífen e sublinhado, impede que um texto qualquer vá parar dentro de um endereço de imagem ou de um iframe.
Se preferir não depender do formato do endereço, o YouTube também tem um endpoint oEmbed (https://www.youtube.com/oembed?url=...&format=json) que devolve, entre outros campos, thumbnail_url e o título do vídeo.
Capa que vira player só no clique
Na lista de aulas, você nem precisa carregar o player do YouTube, que pesa bem mais do que uma imagem. Mostre a capa dentro de um botão e troque pelo iframe quando a pessoa clicar:
<button type="button" class="video" data-id="aB3dE5fG7hJ">
<img src="https://i.ytimg.com/vi/aB3dE5fG7hJ/mqdefault.jpg"
width="320" height="180" loading="lazy" alt="">
<span>Assistir: Aula 12, formulários acessíveis</span>
</button>
<script>
document.querySelectorAll('button.video').forEach((botao) => {
botao.addEventListener('click', () => {
const iframe = document.createElement('iframe');
iframe.src = 'https://www.youtube-nocookie.com/embed/' + botao.dataset.id + '?autoplay=1';
iframe.title = botao.textContent.trim();
iframe.width = 560;
iframe.height = 315;
iframe.allow = 'autoplay; encrypted-media; picture-in-picture';
iframe.allowFullscreen = true;
botao.replaceWith(iframe);
});
});
</script>
O texto do botão diz o que vai acontecer, então a imagem pode ter alt vazio. Os atributos width e height reservam o espaço e evitam que a página pule quando a imagem chega.
Como pegar a capa de um vídeo do Vimeo
O Vimeo não usa um endereço de imagem previsível a partir do id, mas oferece oEmbed. Uma requisição para https://vimeo.com/api/oembed.json?url= seguida do endereço do vídeo (codificado) devolve um JSON com thumbnail_url, largura e altura. Faça essa chamada quando a aula é cadastrada, grave a URL da capa (ou baixe a imagem e sirva do seu domínio) e nunca a repita a cada visita. Com prazo de resposta curto, porque uma API externa lenta não pode travar o cadastro; o guia de timeout antes do infinito mostra como.
Quando o vídeo é seu: ffmpeg uma vez, no upload
Vídeo enviado por usuário não tem capa pronta. Aí sim você extrai um frame, seguindo estes passos:
- Valide o upload antes: tamanho máximo, tipo real do arquivo e duração. O guia de upload de arquivo em PHP cobre a primeira parte.
- Coloque a extração numa fila, fora da requisição web. O upload responde rápido e o job trabalha em segundo plano.
- Leia a duração com ffprobe para escolher um instante que exista.
- Extraia um frame com ffmpeg, já redimensionado.
- Grave o caminho da capa no registro da aula e mostre uma imagem padrão enquanto o job não termina.
$ ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 aula-12.mp4
754.320000
$ ffmpeg -nostdin -y -ss 3 -i aula-12.mp4 -frames:v 1 -vf scale=640:-2 -q:v 3 aula-12.jpg
O -ss antes do -i faz o ffmpeg pular direto para perto do instante pedido, sem decodificar tudo desde o início. O scale=640:-2 mantém a proporção com altura par, e o -q:v 3 dá um JPEG de boa qualidade e tamanho pequeno. Se os primeiros segundos costumam ser uma tela preta, o filtro thumbnail do ffmpeg escolhe um frame representativo de um trecho: -vf "thumbnail,scale=640:-2".
No job em PHP, monte o comando com escapeshellarg em cada caminho, porque o nome do arquivo veio, direta ou indiretamente, do usuário:
$cmd = 'ffmpeg -nostdin -y -ss 3 -i ' . escapeshellarg($origem)
. ' -frames:v 1 -vf scale=640:-2 -q:v 3 ' . escapeshellarg($destino);
exec($cmd . ' 2>&1', $saida, $codigo);
if ($codigo !== 0) {
throw new RuntimeException('ffmpeg falhou: ' . implode(PHP_EOL, $saida));
}
Se o vídeo está num armazenamento de objetos e o servidor aceita requisições com Range, o ffmpeg pode ler direto da URL e buscar só os trechos de que precisa, em vez de baixar o arquivo todo. Isso funciona melhor quando o MP4 foi gravado com o índice no início (opção -movflags +faststart na codificação).
Erros comuns
- Gerar a miniatura a cada visualização. O custo cresce com o tráfego e a página fica tão lenta quanto o processamento de vídeo.
- Usar
maxresdefaultsem alternativa. Vídeos sem essa resolução mostram uma imagem cinza no meio da sua vitrine. - Guardar a URL colada em vez do id validado. Links com parâmetros, encurtadores e textos inválidos acabam dentro do seu HTML.
- Extrair o frame dentro da requisição de upload. O upload demora, estoura o tempo limite e a pessoa envia de novo.
- Montar o comando do ffmpeg concatenando o nome do arquivo. Um nome com aspas ou ponto e vírgula vira comando no shell.
- Carregar dezenas de iframes do YouTube na mesma lista. Cada player traz scripts próprios e a página fica pesada sem ninguém ter dado play.
Checklist
- Vídeos de plataforma têm o id validado gravado no banco.
- A capa usada existe para todos os vídeos, com imagem padrão para falhas.
- Chamadas oEmbed acontecem no cadastro, com timeout, e o resultado fica salvo.
- Vídeos próprios geram capa num job, uma única vez, após validação do upload.
- Comandos de shell usam
escapeshellargem todo caminho. - Imagens de capa têm
width,heighteloading="lazy". - O player só carrega quando a pessoa pede.
Abra a página que lista vídeos no seu site com a aba de rede do navegador aberta e conte quantas requisições vão para o arquivo de vídeo ou para o player antes de qualquer clique. Se a resposta não for zero, qual delas pode virar uma imagem estática ainda hoje?
Referência: http://www.igorescobar.com/blog/2010/09/21/capturando-miniatura-de-videos-com-php/