Adaptives Videostreaming in Go mit FFmpeg
Führen Sie FFmpeg mit Go aus, paketieren Sie ein lokales Video als HLS oder MPEG-DASH und spielen Sie die fertigen Dateien im Browser ab. Das Beispiel erstellt auch einen DASH-Stream mit zwei Varianten. Diese Anleitung behandelt die lokale Paketierung und Wiedergabe, keinen produktiven Auslieferungsdienst und keine Demonstration der Anpassung an wechselnde Netzwerkbedingungen.
Einführung in adaptives Streaming: HLS vs. MPEG-DASH
Adaptives Bitratenstreaming passt die Videoqualität dynamisch an die Netzwerkbedingungen der
Zuschauenden an. Zwei beliebte Standards sind HLS (von Apple
entwickelt) und MPEG-DASH (ein offener Standard). HLS verwendet Manifestdateien im Format
.m3u8, MPEG-DASH dagegen Dateien im Format .mpd.
Beide Formate teilen Videos in kleinere Segmente auf. Ein Player benötigt mehrere kompatible
Varianten, um die Qualität wechseln zu können. Ein einzelnes Encoding in Segmente aufzuteilen,
macht es noch nicht adaptiv.
FFmpeg mit Go einrichten: den passenden Wrapper wählen
Diese Anleitung verwendet das Standardpaket os/exec
von Go und die dokumentierte Kommandozeilenschnittstelle von FFmpeg.
Ein Go-Wrapper ist nicht erforderlich. Verwenden Sie Bash für die Shell-Befehle; Go, FFmpeg, ffprobe
und cURL müssen über PATH erreichbar sein. Die Konvertierung wurde mit
Go 1.26.8 und FFmpeg/ffprobe 9.0.1 unter Linux getestet. Ihr FFmpeg-Build benötigt die Encoder
libx264 und AAC sowie die HLS- und DASH-Muxer:
go version && ffmpeg -version && ffprobe -version && ffmpeg -encoders && ffmpeg -muxers
Arbeiten Sie in einem Verzeichnis mit einem gültigen lokalen Video im Querformat namens
input.mp4, das eine Audiospur und eine Höhe von mindestens 360 Pixeln hat.
Speichern Sie die beiden Go-Programme unter den unten angegebenen Dateinamen und führen Sie sie
einzeln aus; go run . würde ihre beiden Funktionen
main zusammenführen. Der Konverter vereinheitlicht das Video auf 24 Bilder
pro Sekunde und richtet die Keyframes alle vier Sekunden aus. Passen Sie die Abmessungen und die
Bitratenleiter an Ihr Ausgangsmaterial an. Zwei Varianten zu erzeugen ist für sich genommen noch
keine Qualitätsempfehlung.
Videos mit Go und FFmpeg ins HLS-Format konvertieren
Speichern Sie das folgende vollständige Programm als stream.go. Dasselbe
Ausführungsprogramm unterstützt HLS, DASH mit einer Videovariante und DASH mit zwei Videovarianten.
Jeder Aufruf erfordert ein neues Ausgabeverzeichnis, damit Manifeste und Segmente nicht mit denen
eines früheren Jobs kollidieren. 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 Folgendes aus:
go run stream.go input.mp4 hls-output hls
Dadurch entstehen eine VOD-Medienplaylist und MPEG-TS-Segmente mit einer Videobitrate. Das ist segmentiertes Streaming, noch kein adaptives Bitratenstreaming. Machen Sie das Ausgabeverzeichnis erst für Zuschauende zugänglich, wenn der Befehl erfolgreich abgeschlossen wurde.
MPEG-DASH-Konvertierung in Go implementieren
Verwenden Sie dasselbe Programm im Modus dash:
go run stream.go input.mp4 dash-output dash
Die Ausgabe enthält index.mpd, Initialisierungssegmente und Mediensegmente.
Behalten Sie bei der Veröffentlichung ihre relativen Dateinamen und ihre Zuordnung bei. Dieser
Modus hat eine Videovariante und ein separates Audio-Adaptation-Set.
Adaptive Bitratenvarianten erstellen
Der Modus adaptive-dash ordnet das Eingabevideo zweimal zu, skaliert jede Ausgabe
und weist separate Bitraten zu. Allein die Einstellung -b:v:1 erzeugt keinen
zweiten Videostream; die zusätzliche Zuordnung ist unverzichtbar. Beide Varianten nutzen
aufeinander ausgerichtete Keyframes und einen gemeinsamen 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 berücksichtigen. HLS mit mehreren Varianten benötigt zusätzlich mehrere Medienplaylists und eine Master-Playlist; der obige HLS-Modus mit einer Variante erstellt diese nicht.
Einen Streamingserver mit Segmentverarbeitung erstellen
Legen Sie für einen lokalen Wiedergabetest nur fertiggestellte, öffentliche Testausgaben in
einem Verzeichnis namens videos ab. Verschieben Sie nach dem erfolgreichen
Abschluss aller drei Konvertierungen deren Ausgabeverzeichnisse gemeinsam:
test -f hls-output/index.m3u8 &&
test -f dash-output/index.mpd &&
test -f adaptive-output/index.mpd &&
mkdir videos &&
mv hls-output dash-output adaptive-output videos/
Wenn das Verzeichnis videos bereits existiert, stoppt dieser Befehl, ohne
seinen Inhalt zu ersetzen. Verwenden Sie für einen weiteren vollständigen Durchlauf ein neues
Projektverzeichnis, statt vorhandene Daten zu löschen.
Speichern Sie dieses separate Programm als serve.go. Es bindet sich an die
Loopback-Schnittstelle und setzt die relevanten MIME-Typen. Die Funktion
http.FileServer von Go ermöglicht hier
Verzeichnisauflistungen. Legen Sie daher keine privaten Dateien im öffentlichen Verzeichnis ab.
package main
import (
"flag"
"fmt"
"log"
"mime"
"net"
"net/http"
"time"
)
func main() {
port := flag.Int("port", 8080, "Loopback port for local playback")
flag.Parse()
for extension, contentType := range map[string]string{
".m3u8": "application/vnd.apple.mpegurl", ".mpd": "application/dash+xml",
".ts": "video/mp2t", ".m4s": "video/iso.segment", ".mp4": "video/mp4",
} {
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"))))
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", *port))
if err != nil {
log.Fatal(err)
}
server := &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second}
log.Printf("Serving local test media at http://%s/videos/", listener.Addr())
log.Fatal(server.Serve(listener))
}
Laden Sie einen Build von Shaka Player mit festgelegter Version in dasselbe öffentliche Verzeichnis herunter. Dieser Befehl benötigt Internetzugang und ersetzt eine vorhandene Kopie der Bibliothek unter diesem Dateinamen. Fahren Sie erst fort, wenn der Download erfolgreich abgeschlossen wurde:
curl -fsSLo videos/shaka-player.compiled.js https://cdn.jsdelivr.net/npm/shaka-player@5.2.12/dist/shaka-player.compiled.js
Speichern Sie diese Seite als videos/player.html. Sie folgt der
Grundeinrichtung von Shaka und lädt die Manifeste relativ zur Seite,
sodass Medien und Player denselben Ursprung haben:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Local HLS and DASH playback</title>
<style>
html { color-scheme: light dark; }
video { max-width: 100%; height: auto; }
</style>
<script src="shaka-player.compiled.js"></script>
</head>
<body>
<form id="streams">
<label>Stream
<select id="manifest" disabled>
<option value="hls-output/index.m3u8">HLS</option>
<option value="dash-output/index.mpd">DASH</option>
<option value="adaptive-output/index.mpd">Adaptive DASH</option>
</select>
</label>
<button disabled>Load stream</button>
</form>
<p id="status" role="status">Initializing player…</p>
<video id="video" aria-label="Video preview" controls width="640"></video>
<script>
async function initialize() {
const status = document.getElementById('status')
const form = document.getElementById('streams')
const manifest = document.getElementById('manifest')
const button = form.querySelector('button')
shaka.polyfill.installAll()
if (!shaka.Player.isBrowserSupported()) {
status.textContent = 'This browser cannot play these streams.'
return
}
const player = new shaka.Player()
await player.attach(document.getElementById('video'))
window.player = player
player.addEventListener('error', () => {
status.textContent = 'Playback failed. Check the manifest and segments.'
})
form.addEventListener('submit', async (event) => {
event.preventDefault()
button.disabled = manifest.disabled = true
status.textContent = 'Loading stream…'
try {
await player.load(manifest.value)
status.textContent = 'Ready. Press play.'
} catch {
status.textContent = 'Playback failed. Check the manifest and segments.'
} finally {
button.disabled = manifest.disabled = false
}
})
button.disabled = manifest.disabled = false
status.textContent = 'Choose a stream.'
}
initialize().catch(() => {
document.getElementById('status').textContent = 'Player initialization failed.'
})
</script>
</body>
</html>
Starten Sie den Server im Vordergrund aus dem Verzeichnis, das serve.go und
videos enthält:
go run serve.go -port 8080
Öffnen Sie http://127.0.0.1:8080/videos/player.html in einem Browser, der H.264/AAC und Media Source Extensions
unterstützt. Wählen Sie einen Stream, klicken Sie auf Load stream,
warten Sie auf Ready. Press play. und starten Sie die
Wiedergabe über die Videosteuerung. Probieren Sie alle drei Einträge aus und springen Sie jeweils
kurz vor das Ende. Stoppen Sie den Server mit Ctrl+C. Falls Port 8080 belegt ist, wählen Sie einen
anderen Wert für -port und verwenden Sie diesen Port in der Browser-URL.
Schlägt die Bindung fehl, beendet sich das Programm mit einem Fehler, statt eine Server-URL
anzuzeigen. Die Wiedergabe wurde mit Shaka Player 5.2.12 und Chromium 145 geprüft.
Leistung durch parallele Verarbeitung optimieren
Beginnen Sie mit einem Konvertierungsworker und messen Sie die CPU-, Arbeitsspeicher- und Datenträgernutzung. Erhöhen Sie die Parallelität mit einem begrenzten Worker-Pool, nicht mit einer eigenen Goroutine für jeden Upload in der Queue. Geben Sie jedem Job ein eigenes Ausgabeverzeichnis und melden Sie jeden Fehler an die Queue, damit ein erneuter Versuch gezielt erfolgen kann. Das Zeitlimit des Programms begrenzt einen FFmpeg-Prozess; es ersetzt weder Betriebssystemlimits noch eine Sandbox.
Verwenden Sie für nicht vertrauenswürdige Medien isolierte Worker ohne Zugangsdaten oder uneingeschränkten Netzwerkzugriff. Veröffentlichen Sie fertige Ausgaben über einen korrekt konfigurierten Webserver oder ein CDN mit HTTPS, Cache-Regeln, Zugriffskontrollen bei Bedarf und CORS nur für die Player-Ursprünge, die Sie unterstützen möchten. Der Loopback-Entwicklungsserver ist kein Streamingdienst für den Produktivbetrieb.
Ihre Streamingimplementierung testen und Fehler beheben
Der obige Player prüft die lokale Wiedergabe. Er beweist nicht, dass eine Bitratenleiter gut aussieht oder der automatische Wechsel unter realen Netzwerkbedingungen gut funktioniert. Prüfen Sie das decodierte Video und Audio einschließlich des Aufnahmeendes sowie die Diagnosemeldungen von FFmpeg vor der Veröffentlichung.
Eine fehlende Eingabe, eine fehlende Audiospur oder ein fehlgeschlagener FFmpeg-Prozess liefert einen Status ungleich null zurück. Das Ausführungsprogramm entfernt das neue Ausgabeverzeichnis nach einer fehlgeschlagenen Konvertierung und verweigert das Überschreiben eines bestehenden Ziels. FFmpeg kann sich von manchen beschädigten Eingaben erholen und trotzdem Erfolg melden; „Conversion complete“ garantiert nicht, dass die Eingabe intakt war. Verwenden Sie nachweislich gültige Quelldateien und vergleichen Sie Inhalt und Dauer der Ausgabe mit der Quelle.
Untersuchen Sie vom Projektverzeichnis aus die adaptiven DASH-Streams und decodieren Sie alle Streams in jeder fertigen Ausgabe. Absolute Manifestpfade vermeiden Mehrdeutigkeiten, wenn der DASH-Demuxer die Segmentdateinamen auflöst:
(
decodeStream() {
local diagnostics
if diagnostics=$(ffmpeg -v error -xerror -i "$1" -map 0 -f null - 2>&1); then
if [ -z "$diagnostics" ]; then return 0; fi
fi
printf 'Decode check failed for %s\n%s\n' "$1" "$diagnostics" >&2
return 1
}
ffprobe -v error -count_frames -show_entries stream=codec_name,codec_type,width,height,nb_read_frames -of json "$PWD/videos/adaptive-output/index.mpd" &&
decodeStream "$PWD/videos/hls-output/index.m3u8" &&
decodeStream "$PWD/videos/dash-output/index.mpd" &&
decodeStream "$PWD/videos/adaptive-output/index.mpd"
)
Das JSON sollte H.264-Video mit Höhen von 180 und 360 Pixeln sowie AAC-Audio auflisten. Die Breite
hängt vom Seitenverhältnis Ihrer Quelle ab. Vergleichen Sie nb_read_frames für beide
Videovarianten: Dieselbe Eingabe und die Vereinheitlichung der Bildrate sollten dieselbe Anzahl
ergeben. Ein beschädigtes letztes Fragment kann eine Variante unbemerkt verkürzen, selbst wenn
FFmpeg ohne Fehlerdiagnose null zurückgibt. Eine abweichende Anzahl muss untersucht werden; die
Ausgabe darf dann nicht veröffentlicht werden. Vergleichen Sie auch das letzte Bild und den
Ton am Ende mit der Quelle; gleiche Anzahlen allein belegen keine intakten Inhalte. Die
Decoderprüfungen bleiben bei Erfolg ohne Ausgabe und stoppen die Befehlskette bei einem
fehlgeschlagenen Prozess oder einer Fehlerdiagnose, selbst wenn FFmpeg sich erholt und null
zurückgibt. Dennoch belegen sie weder, dass die ursprüngliche Aufnahme intakt war, noch, dass
jedes erwartete Bild erhalten blieb. AAC-Padding kann dazu führen, dass das Audio etwas länger
ist als das Video. Vergleichen Sie daher die decodierten Inhalte und Zeitstempel, statt sich nur
auf eine gerundete Dauer im Manifest zu verlassen.
Prüfen Sie außerdem, ob jedes referenzierte Initialisierungs- und Mediensegment existiert, den
erwarteten MIME-Typ hat und eine erfolgreiche Antwort liefert. Untersuchen Sie bei
adaptive-dash das MPD auf zwei Videovarianten, statt die Anpassungsfähigkeit aus
einem Dateinamen abzuleiten. Details zur Paketierung finden Sie in den
Optionen der DASH- und HLS-Muxer von FFmpeg.
Fazit
Sie haben jetzt einen lokalen HLS-Stream, einen DASH-Stream mit einer Videovariante und einen DASH-Stream mit zwei Videovarianten. Behalten Sie jedes Manifest mit seinen Segmenten zusammen, wenn Sie von diesem Loopback-Test zu Ihrer Veröffentlichungsumgebung wechseln.
Wenn Sie eine verwaltete Lösung suchen, übernimmt der Robot 🤖 /video/adaptive von Transloadit die Paketierung für HLS und MPEG-DASH. Zur Integration mit Go nutzen Sie unser go-sdk.
