Transmite miniaturas de video con cURL y tuberías de FFmpeg
Extraer miniaturas de videos remotos puede requerir la descarga de archivos grandes. Para los archivos cuyos metadatos y fotogramas solicitados están cerca del inicio, una solicitud de rango de bytes puede reducir esa descarga. Esta guía muestra la técnica de tubería y un script auxiliar de Bash que recurre a una descarga completa, en la que se puede saltar a cualquier punto, cuando un prefijo no es suficiente. Los rangos de bytes por sí solos no pueden garantizar una miniatura utilizable.
Comprende las solicitudes de rango HTTP con cURL
Una solicitud de rango HTTP permite que un cliente obtenga una porción de bytes de un recurso en
lugar del objeto completo. Los servidores pueden anunciar su compatibilidad con
Accept-Ranges: bytes, pero lo que importa es la respuesta a una solicitud GET de rango
real.
Primero, asegúrate de que el origen admita rangos:
curl -fsSL --range 0-0 --max-filesize 1 -D - -o /dev/null https://example.com/video.mp4
Busca una respuesta final 206 Partial Content y un Content-Range
correspondiente. El límite de tamaño evita que un rango ignorado descargue un archivo grande durante
esta comprobación. Para obtener el primer 1 MiB, usa la opción --range
(-r) de cURL en lugar de establecer la cabecera 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
Los extremos del rango son inclusivos. Un servidor puede ignorar el rango y devolver
200 OK con el cuerpo completo; cURL no convierte eso automáticamente en una
descarga parcial.
Envía datos parciales de video a FFmpeg mediante una tubería
FFmpeg puede leer de la entrada estándar (pipe:0) y normalmente analiza el
contenedor de forma automática. Usa -f solo cuando conozcas el formato.
La entrada MP4 debe admitir la lectura secuencial, normalmente con sus metadatos
moov al inicio («faststart»); una tubería no puede retroceder hasta los
datos multimedia después de leer los metadatos situados al final. Los siguientes ejemplos con
tuberías dan por sentado este diseño y una cantidad de bytes suficiente para decodificar el
fotograma solicitado. Ejecuta cada ejemplo con Bash en un directorio de salida nuevo:
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
Esto decodifica hacia adelante hasta los diez segundos y escribe un JPEG sin guardar el video de
entrada. FFmpeg puede cerrar la tubería en cuanto tiene el fotograma, lo que hace que cURL informe
un error de escritura (23). Con pipefail, eso hace que el pipeline falle
incluso si se produjo una imagen. El script auxiliar de automatización que aparece más abajo
descarga en un archivo temporal para evitar esta ambigüedad y permitir los saltos y los reintentos.
Calcula un rango de bytes adecuado
La cantidad de bytes que necesitas depende de la tasa de bits, la marca de tiempo y un margen para los metadatos del contenedor y el fotograma clave más cercano a la posición a la que quieres saltar. Una fórmula rápida y aproximada es:
bytes ≈ seconds × bitrate(B/s) + buffer
Con la tasa de bits en bytes por segundo (1 Mb/s ≈ 125.000 B/s) y un margen 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
Esto es solo una estimación: la tasa de bits variable y los metadatos al final pueden invalidarla. Después de ejecutar el cálculo anterior, solicita ese prefijo de la siguiente manera:
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
Automatiza el flujo de trabajo con un script auxiliar de bash
Guarda esto como thumbnail.sh y ejecútalo con Bash. Acepta una marca de tiempo con
el formato HH:MM:SS[.ms] y una tasa de bits estimada en números enteros de 1 a
999 Mb/s. El script auxiliar reintenta las descargas en un archivo temporal, primero prueba con el
prefijo y luego descarga el video completo si es necesario. Esa alternativa puede consumir el ancho
de banda y el espacio en disco del archivo completo.
#!/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"
Maneja distintos formatos de contenedor
No todos los archivos se pueden decodificar a partir de un prefijo. No hay garantía de que estos tamaños de rango ilustrativos contengan las cabeceras y los fotogramas necesarios; usa la alternativa de descarga completa del script auxiliar para la automatización.
MP4
Usa un prefijo solo cuando el átomo moov preceda a los datos multimedia.
Un prefijo más grande no resuelve el problema de los metadatos al final, a menos que incluya el
archivo completo:
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
Las cabeceras de WebM son más ligeras, pero los fotogramas clave pueden estar muy separados, así que una porción más grande ayuda:
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
Matroska admite saltar a cualquier punto, pero una tubería sigue requiriendo una decodificación secuencial. El prefijo debe contener las cabeceras de las pistas y suficientes clústeres para llegar al fotograma 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
Mejores prácticas de rendimiento
- Una descarga completa puede ser más rápida que varias solicitudes de prefijo fallidas; mide con tus propios archivos.
- Elige los tamaños de prefijo según la tasa de bits real y el diseño del contenedor, no solo según la resolución.
- Cuando necesites varias miniaturas de un mismo archivo, el filtro
selectde FFmpeg puede extraer varios fotogramas en una sola pasada:
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 reales
Cuadrículas de vista previa de video
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 bajo demanda para plataformas de streaming
Un segmento HLS de MPEG-TS sin cifrar y que se puede decodificar de forma independiente se puede leer directamente. Los segmentos de MP4 fragmentado que usan DASH y algunos flujos HLS también necesitan su segmento de inicialización; para esos casos, dale a FFmpeg la URL del manifiesto en lugar de enviar un segmento multimedia aislado por una tubería.
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
Procesamiento por lotes en paralelo
GNU Parallel puede ejecutar el script auxiliar para varias URL. Cada trabajo obtiene un nombre de archivo de salida 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
Soluciona problemas comunes
- Imágenes faltantes o incompletas: usa la alternativa de descarga completa y comprueba que la marca de tiempo esté dentro de la duración del video. FFmpeg puede terminar correctamente sin producir un fotograma cuando la marca de tiempo solicitada está más allá del final.
- Descargas lentas: usa un CDN cercano y mide los rangos adecuados.
--compressednegocia la compresión del cuerpo de la respuesta, no de las cabeceras, y por lo general no ayuda con el video comprimido. - FFmpeg no puede detectar el formato: busca un cuerpo de error HTTP o metadatos ausentes.
Forzar
-f mp4no puede reparar una entrada truncada o que no permite saltar a otra posición.
Conclusión
Las solicitudes de rango de bytes pueden reducir los costos de descarga de miniaturas cuando el diseño del archivo lo permite. Revisa los fallos de descarga y del decodificador, verifica que se haya producido una imagen y conserva una alternativa que permita saltar a cualquier punto.
Si prefieres un servicio listo para usar, echa un vistazo al Robot /video/thumbs de Transloadit. Se encarga de la detección de formatos, los reintentos y el escalado, para que puedas centrarte en crear tu producto.
