No construí este proceso para experimentar con códecs. Lo hice porque las imágenes animadas se habían convertido en una forma cara de distribuir algo que, en la práctica, ya era vídeo corto y sin sonido.
Mi carga consiste sobre todo en WebP animado, GIF y APNG cortos: normalmente duran unos pocos segundos, suelen tener unas decenas de fotogramas visibles y contienen mucha redundancia temporal. La mayoría de las reproducciones llegan desde móviles, los mismos archivos pueden solicitarse muchas veces y el coste de codificar una sola vez importa mucho menos que los bytes que se sirven después.
La parte difícil no es ejecutar FFmpeg. Una imagen animada no tiene por qué ser una pila limpia de imágenes completas a una frecuencia regular. Puede contener rectángulos parciales, reglas de mezcla y descarte, canal alfa, retrasos irregulares, fotogramas de duración cero, orientaciones distintas y metadatos temporales que una herramienta genérica puede resumir mal.
Por eso trato la conversión como un conjunto de invariantes y no como una sola orden:
WebP animado / GIF / APNG
↓
reconstruir los estados completos del lienzo mostrado
↓
recuperar y normalizar la temporización de origen
↓
analizar todas las fuentes de la secuencia final
↓
elegir un único CFR para el MP4 final
↓
calcular el lienzo común mínimo sin ampliar
↓
codificar segmentos H.264 compatibles
↓
verificar el contrato del flujo
↓
concatenar copiando el flujo
↓
normalizar y validar la línea temporal de paquetes
↓
validar la entrega HTTP
↓
publicar de forma atómica
El códec importa, pero importa más conservar exactamente lo que la animación mostraba.
Un resultado medido en producción: 217 WebP animados se convirtieron en un MP4 de 78,49 MB
La entrada no era un vídeo de 1,49 GB. Eran 217 archivos WebP animados separados con 10.633 fotogramas visibles. En conjunto, las animaciones originales ocupaban unos 1,49 GB.
ENTRADA
217 archivos WebP animados
1,49 GB en total
10.633 fotogramas visibles
SALIDA
1 MP4 H.264
78,49 MB
0,98 Mbps
1264×720
30 fps CFR
Main@3.1 / CRF 28 / veryslow
El H.264 resultante pesaba 78,49 MB, con un flujo medio de unos 0,98 Mbps. Frente al tamaño combinado de las animaciones originales, son aproximadamente 19 veces menos, es decir, cerca de un 94,7% menos de datos.
Es un resultado real de extremo a extremo, no un A/B limpio de «H.264 antiguo frente a H.264 nuevo». La representación pasó de cientos de imágenes animadas a un solo vídeo con compresión temporal, así que no atribuyo toda la reducción de 19× a CRF 28, veryslow ni a una sola opción del codificador.
Primero reconstruyo las imágenes que el espectador realmente ve
El atajo más peligroso consiste en asumir que cada fotograma almacenado es una imagen completa que sustituye a la anterior.
Un fotograma de WebP animado puede describir un rectángulo situado en una posición concreta, junto con reglas de mezcla y descarte. APNG tiene desplazamientos, dimensiones, duración y operaciones de mezcla y descarte. GIF también puede conservar el lienzo anterior, limpiar una región o restaurar un estado previo.
Por tanto, un fotograma almacenado puede ser solo un parche cuyo significado depende del lienzo construido por los fotogramas anteriores. Codificar esos parches como imágenes completas produce una animación incorrecta, no una copia más pequeña de la correcta.
Mi unidad de extracción es el estado completo del lienzo mostrado: los píxeles ya compuestos que mostraría un visor correcto después de aplicar el descarte del fotograma anterior y la mezcla del actual.
Esta es la primera garantía de corrección. Una vez que un parche mal reconstruido queda aplanado dentro de H.264, ningún CRF, preajuste ni opción del contenedor puede repararlo.
La duración de cada fotograma es un dato de origen, no un FPS que haya que adivinar
Los formatos de animación guardan el tiempo de formas distintas. WebP animado usa duraciones por fotograma en unidades de 1 ms. GIF guarda retrasos en centésimas de segundo. APNG usa un numerador y un denominador para cada retraso; si el denominador es cero, la especificación PNG indica que debe tratarse como 100.
Esos retrasos forman la línea temporal. El FPS que muestre una herramienta genérica es solo un resumen y puede ser engañoso.
Un WebP real de mi proceso tenía 1264×720 y 49 fotogramas visibles. Los retrasos alternaban entre 62 y 63 ms, con una duración total de 3,063 segundos. En la práctica eso equivale a un ritmo de 16 fps, porque un fotograma a 16 fps dura 62,5 ms.
Sin embargo, una herramienta genérica informó 25 fps para ese archivo. Si hubiera confiado en esa cifra, habría cambiado la temporización original o creado repeticiones innecesarias.
También necesito una política explícita para retrasos defectuosos o ambiguos. WebP deja a la implementación la interpretación de una duración cero y, a menudo, de duraciones extremadamente pequeñas. GIF puede contener retrasos de cero. APNG permite numerador cero, que significa mostrar el siguiente fotograma lo antes posible, aunque el visor puede imponer un mínimo práctico.
Mi política conserva precisión de milisegundos, aplica un mínimo pequeño de 10 ms a duraciones nulas o claramente absurdas y usa 100 ms solo como valor de reserva cuando no existe información temporal útil. Son decisiones de ingeniería, no normas universales.
Elijo un único CFR para todo el MP4 final; no impongo 30 fps por defecto
Una vez reconstruidos los estados visibles y sus duraciones, proyecto la línea temporal original sobre una línea temporal de vídeo. No codifico todo ciegamente a 30 fps.
Para todas las fuentes que terminarán en el mismo MP4 evalúo este pequeño conjunto:
10, 12, 15, 16, 18, 20, 24, 25, 30 fps
El selector escoge el CFR más bajo que represente adecuadamente la secuencia completa. MP4 finales distintos pueden usar frecuencias diferentes, pero todos los segmentos codificados por separado dentro de un mismo MP4 usan exactamente el mismo CFR.
El ejemplo de 3,063 segundos hace visible el ahorro: a 16 fps necesita unos 49 fotogramas de salida; a 30 fps, unos 92. Si otra fuente del mismo MP4 necesita realmente 30 fps, toda la colección usa 30. No mezclo frecuencias distintas dentro de un mismo flujo final.
Uso una escala temporal de 90.000 Hz en la pista de vídeo, porque cada CFR permitido produce una duración entera:
10 fps → 9000 unidades
12 fps → 7500 unidades
15 fps → 6000 unidades
16 fps → 5625 unidades
18 fps → 5000 unidades
20 fps → 4500 unidades
24 fps → 3750 unidades
25 fps → 3600 unidades
30 fps → 3000 unidades
Es la escala de la pista de vídeo la que define esta cuadrícula exacta. También configuro la escala temporal general del MP4 a 90.000 por coherencia, pero es un reloj distinto del contenedor. El validador comprueba los paquetes de vídeo contra la cuadrícula entera exacta, no contra duraciones decimales redondeadas.
Los límites de resolución son techos, no lienzos obligatorios
Mi envolvente de entrega llega aproximadamente a 1280×720 en horizontal, 720×1280 en vertical y a un máximo de 960 tanto de ancho como de alto en material de orientación mixta.
La regla innegociable es no ampliar nunca. Una fuente de 900×600 no mejora al convertirla en 1280×720; solo crea píxeles interpolados que el codificador tendrá que describir.
La segunda regla es menos obvia: 960×960 es una envolvente máxima, no un lienzo cuadrado obligatorio.
Primero calculo las dimensiones activas de cada fuente permitiendo únicamente reducir. Después construyo el lienzo común par más pequeño capaz de contener todos esos rectángulos ya reducidos.
Por ejemplo, si la secuencia necesita una imagen horizontal de 960×540 y una vertical de 500×900, el lienzo común puede ser 960×900, no 960×960. Todos los segmentos siguen teniendo idénticas dimensiones codificadas, de modo que la concatenación por copia sigue siendo posible, pero evito codificar zona negra inútil.
Mantengo la relación de aspecto y relleno el espacio libre en vez de deformar la imagen. En mi proceso el fondo es negro. Como H.264/yuv420p ordinario no conserva el alfa original, la transparencia se compone deliberadamente contra ese fondo.
Por qué H.264 MP4 encaja bien en este problema de distribución
GIF, APNG y WebP animado no son formatos primitivos. También pueden evitar redibujar zonas sin cambios, así que afirmar que «el vídeo siempre es más pequeño» sería falso.
Pero H.264 está diseñado para predecir temporalmente entre imágenes. Los bucles ilustrados cortos, con fondos estáticos y regiones pequeñas que cambian, son un caso favorable para imágenes de referencia, predicción entre fotogramas y fotogramas P y B.
La documentación actual de Safari de Apple recomienda MP4 codificado con H.264 para vídeo estático y afirma que los GIF animados pueden consumir hasta 12 veces más ancho de banda y el doble de energía que un códec de vídeo moderno. Ese 12× es un ejemplo de Apple, no mi medición.
Mi resultado medido de 19× es útil como observación de producción, pero aun así comparo los archivos en lugar de asumir que cualquier WebP animado ya pequeño perderá frente a MP4.
Cada animación se codifica como un segmento bajo un único contrato de flujo
Cuando llega el momento de codificar, el proceso ya conoce los fotogramas visibles, el CFR común, el lienzo final y la duración esperada.
La parte central de mi orden para cada segmento es aproximadamente:
ffmpeg -framerate "$COLLECTION_FPS" -i frame-%06d.png \
-c:v libx264 \
-preset veryslow \
-tune animation \
-crf 28 \
-profile:v main \
-level:v 3.1 \
-pix_fmt yuv420p \
-tag:v avc1 \
-refs 4 \
-bf 5 \
-g "$GOP_FRAMES" \
-maxrate:v 4M \
-bufsize:v 8M \
-x264-params "stitchable=1:open-gop=0:b-pyramid=normal:nal-hrd=none" \
-color_range tv \
-color_primaries bt709 \
-color_trc bt709 \
-colorspace bt709 \
-fps_mode passthrough \
-map_metadata -1 \
-map_chapters -1 \
-an -sn -dn \
-video_track_timescale 90000 \
-movie_timescale 90000 \
-t "$EXPECTED_DURATION" \
segment.mp4
El GOP máximo ronda los cinco segundos y se deriva del CFR elegido: 80 fotogramas a 16 fps, 120 a 24 fps y 150 a 30 fps.
La protección -t "$EXPECTED_DURATION" no es decorativa. En mi lista de imágenes repito la última como elemento centinela para que se respete la duración del último fotograma real. Sin un límite explícito de duración, ese centinela puede convertirse en una muestra final adicional.
Lo reproduje con el caso de 49 fotogramas y 3,063 segundos. Sin -t obtuve 50 fotogramas a 16 fps y 94 a 30 fps. Con -t 3.063 obtuve los 49 previstos a 16 fps y 92 a 30 fps.
La concatenación por copia solo es segura después de comprobar la compatibilidad
El demultiplexor concat de FFmpeg espera los mismos flujos, incluido el códec y la base temporal, y utiliza la duración de cada archivo para colocar el siguiente. Una duración incorrecta puede crear artefactos temporales.
No uso la concatenación para volver compatibles archivos incompatibles. Cada segmento debe cumplir el contrato antes de aceptarse:
CFR de la colección = idéntico
base temporal del flujo = idéntica
escala temporal de la pista MP4 = idéntica
dimensiones del lienzo / SAR = idénticas
perfil / nivel / formato de píxel = idénticos
señalización de color = idéntica
avcC / datos adicionales AVC = idénticos byte a byte
Uso stitchable=1 de x264 porque los segmentos se codifican por separado, pero no trato ese parámetro como prueba de que la configuración AVC coincida. Antes de concatenar comparo los bytes reales de configuración.
Cuando el contrato se cumple, la unión final no necesita una segunda compresión:
ffmpeg -f concat -safe 0 -i segments.ffconcat \
-c:v copy \
-an -sn -dn \
-movflags +faststart \
final.mp4
-c:v copy evita decodificar y volver a comprimir los segmentos H.264 ya terminados.
La validación cubre tanto el MP4 como la forma en que se entrega
No publico un archivo solo porque FFmpeg termine con código 0.
El validador ha detectado fallos temporales deterministas reales: a 16 fps encontré 5580 unidades donde el contrato exigía 5625; más tarde, una salida a 24 fps contenía 3751 donde la cuadrícula exacta exigía 3750. La investigación detallada del desfase de una unidad es otro problema; aquí basta la lección de que repetir el mismo proceso no arregla un error temporal determinista.
En el objeto multimedia verifico el número esperado de flujos, perfil y nivel H.264, formato de píxel, dimensiones exactas planificadas, SAR, señalización de color, escala temporal de la pista de vídeo a 90 kHz, duraciones de paquetes, recuentos de fotogramas y paquetes, duración total, relaciones PTS/DTS, configuración AVC idéntica entre segmentos, moov antes de mdat y una decodificación completa con tratamiento estricto de errores.
ffprobe -v error \
-select_streams v:0 \
-show_streams \
-show_packets \
-of json \
final.mp4
ffmpeg -v error -xerror -err_detect explode \
-i final.mp4 -f null -
Pero un MP4 local correcto todavía puede servirse mal por la red. Por eso verifico también la entrega HTTP: Content-Type esperado, Content-Length correcto, compatibilidad con rangos de bytes, una respuesta válida 206 Partial Content y un Content-Range correcto.
Cuando cambio el contrato del codificador ejecuto además una prueba pequeña en dispositivos y navegadores reales; no supongo que ffprobe demuestre la compatibilidad del dispositivo. Compruebo inicio, desplazamiento, bucle, paso a segundo plano y reanudación, además de reproducción por rangos, en un iPhone actual con Safari, un Android modesto y los principales navegadores de escritorio.
Solo después de superar el contrato el archivo y su ruta de entrega sustituyo el recurso de producción de forma atómica.
Dónde este proceso pierde información de forma deliberada
Es una transformación para distribución, no un máster de archivo. El alfa se aplana. La temporización irregular se cuantiza a un solo CFR para el MP4 final. Las fuentes grandes pueden reducirse. Un WebP animado con pérdida recibe otra generación con pérdida. El audio se omite de forma intencionada.
No usaría exactamente este proceso cuando deba conservarse transparencia sobre fondos arbitrarios, cuando la temporización irregular exacta sea parte del significado, cuando esté creando una fuente de archivo o cuando la aplicación ya tenga una pila de vídeo adaptativa y multicodec que resuelva la distribución de otra manera.
También comparo primero los WebP animados diminutos y ya muy optimizados en vez de asumir que MP4 tiene que ganar.
La secuencia de producción que uso ahora
- Detectar el formato animado y leer los metadatos reales de control de fotogramas.
- Reconstruir estados completos del lienzo respetando las reglas de mezcla y descarte.
- Recuperar y normalizar la duración de cada fotograma.
- Construir la línea temporal original con precisión de milisegundos.
- Analizar todas las fuentes que terminarán en el mismo MP4.
- Elegir un único CFR común entre 10/12/15/16/18/20/24/25/30.
- Proyectar los estados visibles sobre esa línea temporal CFR.
- Calcular dimensiones activas permitiendo solo reducir; nunca ampliar.
- Construir el lienzo común par más pequeño que necesite la secuencia.
- Rellenar sin deformar y componer el alfa de forma intencionada.
- Codificar cada fuente como H.264 Main@3.1 / yuv420p / avc1 bajo el mismo contrato.
- Limitar cada segmento a su duración esperada.
- Rechazar cualquier segmento cuya configuración AVC o temporización real incumpla el contrato.
- Concatenar los segmentos aceptados con
-c:v copy. - Normalizar y validar la línea temporal final de paquetes.
- Decodificar completamente el resultado.
- Verificar cabeceras HTTP, rangos de bytes y respuestas parciales.
- Ejecutar una prueba en dispositivos y navegadores tras cambiar el perfil de codificación.
- Publicar de forma atómica solo cuando todo haya pasado.
La fuente de verdad es la línea temporal, no la extensión del archivo
Un WebP animado, GIF o APNG es una secuencia temporal de estados visibles del lienzo, no simplemente una carpeta de imágenes con cierta extensión.
H.264 puede aprovechar muy bien la redundancia temporal, pero no puede reparar una composición incorrecta, una temporización inventada ni metadatos de segmentos incompatibles. Gran parte de la ingeniería que volvió fiable este proceso ocurre antes y después de x264.
La lección que mantengo es esta: solo conviene cambiar la representación después de poder describir con precisión qué debe permanecer invariable.
Documentación primaria
- Especificación del contenedor WebP de Google — rectángulos, duración, mezcla y descarte de fotogramas.
- Especificación PNG de W3C, tercera edición — temporización, desplazamientos, mezcla y descarte de APNG.
- Especificación GIF89a — retrasos y comportamiento de descarte.
- Documentación de formatos de FFmpeg — requisitos de concat y comportamiento de MP4.
- Documentación de filtros de flujo de bits de FFmpeg —
setts. - Documentación de ffprobe — inspección de flujos y paquetes.
- Apple: entrega de contenido de vídeo para Safari — H.264 MP4 para vídeo estático y sustitución de GIF animados.
- Formatos multimedia compatibles con Android — compatibilidad con H.264 y requisitos de transmisión HTTP.