Streaming de vídeo adaptativo em Go com FFmpeg
O streaming adaptativo é essencial para entregar conteúdo de vídeo de alta qualidade com eficiência em diferentes condições de rede. Neste DevTip, vamos ver como implementar streaming de vídeo adaptativo em Go usando FFmpeg, com foco nos formatos HTTP Live Streaming (HLS) e MPEG-DASH. Antes de começar, confira se você tem um ambiente de desenvolvimento Go configurado e o FFmpeg instalado no seu sistema.
Introdução ao streaming adaptativo: HLS vs. MPEG-DASH
O streaming com bitrate adaptativo ajusta dinamicamente a qualidade do vídeo de acordo com as
condições de rede de quem assiste. Dois padrões populares são o HLS (desenvolvido pela Apple) e o
MPEG-DASH (um padrão aberto). O HLS usa arquivos de manifesto .m3u8, enquanto o MPEG-DASH utiliza arquivos .mpd. Os dois
formatos dividem os vídeos em segmentos menores, o que permite ajustes de qualidade sem interrupções.
Configurar o FFmpeg com Go: como escolher o wrapper certo
Este guia usa o pacote padrão os/exec do Go e a interface de linha de comando documentada do
FFmpeg. Nenhum wrapper para Go é necessário. Verifique se o Go e uma build do FFmpeg com o encoder
libx264 estão disponíveis:
go version
ffmpeg -version
ffmpeg -encoders
Os exemplos usam um vídeo local em formato paisagem com uma faixa de áudio. Eles normalizam o vídeo para 24 quadros por segundo e alinham os keyframes a cada quatro segundos. Adapte as dimensões e a escada de bitrates ao seu material de origem; gerar duas representações não é, por si só, uma recomendação de qualidade.
Converter vídeos para o formato HLS com Go e FFmpeg
Salve o programa completo a seguir como stream.go. O mesmo executor oferece suporte a HLS, a DASH com
um único vídeo e a DASH com duas representações de vídeo. Cada execução exige um novo diretório de
saída, para que manifestos e segmentos não entrem em conflito com uma tarefa anterior. Ele inicia o
FFmpeg sem um 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")
}
Execute go run stream.go input.mp4 hls-output hls. Isso gera uma playlist de mídia VOD e segmentos MPEG-TS com um
único bitrate de vídeo. Isso é streaming segmentado, mas ainda não é streaming com bitrate
adaptativo. Não exponha o diretório de saída aos espectadores até que o comando seja concluído com
sucesso.
Implementar a conversão para MPEG-DASH em Go
Use o mesmo programa com o modo dash:
go run stream.go input.mp4 dash-output dash
A saída contém index.mpd, segmentos de inicialização e segmentos de mídia. Mantenha os nomes de
arquivo relativos juntos ao publicar. Esse modo tem uma representação de vídeo e um adaptation set
de áudio separado.
Criar variantes de bitrate adaptativo
O modo adaptive-dash mapeia o vídeo de entrada duas vezes, redimensiona cada saída e atribui
bitrates separados. Definir apenas -b:v:1 não cria um segundo stream de vídeo; o mapeamento adicional
é essencial. As duas representações compartilham keyframes alinhados e um único stream de áudio.
go run stream.go input.mp4 adaptive-output adaptive-dash
Use uma origem com pelo menos 360 pixels de altura para esta escada ilustrativa de 180p/360p e inspecione texto, movimento e proporção de tela nas duas qualidades. Escadas de bitrates de produção devem refletir o seu conteúdo e os dispositivos de destino. O HLS com múltiplas variantes também precisa de várias playlists de mídia e de uma playlist mestre; o modo HLS de variante única acima não as cria.
Criar um servidor de streaming com tratamento de segmentos
Para uma verificação local de reprodução, coloque apenas saídas de teste públicas e concluídas
em um diretório videos. Salve este programa separado como serve.go. Ele se vincula à interface
de loopback e define os tipos MIME relevantes. Este servidor de desenvolvimento habilita a listagem
de diretórios, então não coloque arquivos privados ali.
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())
}
Otimizar o desempenho com processamento concorrente
Comece com um único worker de conversão e meça o uso de CPU, memória e disco. Aumente a concorrência com um pool de workers limitado, e não com uma goroutine para cada upload na fila. Dê a cada tarefa o seu próprio diretório de saída e informe cada falha à fila, para que ela possa ser repetida de forma deliberada. O timeout do programa limita um processo do FFmpeg; ele não substitui os limites do sistema operacional nem um sandbox.
Para mídia não confiável, use workers isolados, sem credenciais nem acesso irrestrito à rede. Publique as saídas concluídas por meio de um servidor web ou CDN configurado corretamente, com HTTPS, regras de cache, controles de acesso quando necessário e CORS apenas para as origens de player que você pretende suportar. O servidor de desenvolvimento em loopback não é um serviço de streaming de produção.
Testar e depurar a sua implementação de streaming
Use ferramentas como o VLC media player ou players HLS/DASH baseados no navegador (por exemplo,
Shaka Player e Video.js) para testar os seus streams. Verifique se os seus arquivos de manifesto
(.m3u8 ou .mpd) referenciam corretamente os segmentos de vídeo e se a reprodução se adapta a
mudanças simuladas nas condições de rede, caso o seu player ofereça esse recurso. Confira os logs do
FFmpeg em busca de erros durante a transcodificação.
Verifique também se cada segmento de inicialização e de mídia referenciado existe, tem o tipo MIME
esperado e retorna uma resposta bem-sucedida. No caso de adaptive-dash, inspecione o MPD para confirmar que há
duas representações de vídeo, em vez de deduzir a adaptação a partir de um nome de arquivo. Consulte
as opções dos muxers DASH e HLS do FFmpeg para ver detalhes sobre o empacotamento.
Conclusão
Implementar streaming adaptativo em Go com FFmpeg é simples e poderoso. Seguindo estes passos, você consegue entregar com eficiência conteúdo de vídeo de alta qualidade, adaptado às condições de rede dos seus usuários.
Se você procura uma solução gerenciada, o Robot 🤖 /video/adaptive da Transloadit simplifica o streaming adaptativo ao cuidar das conversões para HLS e MPEG-DASH sem complicações. Confira o nosso go-sdk para uma integração fácil.
