Para transmitir video HLS con FFmpeg y un CDN, codifica tu archivo con ffmpeg -i input.mp4 -hls_time 6 -hls_list_size 0 output.m3u8, sube los segmentos a S3 con headers de caché agresivos y reprodúcelos en el navegador con hls.js. Para bitrate adaptativo, codifica a múltiples resoluciones y crea una playlist maestra que apunte a cada nivel de calidad.
Si alguna vez intentaste servir un MP4 directamente desde un CDN, probablemente notaste el problema: todos los usuarios reciben el mismo bitrate sin importar su conexión, y el caché del CDN funciona mal porque cada seek genera range requests contra el origen.
HLS (HTTP Live Streaming) resuelve exactamente eso. El protocolo, definido en RFC 8216, divide el video en segmentos pequeños que se sirven como archivos HTTP normales — justo lo que los CDN saben cachear bien. Con FFmpeg puedes convertir cualquier archivo fuente a HLS con un solo comando.
En esta guía vas a configurar todo el pipeline: codificación HLS básica, bitrate adaptativo (ABR) con playlist maestra, subida a S3 y distribución por CloudFront, configuración CORS, y reproducción en el navegador con hls.js.
Qué es HLS
HLS (HTTP Live Streaming) es un protocolo de streaming desarrollado por Apple. Funciona dividiendo un video en segmentos pequeños (archivos .ts, típicamente de unos pocos segundos cada uno) y proporcionando un archivo índice llamado playlist (.m3u8) que lista todos los segmentos en orden.
Comparado con servir un MP4 simple, HLS ofrece tres ventajas principales:
- Caché a nivel de segmento: Cada segmento es una solicitud HTTP independiente, lo que los CDN cachean con alta eficiencia
- Bitrate adaptativo (ABR): El reproductor cambia de nivel de calidad en tiempo real según el ancho de banda disponible (cuando se usa una playlist maestra)
- Búsqueda eficiente: El reproductor solo descarga el segmento que contiene la marca de tiempo solicitada, haciendo que navegar por videos largos sea rápido
Hay dos tipos de playlists. Una playlist de medios lista los segmentos para un solo nivel de calidad. Una playlist maestra lista múltiples playlists de medios, una por nivel de calidad — ese es el punto de entrada cuando ABR está en uso.
Codificación HLS de un solo bitrate
Comencemos con el caso más simple: convertir un archivo de video a HLS con un solo bitrate.
ffmpeg -i input.mp4 \
-c:v libx264 \
-preset fast \
-crf 22 \
-c:a aac \
-b:a 128k \
-hls_time 6 \
-hls_list_size 0 \
-hls_segment_filename "output/segment_%04d.ts" \
output/index.m3u8
La lista completa de opciones del muxer HLS está en la documentación oficial de FFmpeg. Esto es lo que hace cada flag relevante:
| Flag | Propósito |
|---|---|
-hls_time 6 | Duración objetivo del segmento en segundos. De 4 a 10 segundos es común para VOD |
-hls_list_size 0 | Mantener todos los segmentos en la playlist (el streaming en vivo usa un valor diferente) |
-hls_segment_filename | Patrón de ruta para los archivos de segmento de salida |
-crf 22 | Nivel de calidad — menor es mejor calidad y mayor tamaño de archivo. El rango práctico es 18–28 |
-preset fast | Balance entre velocidad de codificación y compresión. slow genera archivos más pequeños, fast ejecuta más rápido |
Después de ejecutar el comando, el directorio output/ contendrá:
output/
├── index.m3u8 # Archivo de playlist
├── segment_0000.ts # Primer segmento (~6 segundos)
├── segment_0001.ts
├── segment_0002.ts
└── ...
Sube este directorio a cualquier host estático o CDN y estará listo para reproducir.
Construye streams de bitrate adaptativo (ABR)
Con un solo bitrate, los espectadores con conexiones lentas reciben el mismo archivo que los de conexiones rápidas — lo que provoca buffering para algunos y desperdicio de ancho de banda para otros. ABR permite que el reproductor mida la velocidad de conexión y seleccione automáticamente el mejor nivel de calidad para cada espectador.
Codifica tres niveles de calidad por separado:
# Calidad baja (360p, ~500 kbps)
ffmpeg -i input.mp4 \
-c:v libx264 -preset fast -crf 28 \
-vf "scale=640:360" \
-c:a aac -b:a 96k \
-hls_time 6 -hls_list_size 0 \
-hls_segment_filename "output/360p/segment_%04d.ts" \
output/360p/index.m3u8
# Calidad media (720p, ~1500 kbps)
ffmpeg -i input.mp4 \
-c:v libx264 -preset fast -crf 23 \
-vf "scale=1280:720" \
-c:a aac -b:a 128k \
-hls_time 6 -hls_list_size 0 \
-hls_segment_filename "output/720p/segment_%04d.ts" \
output/720p/index.m3u8
# Calidad alta (1080p, ~3000 kbps)
ffmpeg -i input.mp4 \
-c:v libx264 -preset fast -crf 20 \
-vf "scale=1920:1080" \
-c:a aac -b:a 192k \
-hls_time 6 -hls_list_size 0 \
-hls_segment_filename "output/1080p/segment_%04d.ts" \
output/1080p/index.m3u8
Una vez que las tres codificaciones terminen, crea la playlist maestra manualmente:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=500000,RESOLUTION=640x360,CODECS="avc1.42e01e,mp4a.40.2"
360p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1500000,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
720p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2"
1080p/index.m3u8
Guarda esto como output/master.m3u8. El reproductor carga este archivo primero, luego cambia entre las playlists de medios en tiempo real según el ancho de banda medido. La estructura final del directorio se ve así:
output/
├── master.m3u8 # Playlist maestra (punto de entrada del reproductor)
├── 360p/
│ ├── index.m3u8
│ ├── segment_0000.ts
│ └── ...
├── 720p/
│ ├── index.m3u8
│ └── ...
└── 1080p/
├── index.m3u8
└── ...
Subida a un CDN
La configuración más común es S3 (o un almacenamiento compatible con S3 como Cloudflare R2) como origen, con CloudFront o Cloudflare al frente para caché y distribución global.
Sube los archivos con la CLI de AWS, aplicando diferentes headers de caché para playlists y segmentos:
# Subir playlists m3u8 — deshabilitar caché para que los reproductores siempre obtengan la última versión
aws s3 sync output/ s3://your-bucket-name/videos/sample/ \
--exclude "*" \
--include "*.m3u8" \
--content-type "application/vnd.apple.mpegurl" \
--cache-control "no-cache, no-store"
# Subir segmentos ts — cachear agresivamente ya que los archivos de segmento nunca cambian
aws s3 sync output/ s3://your-bucket-name/videos/sample/ \
--exclude "*" \
--include "*.ts" \
--content-type "video/mp2t" \
--cache-control "public, max-age=31536000, immutable"
La estrategia de caché importa aquí:
- Playlists m3u8: Usa
no-cache. Una playlist puede cambiar si se agregan segmentos (en vivo) o el video se recodifica. Quieres que los reproductores siempre obtengan una copia fresca. - Segmentos ts: Usa
max-age=31536000(un año). Los nombres de archivo de los segmentos contienen un número de secuencia que nunca cambia, así que el contenido es inmutable — deja que el CDN lo cachee indefinidamente.
Si usas CloudFront, configura comportamientos de caché separados para *.m3u8 y *.ts para aplicar estas políticas de forma independiente.
Configuración de CORS y seguridad
Cuando hls.js carga segmentos desde un CDN en un origen diferente al de tu sitio web, el navegador aplica CORS. Sin los headers correctos, el navegador bloqueará cada solicitud de segmento y el video no se reproducirá.
Aplica una regla CORS al bucket S3:
# Crear el archivo de configuración y aplicarlo
cat > cors-config.json << 'EOF'
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedOrigins": ["https://your-site.com"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]
EOF
aws s3api put-bucket-cors \
--bucket your-bucket-name \
--cors-configuration file://cors-config.json
Para CloudFront, agrega una política de headers de respuesta que incluya Access-Control-Allow-Origin. En la consola de AWS, ve a CloudFront → Response Headers Policies → Create policy y agrega los headers CORS ahí.
En cuanto a la seguridad, mantén el bucket S3 privado y usa Origin Access Control (OAC) para que solo CloudFront pueda leer de él. Para contenido de pago o autenticado, las URLs firmadas o cookies firmadas de CloudFront te permiten restringir el acceso a espectadores específicos.
Reproducción de HLS en el navegador con hls.js
Safari e iOS soportan HLS de forma nativa a través del elemento <video>. Chrome y Firefox requieren hls.js. El componente a continuación maneja ambos casos:
"use client";
import { useEffect, useRef } from "react";
import Hls from "hls.js";
interface HlsPlayerProps {
src: string;
poster?: string;
}
export function HlsPlayer({ src, poster }: HlsPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
// Safari has native HLS support — no hls.js needed
if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = src;
return;
}
if (!Hls.isSupported()) {
console.error("HLS is not supported in this browser");
return;
}
const hls = new Hls({
maxBufferLength: 30,
maxMaxBufferLength: 600,
});
hls.loadSource(src);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play().catch(() => {
// Autoplay may be blocked by browser policy unless the video is muted
});
});
hls.on(Hls.Events.ERROR, (_, data) => {
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
hls.startLoad(); // Retry on network errors
break;
case Hls.ErrorTypes.MEDIA_ERROR:
hls.recoverMediaError(); // Attempt recovery on decode errors
break;
default:
hls.destroy(); // Unrecoverable error — clean up
}
}
});
return () => {
hls.destroy();
};
}, [src]);
return (
<video
ref={videoRef}
controls
poster={poster}
style={{ width: "100%", aspectRatio: "16/9" }}
/>
);
}
Instala hls.js con npm install hls.js. Desde la v1.6, hls.js incluye sus propios tipos de TypeScript, así que no necesitas @types/hls.js por separado.
Usa el componente así:
// app/video/page.tsx
import { HlsPlayer } from "@/components/HlsPlayer";
export default function VideoPage() {
return (
<main>
<h1>Sample Video</h1>
<HlsPlayer
src="https://cdn.your-site.com/videos/sample/master.m3u8"
poster="https://cdn.your-site.com/videos/sample/thumbnail.jpg"
/>
</main>
);
}
Solución de problemas
Aquí están los problemas más comunes y cómo solucionarlos.
Los segmentos devuelven 404 o errores CORS
- Verifica que las reglas CORS del bucket S3 incluyan los
AllowedOriginscorrectos - Comprueba que la política de headers de respuesta de CloudFront tenga
Access-Control-Allow-Originconfigurado - Confirma que los archivos
.tsse subieron conContent-Type: video/mp2t— usarapplication/octet-streampuede causar problemas con algunos reproductores
La reproducción se detiene a mitad del video
- La duración del segmento puede ser demasiado larga. Intenta reducir
-hls_timea 4 segundos - Verifica si el caché del CDN realmente está funcionando. Revisa la tasa de aciertos de caché de CloudFront en las métricas de CloudWatch
- El bitrate del stream de baja calidad aún puede ser demasiado alto para conexiones lentas. Intenta aumentar el CRF para 360p a 30 o más
Safari no puede reproducir el stream
- Asegúrate de que los archivos
.m3u8se sirvan conContent-Type: application/vnd.apple.mpegurl - Verifica que no haya redirecciones de S3 interfiriendo. Accede a la URL del m3u8 directamente en un navegador para verificar
hls.js lanza MANIFEST_LOAD_ERROR
- Revisa que la URL de la playlist maestra sea correcta
- Asegúrate de que la lista de
AllowedOriginsen CORS incluyahttp://localhost:3000para desarrollo local (agrégalo como una entrada separada junto al dominio de producción)
La codificación es demasiado lenta
- Cambia
-presetaultrafastpara una codificación más rápida a costa del tamaño del archivo - Usa codificación por GPU:
-c:v h264_nvencpara NVIDIA,-c:v h264_videotoolboxen Mac. Consulta la Guía de codificación GPU para más detalles - Ejecuta las tres codificaciones en paralelo usando jobs en segundo plano de bash:
ffmpeg -i input.mp4 -vf "scale=640:360" ... output/360p/index.m3u8 &
ffmpeg -i input.mp4 -vf "scale=1280:720" ... output/720p/index.m3u8 &
ffmpeg -i input.mp4 -vf "scale=1920:1080" ... output/1080p/index.m3u8 &
wait
echo "All encodes finished"
Para una introducción más amplia a los comandos de FFmpeg, consulta el tutorial de uso de FFmpeg.
Preguntas frecuentes
¿Cuál es la diferencia entre HLS y DASH?
Ambos son protocolos de streaming con bitrate adaptativo que dividen el video en segmentos. HLS fue desarrollado por Apple y usa playlists .m3u8 con segmentos .ts. DASH (Dynamic Adaptive Streaming over HTTP) usa manifiestos .mpd con segmentos .m4s. HLS tiene mayor soporte nativo en navegadores (especialmente iOS/Safari) y es la opción más común para distribución de video general. DASH ofrece algo más de flexibilidad en la elección de códecs, pero necesita un reproductor JavaScript en todas las plataformas.
¿Qué duración de segmento debo usar?
Para VOD (video bajo demanda), entre 4 y 6 segundos funciona bien. Segmentos más cortos (2 segundos) permiten cambios de calidad más rápidos en ABR pero aumentan el número de solicitudes HTTP. Segmentos más largos (10 segundos) reducen el overhead de red pero hacen que la navegación por el video sea menos precisa. La especificación de Apple para HLS recomienda 6 segundos como duración objetivo.
¿Puedo usar HLS para transmisión en vivo con FFmpeg?
Sí. Para streaming en vivo, usa -hls_flags delete_segments con un -hls_list_size distinto de cero (por ejemplo, 5) para que la playlist solo mantenga los segmentos más recientes. También conviene agregar -hls_allow_cache 0 para evitar que los reproductores cacheen segmentos obsoletos. Puedes conectar la entrada de una cámara o captura de pantalla por pipe para transmitir en tiempo real.
¿Necesito CloudFront o puedo usar otro CDN?
Cualquier CDN que sirva archivos estáticos funciona: Cloudflare, Fastly, Akamai, Bunny CDN, etc. Este artículo usa CloudFront como ejemplo porque se integra directamente con S3 y permite configurar políticas de caché por ruta, lo cual es práctico para aplicar headers diferentes a .m3u8 y .ts. Si usas Cloudflare R2 como origen, el CDN integrado de Cloudflare es una opción natural con cero costos de egreso.
¿Por qué los segmentos son .ts en vez de .mp4?
El formato .ts (MPEG Transport Stream) es el formato de segmento predeterminado de HLS y tiene la mayor compatibilidad con reproductores. FFmpeg también soporta segmentos fMP4 (-hls_segment_type fmp4), que producen archivos .m4s. Los segmentos fMP4 son más pequeños y funcionan mejor con códecs modernos como AV1, pero algunos reproductores antiguos no los soportan. Quédate con .ts a menos que tengas una razón específica para cambiar.
¿Cuánto almacenamiento extra requiere ABR?
Aproximadamente 2–3x más que una codificación única a 1080p. Una escalera de tres niveles (360p + 720p + 1080p) almacena la versión a máxima calidad más dos copias a menor bitrate. Las versiones 360p y 720p pesan bastante menos por segundo de video, así que el total es menos de 3x. Para un video de 10 minutos a 1080p (~3 Mbps), calcula unos 250 MB para tres niveles frente a ~140 MB en un solo bitrate.
¿Funciona hls.js en navegadores móviles?
En iOS, Safari reproduce HLS de forma nativa sin necesidad de hls.js. En Android, Chrome soporta HLS a través de hls.js usando Media Source Extensions (MSE). El componente de este artículo detecta primero si hay soporte nativo y solo recurre a hls.js cuando es necesario, así que funciona en todos los navegadores y dispositivos modernos.
Conclusión
El pipeline completo de streaming HLS con FFmpeg y un CDN se resume en cinco pasos:
- Codifica con
ffmpegusando-hls_time 6para producir segmentos y una playlist - Para ABR, codifica tres niveles de calidad (360p / 720p / 1080p) y escribe una playlist maestra a mano
- Sube a S3 —
no-cachepara.m3u8,max-age=31536000para.ts - Configura CORS tanto en S3 como en CloudFront
- Usa hls.js en el navegador (con fallback nativo en Safari)
Configurar todo esto lleva más trabajo que subir un MP4 a un servidor, pero la diferencia en producción es real: el caché del CDN se aprovecha al máximo y los usuarios con conexiones lentas ven el video sin cortes en lugar de sufrir buffering constante. Si vas a poner video en producción, arrancar con HLS y un CDN desde el principio es la decisión correcta.
Codificar múltiples niveles de ABR consume mucha CPU. Si no quieres atar tu máquina local, monta un servidor de codificación dedicado en un VPS y ejecuta los tres encodes en paralelo en una máquina remota.
VPS en la nube de grado empresarial con centros de datos globales
- 13 centros de datos (EE.UU., Europa, Asia, Oriente Medio)
- Desde $4/mes por 1GB RAM — pago por uso
- Prueba gratuita de 30 días
Artículos relacionados:
- Cómo recibir, convertir y transmitir cámaras RTSP con FFmpeg
- Construir un panel de vigilancia multicámara con FFmpeg
- La guía completa de compresión de video con FFmpeg
- AV1 vs H.265 vs H.264: ¿Qué códec deberías usar?
- Construye un servidor de codificación FFmpeg en un VPS
- Procesa video en el navegador con ffmpeg.wasm
- Comandos FFmpeg: Guía práctica de lo básico a lo avanzado