Miniaturas de vídeo via streaming com cURL e pipes do FFmpeg
Extrair miniaturas de vídeos remotos pode exigir o download de arquivos grandes. Para arquivos cujos metadados e frames solicitados ficam perto do início, uma requisição de intervalo de bytes pode reduzir esse download. Este guia mostra a técnica de pipe e um helper em Bash que recorre a um download completo, com suporte a busca, quando um prefixo não é suficiente. Intervalos de bytes, por si só, não garantem uma miniatura utilizável.
Entenda as requisições de intervalo HTTP com cURL
Uma requisição de intervalo HTTP permite que um cliente busque uma fatia de bytes de um recurso em
vez do objeto completo. Os servidores podem anunciar suporte com Accept-Ranges: bytes, mas o que importa é a
resposta a um GET de intervalo real.
Primeiro, verifique se a origem oferece suporte a intervalos:
curl -fsSL --range 0-0 --max-filesize 1 -D - -o /dev/null https://example.com/video.mp4
Procure uma resposta final 206 Partial Content e um Content-Range correspondente. O limite de tamanho impede
que um intervalo ignorado baixe um arquivo grande durante esse teste. Para buscar o primeiro 1 MiB,
use a flag --range (-r) do cURL em vez de definir o cabeçalho manualmente:
set -euo pipefail
mkdir video-prefix
curl -fsSL -r 0-1048575 --max-filesize 1048576 https://example.com/video.mp4 -o video-prefix/head.mp4
Os limites do intervalo são inclusivos. Um servidor pode ignorar o intervalo e retornar 200 OK com o
corpo completo; o cURL não transforma isso automaticamente em um download parcial.
Envie dados parciais de vídeo ao FFmpeg por pipe
O FFmpeg pode ler da entrada padrão (pipe:0) e normalmente detecta o contêiner automaticamente. Use
-f apenas quando você souber o formato. A entrada MP4 precisa permitir leitura sequencial,
normalmente com os metadados moov no início (“faststart”); um pipe não consegue voltar aos dados
de mídia depois de ler metadados no final. Os exemplos de pipe a seguir pressupõem esse layout e
bytes suficientes para decodificar o frame solicitado. Execute cada exemplo com Bash em um novo
diretório de saída:
set -euo pipefail
mkdir first-thumbnail
curl -fsSL https://example.com/video.mp4 | \
ffmpeg -nostdin -n -hide_banner -loglevel error \
-f mp4 -i pipe:0 \
-ss 00:00:10 -frames:v 1 -update 1 -f image2 first-thumbnail/thumbnail.jpg
Isso decodifica sequencialmente até os dez segundos e grava um JPEG sem salvar o vídeo de entrada. O
FFmpeg pode fechar o pipe assim que obtém o frame, fazendo o cURL reportar um erro de escrita (23).
Com pipefail, isso faz o pipeline falhar mesmo que uma imagem tenha sido gerada. O helper de
automação abaixo baixa para um arquivo temporário para evitar essa ambiguidade e permitir busca e
novas tentativas.
Calcule um intervalo de bytes adequado
A quantidade de bytes necessária depende do bitrate, do timestamp e de uma margem para os metadados do contêiner e para o keyframe mais próximo da posição de busca. Uma fórmula rápida e aproximada é:
bytes ≈ seconds × bitrate(B/s) + buffer
Com o bitrate em bytes por segundo (1 Mb/s ≈ 125.000 B/s) e uma margem de 1 MiB:
# Thumbnail at 10 s from a 5 Mb/s H.264 MP4 stream
BITRATE_BPS=$((5 * 125000)) # 625,000 B/s
SEEK_SECONDS=10
BUFFER=$((1 * 1024 * 1024)) # 1,048,576 B
BYTES_NEEDED=$((SEEK_SECONDS * BITRATE_BPS + BUFFER))
# Bytes_needed = 7,298,576
Isso é apenas uma estimativa: bitrate variável e metadados no final podem invalidá-la. Depois de executar o cálculo acima, solicite esse prefixo da seguinte forma:
set -euo pipefail
mkdir estimated-thumbnail
curl -fsSL -r "0-$((BYTES_NEEDED - 1))" --max-filesize "$BYTES_NEEDED" https://example.com/video.mp4 | \
ffmpeg -nostdin -n -hide_banner -loglevel error -f mp4 -i pipe:0 \
-ss 00:00:10 -frames:v 1 -update 1 -f image2 estimated-thumbnail/thumb.jpg
Automatize o fluxo de trabalho com um helper em Bash
Salve isto como thumbnail.sh e execute com Bash. O helper aceita um timestamp no formato HH:MM:SS[.ms]
e um bitrate estimado, em número inteiro, de 1 a 999 Mb/s. Ele grava os downloads em um arquivo
temporário e os repete em caso de falha, tenta primeiro o prefixo e depois baixa o vídeo completo, se
necessário. Esse fallback pode consumir a largura de banda e o espaço em disco do arquivo inteiro.
#!/usr/bin/env bash
set -euo pipefail
VIDEO_URL=${1:-}
TIMESTAMP=${2:-00:00:05} # HH:MM:SS[.ms]
BITRATE_MBPS=${3:-5} # average megabits-per-second
OUT=${4:-thumbnail.jpg}
if [[ -z "$VIDEO_URL" ]]; then
echo "Usage: $0 <url> [timestamp] [bitrate_mbps] [out]" >&2
exit 1
fi
if [[ ! "$TIMESTAMP" =~ ^[0-9]{2}:[0-5][0-9]:[0-5][0-9]([.][0-9]{1,3})?$ ]] ||
[[ ! "$BITRATE_MBPS" =~ ^[1-9][0-9]{0,2}$ ]]; then
echo "Use HH:MM:SS[.ms] and a whole-number bitrate from 1 to 999 Mb/s" >&2
exit 1
fi
if [[ -e "$OUT" || -L "$OUT" ]]; then
echo "Output already exists: $OUT" >&2
exit 1
fi
# Convert timestamp → seconds
IFS=: read -r H M S <<< "$TIMESTAMP"
SEEK_SECONDS=$((10#$H * 3600 + 10#$M * 60 + 10#${S%.*} + 1))
BYTES=$((SEEK_SECONDS * BITRATE_MBPS * 125000 + 1048576))
WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/video-thumb.XXXXXX")
trap 'rm -f -- "$WORK_DIR/input.mp4" "$WORK_DIR/thumb.jpg"; rmdir "$WORK_DIR"' EXIT
make_thumbnail() {
rm -f -- "$WORK_DIR/thumb.jpg"
ffmpeg -nostdin -y -hide_banner -loglevel error -xerror \
-ss "$TIMESTAMP" -i "$WORK_DIR/input.mp4" -map 0:v:0 \
-frames:v 1 -q:v 2 -c:v mjpeg -update 1 -f image2 "$WORK_DIR/thumb.jpg" &&
[[ -s "$WORK_DIR/thumb.jpg" ]]
}
if curl -fsSL --retry 3 --proto '=http,https' --proto-redir '=http,https' \
--range "0-$((BYTES - 1))" --max-filesize "$BYTES" \
-o "$WORK_DIR/input.mp4" -- "$VIDEO_URL" && make_thumbnail; then
echo "Thumbnail extracted from the initial download"
else
echo "Retrying with a complete, seekable download" >&2
curl -fsSL --retry 3 --proto '=http,https' --proto-redir '=http,https' \
-o "$WORK_DIR/input.mp4" -- "$VIDEO_URL"
if ! make_thumbnail; then
echo "No thumbnail decoded at $TIMESTAMP" >&2
exit 1
fi
fi
# Refuse to replace an output created by another process during the download.
(set -o noclobber; cat "$WORK_DIR/thumb.jpg" > "$OUT")
echo "Thumbnail saved to $OUT"
Lide com diferentes formatos de contêiner
Nem todo arquivo pode ser decodificado a partir de um prefixo. Estes tamanhos de intervalo ilustrativos não têm garantia de conter os cabeçalhos e frames necessários; use o fallback de download completo do helper para automação.
MP4
Use um prefixo apenas quando o atom moov vier antes dos dados de mídia. Um prefixo maior não
resolve metadados no final, a menos que inclua o arquivo inteiro:
set -euo pipefail
mkdir mp4-thumbnail
curl -fsSL -r 0-5242879 https://example.com/video.mp4 | \
ffmpeg -nostdin -n -hide_banner -loglevel error -f mp4 -i pipe:0 -ss 00:00:05 \
-frames:v 1 -update 1 -f image2 mp4-thumbnail/thumb.jpg
WebM
Os cabeçalhos WebM são mais leves, mas os keyframes podem ficar bem espaçados, então uma fatia maior ajuda:
set -euo pipefail
mkdir webm-thumbnail
curl -fsSL -r 0-10485759 https://example.com/video.webm | \
ffmpeg -nostdin -n -hide_banner -loglevel error -f webm -i pipe:0 -ss 00:00:05 \
-frames:v 1 -update 1 -f image2 webm-thumbnail/thumb.jpg
MKV
O Matroska oferece suporte a busca, mas um pipe ainda exige decodificação sequencial. O prefixo precisa conter os cabeçalhos das faixas e clusters suficientes para chegar ao frame solicitado:
set -euo pipefail
mkdir mkv-thumbnail
curl -fsSL -r 0-15728639 https://example.com/video.mkv | \
ffmpeg -nostdin -n -hide_banner -loglevel error -f matroska -i pipe:0 -ss 00:00:05 \
-frames:v 1 -update 1 -f image2 mkv-thumbnail/thumb.jpg
Boas práticas de desempenho
- Um download completo pode ser mais rápido do que várias requisições de prefixo com falha; faça medições com seus arquivos.
- Escolha os tamanhos de prefixo com base no bitrate real e no layout do contêiner, e não apenas na resolução.
- Quando você precisar de várias miniaturas de um mesmo arquivo, o filtro
selectdo FFmpeg pode extrair vários frames em uma única passada:
set -euo pipefail
mkdir selected-thumbnails
curl -fsSL -r 0-15728639 https://example.com/video.mp4 | \
ffmpeg -nostdin -n -hide_banner -loglevel error -f mp4 -i pipe:0 \
-vf "select=eq(n\,150)+eq(n\,300)+eq(n\,450)" -fps_mode vfr -f image2 selected-thumbnails/thumb_%02d.jpg
Casos de uso reais
Grades de pré-visualização de vídeo
set -euo pipefail
mkdir preview-grid
curl -fsSL -r 0-20971519 https://example.com/video.mp4 | \
ffmpeg -nostdin -n -hide_banner -loglevel error -f mp4 -i pipe:0 \
-vf "select='not(mod(n,300))',scale=160:90,tile=4x3" \
-frames:v 1 -update 1 -f image2 preview-grid/preview.jpg
Miniaturas sob demanda para plataformas de streaming
Um segmento HLS MPEG-TS não criptografado e decodificável de forma independente pode ser lido diretamente. Segmentos MP4 fragmentados usados por DASH e por alguns streams HLS também precisam do segmento de inicialização; nesses casos, passe ao FFmpeg a URL do manifesto em vez de enviar por pipe um segmento de mídia isolado.
set -euo pipefail
mkdir live-thumbnail
curl -fsSL https://example.com/live/segment-123.ts | \
ffmpeg -nostdin -n -hide_banner -loglevel error -f mpegts -i pipe:0 \
-frames:v 1 -update 1 -f image2 live-thumbnail/live_thumb.jpg
Processamento em lote paralelo
O GNU Parallel pode executar o helper para várias URLs. Cada tarefa recebe um nome de arquivo de saída distinto:
parallel --halt soon,fail=1 -j 4 bash ./thumbnail.sh {} 00:00:10 5 thumb_{#}.jpg ::: \
https://cdn.example.com/a.mp4 \
https://cdn.example.com/b.mp4 \
https://cdn.example.com/c.mp4
Solucione problemas comuns
- Imagens ausentes ou incompletas: use o fallback de download completo e verifique se o timestamp está dentro da duração do vídeo. O FFmpeg pode terminar com sucesso sem gerar um frame quando o timestamp solicitado está além do final.
- Downloads lentos: use uma CDN próxima e meça os intervalos adequados.
--compressednegocia a compressão do corpo da resposta, não dos cabeçalhos, e geralmente não ajuda com vídeo já comprimido. - O FFmpeg não consegue detectar o formato: verifique se há um corpo de erro HTTP ou metadados
ausentes. Forçar
-f mp4não repara uma entrada truncada ou sem suporte a busca.
Conclusão
Requisições de intervalo de bytes podem reduzir o custo de download de miniaturas quando o layout do arquivo permite. Verifique falhas de download e do decodificador, confirme que uma imagem foi gerada e mantenha um fallback com suporte a busca.
Se você preferir um serviço pronto para uso, conheça o Robot /video/thumbs da Transloadit. Ele cuida da detecção de formato, das novas tentativas e do escalonamento conforme o volume de processamento cresce, para que você possa se concentrar em desenvolver seu produto.
