Adaptives Video-Streaming in Go mit FFmpeg
Adaptives Streaming ist unverzichtbar, um hochwertige Videoinhalte unter unterschiedlichsten Netzwerkbedingungen effizient auszuliefern. In diesem DevTip zeigen wir, wie Sie adaptives Video-Streaming in Go mit FFmpeg umsetzen, mit besonderem Fokus auf die Formate HTTP Live Streaming (HLS) und MPEG-DASH. Bevor wir einsteigen, stellen Sie sicher, dass Sie eine Go-Entwicklungsumgebung eingerichtet und FFmpeg auf Ihrem System installiert haben.
Einführung in adaptives Streaming: HLS vs. MPEG-DASH
Adaptives Bitraten-Streaming passt die Videoqualität dynamisch an die Netzwerkbedingungen der
Zuschauer an. Zwei verbreitete Standards sind HLS (von Apple entwickelt) und MPEG-DASH (ein offener
Standard). HLS verwendet Manifest-Dateien im Format .m3u8, während MPEG-DASH auf Dateien vom Typ
.mpd setzt. Beide Formate zerlegen Videos in kleinere Chunks und ermöglichen so nahtlose
Qualitätsanpassungen.
FFmpeg mit Go einrichten: den richtigen Wrapper wählen
Dieser Leitfaden nutzt das Standardpaket os/exec von Go und die dokumentierte
Kommandozeilen-Schnittstelle von FFmpeg. Ein Go-Wrapper ist nicht nötig. Prüfen Sie, ob Go und ein
FFmpeg-Build mit dem Encoder libx264 verfügbar sind:
go version
ffmpeg -version
ffmpeg -encoders
Die Beispiele verwenden ein lokales Querformat-Video mit Tonspur. Sie normalisieren das Video auf 24 Bilder pro Sekunde und richten die Keyframes alle vier Sekunden aus. Passen Sie die Abmessungen und die Bitratenleiter an Ihr Ausgangsmaterial an; zwei Repräsentationen zu erzeugen ist für sich genommen keine Qualitätsempfehlung.
Videos mit Go und FFmpeg ins HLS-Format konvertieren
Speichern Sie das folgende vollständige Programm als stream.go. Derselbe Runner unterstützt HLS,
DASH mit einem Video sowie DASH mit zwei Videorepräsentationen. Jeder Aufruf benötigt ein neues
Ausgabeverzeichnis, damit Manifeste und Segmente nicht mit einem früheren Job kollidieren können.
Das Programm startet FFmpeg ohne 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")
}
Führen Sie go run stream.go input.mp4 hls-output hls aus. Das erzeugt eine VOD-Media-Playlist und
MPEG-TS-Segmente mit einer einzigen Videobitrate. Das ist segmentiertes Streaming, noch kein
adaptives Bitraten-Streaming. Geben Sie das Ausgabeverzeichnis erst dann für Zuschauer frei, wenn
der Befehl erfolgreich durchgelaufen ist.
MPEG-DASH-Konvertierung in Go umsetzen
Verwenden Sie dasselbe Programm mit dem Modus dash:
go run stream.go input.mp4 dash-output dash
Die Ausgabe enthält die Datei index.mpd, Initialisierungssegmente und Mediensegmente. Halten Sie
ihre relativen Dateinamen beim Veröffentlichen zusammen. Dieser Modus hat eine Videorepräsentation
und ein separates Audio-Adaptation-Set.
Adaptive Bitraten-Varianten erstellen
Der Modus adaptive-dash bindet das Eingabevideo zweimal ein, skaliert jede Ausgabe und weist
separate Bitraten zu. Die Option -b:v:1 allein zu setzen, erzeugt keinen zweiten Videostream; das
zusätzliche Mapping ist entscheidend. Beide Repräsentationen teilen sich ausgerichtete Keyframes und
einen Audiostream.
go run stream.go input.mp4 adaptive-output adaptive-dash
Verwenden Sie für diese beispielhafte 180p/360p-Leiter eine Quelle mit mindestens 360 Pixeln Höhe und prüfen Sie Text, Bewegung und Seitenverhältnis in beiden Qualitätsstufen. Bitratenleitern für den Produktivbetrieb sollten Ihre Inhalte und Zielgeräte widerspiegeln. Multi-Variant-HLS benötigt zusätzlich mehrere Media-Playlists und eine Master-Playlist; der oben gezeigte HLS-Modus mit nur einer Variante erzeugt diese nicht.
Einen Streaming-Server mit Segmentverarbeitung aufbauen
Legen Sie für eine lokale Wiedergabeprüfung nur fertige, öffentliche Testausgaben in das
Verzeichnis videos ab. Speichern Sie dieses separate Programm als serve.go. Es bindet an
Loopback und setzt die passenden MIME-Typen. Dieser Entwicklungsserver aktiviert
Verzeichnisauflistungen, legen Sie dort also keine privaten Dateien ab.
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())
}
Performance durch nebenläufige Verarbeitung optimieren
Beginnen Sie mit einem einzelnen Konvertierungs-Worker und messen Sie CPU-, Speicher- und Festplattennutzung. Erhöhen Sie die Nebenläufigkeit mit einem begrenzten Worker-Pool, nicht mit einer Goroutine pro eingereihtem Upload. Geben Sie jedem Job ein eigenes Ausgabeverzeichnis und melden Sie jeden Fehlschlag an die Queue, damit er gezielt wiederholt werden kann. Das Timeout des Programms begrenzt einen FFmpeg-Prozess; es ersetzt keine Betriebssystem-Limits und keine Sandbox.
Verwenden Sie für nicht vertrauenswürdige Medien isolierte Worker ohne Zugangsdaten und ohne uneingeschränkten Netzwerkzugriff. Veröffentlichen Sie fertige Ausgaben über einen korrekt konfigurierten Webserver oder ein CDN mit HTTPS, Cache-Regeln, Zugriffskontrollen, wo nötig, und CORS nur für die Player-Ursprünge, die Sie unterstützen möchten. Der Loopback-Entwicklungsserver ist kein Streaming-Dienst für den Produktivbetrieb.
Ihre Streaming-Implementierung testen und debuggen
Testen Sie Ihre Streams mit Werkzeugen wie dem VLC media player oder browserbasierten
HLS/DASH-Playern (z. B. Shaka Player, Video.js). Prüfen Sie, ob Ihre Manifest-Dateien
(.m3u8 oder .mpd) die Videosegmente korrekt referenzieren und ob sich die Wiedergabe an
simulierte Änderungen der Netzwerkbedingungen anpasst, sofern Ihr Player das unterstützt. Prüfen Sie
die FFmpeg-Logs auf Fehler beim Transkodieren.
Prüfen Sie außerdem, ob jedes referenzierte Initialisierungs- und Mediensegment existiert, den
erwarteten MIME-Typ hat und eine erfolgreiche Antwort liefert. Sehen Sie beim Modus adaptive-dash im
MPD nach zwei Videorepräsentationen, statt die Adaption aus einem Dateinamen abzuleiten. Details zum
Packaging finden Sie in den FFmpeg-Optionen für DASH- und HLS-Muxer.
Fazit
Adaptives Streaming in Go mit FFmpeg umzusetzen ist unkompliziert und leistungsfähig. Wenn Sie diese Schritte befolgen, liefern Sie hochwertige Videoinhalte effizient und zugeschnitten auf die Netzwerkbedingungen Ihrer Nutzer aus.
Wenn Sie eine verwaltete Lösung suchen: Der Robot 🤖 /video/adaptive von Transloadit vereinfacht adaptives Streaming, indem er HLS- und MPEG-DASH-Konvertierungen nahtlos übernimmt. Für eine einfache Integration schauen Sie sich unser go-sdk an.
