Streaming de video adaptativo en Go con FFmpeg
El streaming adaptativo es esencial para entregar contenido de video de alta calidad de forma eficiente en distintas condiciones de red. En este DevTip, veremos cómo implementar streaming de video adaptativo en Go con FFmpeg, centrándonos en los formatos HTTP Live Streaming (HLS) y MPEG-DASH. Antes de empezar, asegúrate de tener un entorno de desarrollo de Go configurado y FFmpeg instalado en tu sistema.
Introducción al streaming adaptativo: HLS frente a MPEG-DASH
El streaming con bitrate adaptativo ajusta dinámicamente la calidad del video según las condiciones
de red del espectador. Dos estándares populares son HLS (desarrollado por Apple) y MPEG-DASH (un
estándar abierto). HLS usa archivos de manifiesto .m3u8, mientras que MPEG-DASH utiliza archivos .mpd. Ambos
formatos dividen los videos en fragmentos más pequeños, lo que permite ajustes de calidad sin
interrupciones.
Configurar FFmpeg con Go: elegir el wrapper adecuado
Esta guía usa el paquete estándar os/exec de Go y la interfaz de línea de comandos documentada de
FFmpeg. No hace falta ningún wrapper de Go. Comprueba que Go y una compilación de FFmpeg con el
codificador libx264 estén disponibles:
go version
ffmpeg -version
ffmpeg -encoders
Los ejemplos usan un video local en formato horizontal con una pista de audio. Normalizan el video a 24 fotogramas por segundo y alinean los fotogramas clave cada cuatro segundos. Adapta las dimensiones y la escalera de bitrate a tu material de origen; generar dos representaciones no es por sí solo una recomendación de calidad.
Convertir videos al formato HLS con Go y FFmpeg
Guarda el siguiente programa completo como stream.go. El mismo ejecutor admite HLS, DASH con un solo
video y DASH con dos representaciones de video. Cada invocación requiere un directorio de salida
nuevo para que los manifiestos y los segmentos no puedan colisionar con los de un trabajo anterior.
Lanza FFmpeg sin usar un shell.
package main
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
)
func convert(inputPath, outputPath, format string) (err error) {
if format != "hls" && format != "dash" && format != "adaptive-dash" {
return fmt.Errorf("format must be hls, dash, or adaptive-dash")
}
input, err := filepath.Abs(inputPath)
if err != nil {
return err
}
info, err := os.Stat(input)
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return fmt.Errorf("input must be a regular local file")
}
output, err := filepath.Abs(outputPath)
if err != nil {
return err
}
if err = os.Mkdir(output, 0700); err != nil {
return fmt.Errorf("use a new output directory: %w", err)
}
defer func() {
if err != nil {
if cleanupErr := os.RemoveAll(output); cleanupErr != nil {
fmt.Fprintln(os.Stderr, "Cannot remove partial output:", cleanupErr)
}
}
}()
args := []string{"-nostdin", "-n", "-i", input, "-map", "0:v:0"}
if format == "adaptive-dash" {
args = append(args, "-map", "0:v:0")
}
args = append(args, "-map", "0:a:0", "-c:v", "libx264", "-preset", "fast",
"-pix_fmt", "yuv420p", "-r", "24", "-g", "96", "-keyint_min", "96",
"-sc_threshold", "0", "-force_key_frames", "expr:gte(t,n_forced*4)",
"-c:a", "aac", "-b:a", "96k", "-ac", "2", "-threads", "2", "-filter_threads", "1")
if format == "adaptive-dash" {
args = append(args, "-filter:v:0", "scale=-2:180", "-b:v:0", "400k",
"-filter:v:1", "scale=-2:360", "-b:v:1", "800k")
} else {
args = append(args, "-b:v", "800k")
}
if format == "hls" {
args = append(args, "-f", "hls", "-hls_time", "4", "-hls_playlist_type", "vod",
"-hls_segment_filename", "segment-%04d.ts", "index.m3u8")
} else {
args = append(args, "-f", "dash", "-seg_duration", "4", "-use_template", "1",
"-use_timeline", "1", "-adaptation_sets", "id=0,streams=v id=1,streams=a", "index.mpd")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()
cmd := exec.CommandContext(ctx, "ffmpeg", args...)
cmd.Dir = output
cmd.Stderr = os.Stderr
if err = cmd.Run(); err != nil {
return fmt.Errorf("conversion failed: %w", err)
}
return nil
}
func main() {
if len(os.Args) != 4 {
fmt.Fprintln(os.Stderr, "Usage: go run stream.go <input.mp4> <new-output-directory> <hls|dash|adaptive-dash>")
os.Exit(1)
}
if err := convert(os.Args[1], os.Args[2], os.Args[3]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println("Conversion complete")
}
Ejecuta go run stream.go input.mp4 hls-output hls. Esto produce una lista de reproducción multimedia de VOD y segmentos
MPEG-TS con un solo bitrate de video. Eso es streaming segmentado, todavía no streaming con bitrate
adaptativo. No expongas el directorio de salida a los espectadores hasta que el comando termine
correctamente.
Implementar la conversión a MPEG-DASH en Go
Usa el mismo programa con el modo dash:
go run stream.go input.mp4 dash-output dash
La salida contiene index.mpd, segmentos de inicialización y segmentos multimedia. Mantén juntos sus
nombres de archivo relativos al publicarlos. Este modo tiene una sola representación de video y un
conjunto de adaptación de audio independiente.
Crear variantes de bitrate adaptativo
El modo adaptive-dash mapea dos veces el video de entrada, escala cada salida y asigna bitrates
separados. Establecer -b:v:1 por sí solo no crea un segundo flujo de video; el mapeo adicional es
esencial. Ambas representaciones comparten fotogramas clave alineados y un solo flujo de audio.
go run stream.go input.mp4 adaptive-output adaptive-dash
Usa una fuente de al menos 360 píxeles de alto para esta escalera ilustrativa de 180p/360p, e inspecciona el texto, el movimiento y la relación de aspecto en ambas calidades. Las escaleras de bitrate de producción deberían reflejar tu contenido y los dispositivos de destino. El HLS multivariante necesita además varias listas de reproducción multimedia y una lista de reproducción maestra; el modo HLS de una sola variante que aparece arriba no las crea.
Crear un servidor de streaming con manejo de segmentos
Para una comprobación local de reproducción, coloca solo salidas de prueba públicas y completas
en un directorio videos. Guarda este programa aparte como serve.go. Se enlaza a loopback y establece
los tipos MIME correspondientes. Este servidor de desarrollo tiene habilitado el listado de
directorios, así que no pongas archivos privados ahí.
package main
import (
"log"
"mime"
"net/http"
"time"
)
func main() {
for extension, contentType := range map[string]string{
".m3u8": "application/vnd.apple.mpegurl", ".mpd": "application/dash+xml",
".ts": "video/mp2t", ".m4s": "video/iso.segment",
} {
if err := mime.AddExtensionType(extension, contentType); err != nil {
log.Fatal(err)
}
}
mux := http.NewServeMux()
mux.Handle("/videos/", http.StripPrefix("/videos/", http.FileServer(http.Dir("./videos"))))
server := &http.Server{Addr: "127.0.0.1:8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second}
log.Println("Serving local test media at http://127.0.0.1:8080/videos/")
log.Fatal(server.ListenAndServe())
}
Optimizar el rendimiento con procesamiento concurrente
Empieza con un solo worker de conversión y mide el uso de CPU, memoria y disco. Aumenta la concurrencia con un pool de workers acotado, no con una goroutine por cada subida en cola. Da a cada trabajo su propio directorio de salida e informa cada fallo a la cola para que se pueda reintentar de forma deliberada. El tiempo de espera del programa acota un proceso de FFmpeg; no sustituye a los límites del sistema operativo ni a un sandbox.
Para medios no confiables, usa workers aislados, sin credenciales ni acceso de red sin restricciones. Publica las salidas terminadas mediante un servidor web o una CDN correctamente configurados, con HTTPS, reglas de caché, controles de acceso donde haga falta y CORS solo para los orígenes de reproductor que quieras admitir. El servidor de desarrollo en loopback no es un servicio de streaming de producción.
Probar y depurar tu implementación de streaming
Usa herramientas como el reproductor VLC media player o reproductores HLS/DASH basados en navegador
(por ejemplo, Shaka Player, Video.js) para probar tus flujos. Verifica que tus archivos de
manifiesto (.m3u8 o .mpd) referencien correctamente los segmentos de video y que la
reproducción se adapte a cambios simulados en las condiciones de red si tu reproductor lo admite.
Revisa los registros de FFmpeg en busca de errores durante la transcodificación.
Comprueba también que cada segmento de inicialización y cada segmento multimedia referenciado
exista, tenga el tipo MIME esperado y devuelva una respuesta correcta. Para adaptive-dash, inspecciona
el MPD en busca de dos representaciones de video en lugar de inferir la adaptación a partir de un
nombre de archivo. Consulta las
opciones de los muxers DASH y HLS de FFmpeg para conocer los detalles de empaquetado.
Conclusión
Implementar streaming adaptativo en Go con FFmpeg es sencillo y potente. Si sigues estos pasos, podrás entregar de forma eficiente contenido de video de alta calidad adaptado a las condiciones de red de tus usuarios.
Si buscas una solución gestionada, el Robot 🤖 /video/adaptive de Transloadit simplifica el streaming adaptativo al gestionar las conversiones a HLS y MPEG-DASH sin complicaciones. Echa un vistazo a nuestro go-sdk para una integración sencilla.
