Videos mit Rust in HLS und MPEG-DASH konvertieren
Führen Sie FFmpeg mit Rust aus, konvertieren Sie ein lokales Video in HLS und MPEG-DASH und liefern Sie die fertigen Dateien über HTTP aus. Diese Anleitung erstellt eine HLS-Variante und zwei DASH-Videorepräsentationen und spielt anschließend jedes Format mit ffplay ab. Der Rust-Code verwaltet Prozesse und Dateien; FFmpeg übernimmt das Encoding. Sie benötigen also weder FFmpeg-Entwicklungsheader noch Rust-Bindings.
Die Ausgabe verstehen
HLS verwendet Playlists im Format .m3u8, DASH ein XML-Manifest im Format
.mpd. Beide beschreiben Mediensegmente, die ein Player über HTTP abruft.
Für die Wiedergabe mit adaptiver Bitrate sind zusätzlich mehrere kompatible Repräsentationen und
ein Player nötig, der bei veränderten Bedingungen zwischen ihnen wählt.
Das HLS-Beispiel verpackt ein einzelnes Encoding. Das DASH-Beispiel bietet Video in 1920 × 1080 und 1280 × 720 mit einer gemeinsamen Audiospur. Das Erstellen dieser Dateien und ihre lokale Wiedergabe demonstrieren Paketierung und Wiedergabe, nicht die Anpassung an die Bandbreite, eine geringere Auslieferungslatenz oder einen produktionsreifen Streaming-Dienst.
Ein Rust-Projekt einrichten
Verwenden Sie Bash unter Linux, eine stabile Rust-Toolchain von rustup
und eine FFmpeg-Distribution mit ffmpeg,
ffprobe und ffplay. Bei einigen Distributionen ist
ffplay als separates Player-Paket erhältlich; prüfen Sie es, bevor Sie das Projekt erstellen.
Für die Wiedergabe sind eine Desktop-Sitzung und eine Audioausgabe erforderlich. Die Anleitung
wurde mit Rust und Cargo 1.98.1 sowie FFmpeg, ffprobe und ffplay 9.0.1 getestet. Das sind getestete
Versionen, keine zwingenden Mindestversionen. Nutzen Sie die folgenden Funktionsprüfungen, wenn
Sie eine andere gepflegte Version evaluieren.
Unter Ubuntu oder Debian können Sie das Distributionspaket installieren:
sudo apt-get update && sudo apt-get install -y ffmpeg
Prüfen Sie Versionen und Funktionen. Ihr Build benötigt die Encoder libx264
und AAC, die HLS- und DASH-Muxer und -Demuxer sowie die Filter testsrc2,
geq und aevalsrc, die der Beispielgenerator verwendet.
cargo --version && rustc --version &&
ffmpeg -version && ffprobe -version && ffplay -version &&
ffmpeg -encoders && ffmpeg -muxers && ffmpeg -demuxers && ffmpeg -filters
Erstellen Sie das Projekt außerhalb eines bestehenden Cargo-Projekts und ohne eine Konfiguration
.cargo in einem übergeordneten Verzeichnis.
Cargo sucht Konfigurationen in übergeordneten Verzeichnissen,
und beim Erstellen eines Projekts kann ein neues Paket einem umgebenden Workspace zugeordnet
werden. Der folgende Code zum Einfügen prüft diese Abgrenzung, bevor er Dateien erstellt, und
lässt das Verzeichnis und die Optionen Ihrer Shell unverändert:
(
set -eu
for tool in cargo rustc ffmpeg ffprobe ffplay; do
command -v "$tool" >/dev/null
done
directory=$(pwd -P)
while :; do
for config in "$directory/Cargo.toml" "$directory/.cargo/config" "$directory/.cargo/config.toml"; do
if [ -e "$config" ] || [ -L "$config" ]; then
printf 'Choose a directory outside this Cargo configuration: %s\n' "$config" >&2
exit 1
fi
done
[ "$directory" = / ] && break
directory=$(dirname "$directory")
done
cargo new --vcs none --edition 2021 video_converter
)
Falls video_converter bereits existiert, wählen Sie ein neues Verzeichnis, statt ein
bestehendes Projekt zu löschen. Alle folgenden Shell-Blöcke werden aus dem übergeordneten
Verzeichnis ausgeführt, das video_converter enthält.
Ersetzen Sie die generierte Datei video_converter/Cargo.toml durch dieses vollständige Manifest.
Die direkten Abhängigkeiten sind auf Versionen mit der getesteten API festgelegt. Bewahren Sie
die generierte Datei Cargo.lock für wiederholte Builds auf.
[package]
name = "video_converter"
version = "0.1.0"
edition = "2021"
[workspace]
[dependencies]
anyhow = "=1.0.104"
tokio = { version = "=1.53.2", features = ["macros", "rt-multi-thread"] }
warp = { version = "=0.3.7", default-features = false }
Eine bekannte Eingabe erstellen
Generieren Sie ein 9,375 Sekunden langes Video mit 1920 × 1080 und 24 Bildern pro Sekunde sowie einem ansteigenden Ton. Das bewegte Testmuster enthält am oberen Rand acht Schwarz-Weiß-Blöcke. Diese codieren die Bildnummer, beginnend bei null. So lassen sich wiederholte oder falsch angeordnete Inhalte leichter erkennen. Die Subshell lehnt eine bereits vorhandene Eingabe ab, bevor FFmpeg ausgeführt wird:
(
set -eu
cd video_converter
if [ -e input.mp4 ] || [ -L input.mp4 ]; then
printf 'input.mp4 already exists; use a fresh project for this sample.\n' >&2
exit 1
fi
ffmpeg -hide_banner -loglevel error -nostdin -n \
-f lavfi -i 'testsrc2=size=1920x1080:rate=24:duration=9.375' \
-f lavfi -i 'aevalsrc=0.1*sin(2*PI*(440*t+40*t*t)):s=48000:d=9.375' \
-filter_threads 1 \
-vf "geq=lum='if(lt(Y,120),16+219*mod(floor(N/pow(2,floor(X/240))),2),lum(X,Y))':cb='cb(X,Y)':cr='cr(X,Y)'" \
-c:v libx264 -threads:v 2 -preset fast -crf 18 -pix_fmt yuv420p \
-c:a aac -b:a 128k -shortest input.mp4
)
Die überprüfte Wiedergabe verwendet dieses 9,375 Sekunden lange Beispiel. Für eigene Quellen
verwenden Sie eine gültige lokale MP4-Datei mit einem nicht gedrehten 16:9-Video mit quadratischen
Pixeln, mindestens 1920 × 1080 Auflösung und einer Audiospur. Diese feste Abstufung ist auf
diese Eingabe ausgelegt; sie wählt keine geeigneten Größen oder Bitraten für beliebiges
Videomaterial. Nur der erste Video- und der erste Audiostream werden verwendet. Der Konverter
normalisiert das Video auf 24 Bilder pro Sekunde und yuv420p. Prüfen Sie
andere Cliplängen und Player separat.
FFmpeg in Rust integrieren
Mit Command übergibt Rust Argumente direkt
an FFmpeg, ohne eine Shell. Der folgende Runner kanonisiert den Eingabepfad, verlangt ein neues
Ausgabeverzeichnis und wartet auf den Abschluss jedes Kindprozesses. Er meldet den Erfolg erst,
wenn beide Konvertierungen erfolgreich und ohne Diagnosen auf Fehlerebene abgeschlossen sind.
Videokonvertierung in HLS und MPEG-DASH implementieren
Ersetzen Sie video_converter/src/main.rs durch dieses vollständige Programm:
use anyhow::{ensure, Context, Result};
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;
const COMMON_ARGS: &[&str] = &[
"-hide_banner", "-loglevel", "error", "-nostdin", "-n", "-xerror",
"-threads", "2",
];
const ENCODING_ARGS: &[&str] = &[
"-c:v", "libx264", "-threads:v", "2", "-filter_threads", "1",
"-preset", "medium", "-profile:v", "main", "-pix_fmt", "yuv420p",
"-r", "24", "-g", "96", "-sc_threshold", "0", "-flags", "+cgop",
"-force_key_frames", "expr:gte(t,n_forced*4)",
"-c:a", "aac", "-b:a", "128k", "-ar", "48000",
];
const HLS_ARGS: &[&str] = &[
"-map", "0:v:0", "-map", "0:a:0",
"-crf", "23", "-f", "hls", "-hls_time", "4",
"-hls_playlist_type", "vod", "-hls_segment_filename", "segment_%03d.ts",
"index.m3u8",
];
const DASH_ARGS: &[&str] = &[
"-map", "0:v:0", "-map", "0:v:0", "-map", "0:a:0",
"-filter:v:0", "scale=1920:1080", "-filter:v:1", "scale=1280:720",
"-b:v:0", "2M", "-b:v:1", "1M",
"-f", "dash", "-seg_duration", "4", "-use_template", "1", "-use_timeline", "1",
"-format_options", "use_editlist=0",
"-init_seg_name", "init-$RepresentationID$.m4s",
"-media_seg_name", "chunk-$RepresentationID$-$Number%05d$.m4s",
"-adaptation_sets", "id=0,streams=v id=1,streams=a", "manifest.mpd",
];
fn convert(input: &Path, output: &Path, format_args: &[&str]) -> Result<()> {
std::fs::create_dir(output).context("Use a new output directory")?;
let result = Command::new("ffmpeg")
.args(COMMON_ARGS)
.arg("-i").arg(input)
.args(ENCODING_ARGS)
.args(format_args)
.current_dir(output)
.output()
.with_context(|| format!("Could not launch FFmpeg for {}", input.display()))?;
io::stderr().write_all(&result.stderr)?;
ensure!(result.status.success() && result.stderr.is_empty(),
"FFmpeg failed for {} ({})", input.display(), result.status);
Ok(())
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args_os().skip(1).collect();
ensure!(args.len() == 2,
"Usage: cargo run --bin video_converter -- INPUT.mp4 NEW_OUTPUT_DIR");
let input = std::fs::canonicalize(&args[0]).context("Cannot open input")?;
let output = Path::new(&args[1]);
std::fs::create_dir(output).context("Use a new output directory")?;
convert(&input, &output.join("hls"), HLS_ARGS)?;
convert(&input, &output.join("dash"), DASH_ARGS)?;
println!("Conversion complete.");
Ok(())
}
In HLS konvertieren
HLS erzeugt hls/index.m3u8 und MPEG-TS-Segmente. Öffnen Sie diese Medien-Playlist
direkt: Ein Beispiel mit nur einer Variante benötigt keine Master-Playlist. CRF 23 wählt eine
Bitratensteuerung mit konstanter Qualität; die Bitrate variiert daher je nach Videomaterial.
Es ist weder ein Zielwert von 2 Mbit/s noch eine Obergrenze für die Spitzenbitrate.
Der HLS-Muxer schneidet an Keyframes nach der angestrebten Segmentdauer. Bei 24 Bildern pro Sekunde richten die geschlossene GOP mit 96 Bildern und die erzwungenen Keyframes die Grenzen auf Vier-Sekunden-Intervalle aus. Das letzte Segment des Beispiels ist kürzer, da 9,375 Sekunden kein Vielfaches von vier sind.
In MPEG-DASH konvertieren
Das doppelte Mapping des Videos erstellt getrennte Repräsentationen in 1080p und 720p in einem
Video-Adaptation-Set; Audio erhält ein eigenes Set. Der DASH-Muxer
schreibt Initialisierungsfragmente, Medienfragmente und ihre Zeitleiste in
dash/manifest.mpd.
Die MP4-Container-Option use_editlist=0
deaktiviert Edit-Listen. Im getesteten DASH-Reader von FFmpeg verhindert dies, dass der Beschnitt
am AAC-Anfang an jeder Fragmentgrenze erneut angewendet wird und Samples zwischen Segmenten
verloren gehen.
Die DASH-Encoder verwenden beispielhafte Zielwerte für die durchschnittliche Bitrate von 2 Mbit/s und 1 Mbit/s. Sie verwenden nicht zusätzlich CRF, da dies in libx264 einen anderen Modus zur Bitratensteuerung wählen würde. Diese Werte sind weder feste Bandbreitengrenzen noch eine Qualitätsempfehlung für jedes Video. Beide Videorepräsentationen verwenden dasselbe Keyframe-Zeitschema.
Einen einfachen Streaming-Server in Rust erstellen
Erstellen Sie das Verzeichnis für eine separate ausführbare Serverdatei:
mkdir video_converter/src/bin
Speichern Sie dieses vollständige Programm als video_converter/src/bin/server.rs. Es liefert das
angegebene Ausgabeverzeichnis über Loopback aus und weist den Streaming-Dateien Inhaltstypen zu.
Die Bereitschaftsmeldung erscheint nach erfolgreicher Bindung; ein belegter Port führt
stattdessen zu einem Fehler.
use anyhow::{ensure, Result};
use warp::{Filter, Reply};
#[tokio::main(worker_threads = 2)]
async fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
ensure!(args.len() == 2, "Usage: cargo run --bin server -- OUTPUT_DIR PORT");
let directory = std::fs::canonicalize(&args[0])?;
let port: u16 = args[1].parse()?;
let routes = warp::fs::dir(directory).map(|file: warp::fs::File| {
let content_type = match file.path().extension().and_then(|ext| ext.to_str()) {
Some("m3u8") => "application/vnd.apple.mpegurl",
Some("mpd") => "application/dash+xml",
Some("ts") => "video/mp2t",
Some("m4s") => "video/mp4",
_ => return file.into_response(),
};
let mut response = file.into_response();
response.headers_mut().insert(
warp::http::header::CONTENT_TYPE,
warp::http::HeaderValue::from_static(content_type),
);
response
});
let (address, server) = warp::serve(routes)
.try_bind_ephemeral(([127, 0, 0, 1], port))?;
println!("Serving http://{address}/");
server.await;
Ok(())
}
Sowohl Audio- als auch Videofragmente von DASH sind MP4-Container; das MPD beschreibt, welche Spur jeweils enthalten ist. Server und Player verwenden denselben lokalen Ursprung, daher benötigt dieses Beispiel keine freizügige CORS-Konfiguration. Es hat weder einen Upload-Endpunkt noch eine Authentifizierung.
Die Streaming-Einrichtung testen
Kompilieren Sie den Konverter im übergeordneten Verzeichnis und führen Sie ihn aus. Starten Sie den Server anschließend nur, wenn die Konvertierung erfolgreich war:
(
cd video_converter &&
cargo run --bin video_converter -- ./input.mp4 ./output &&
cargo run --bin server -- ./output 3030
)
Der Konverter gibt Conversion complete. aus. Der Server gibt anschließend seine URL aus. Falls Port 3030 belegt ist, wählen Sie im Serverbefehl einen anderen Port und verwenden Sie ihn in den folgenden URLs. Führen Sie keinen weiteren Konverter für dasselbe Verzeichnis aus, während es ausgeliefert wird.
Spielen Sie in einem zweiten Terminal beide Manifeste mit dem installierten ffplay-Player ab. Der erste Player wird am Ende beendet, bevor der zweite startet:
ffplay -autoexit -threads 2 http://127.0.0.1:3030/hls/index.m3u8 &&
ffplay -autoexit -threads 2 http://127.0.0.1:3030/dash/manifest.mpd
Jede Wiedergabe sollte etwa 9,375 Sekunden lang dasselbe bewegte Muster und wechselnde Blöcke
zeigen, begleitet von einem stetig ansteigenden Ton. Die generierte Eingabe enthält 225
Videobilder. DASH weist beide Videorepräsentationen in 1920 × 1080 und 1280 × 720 aus. ffplay wählt
einen Videostream, daher demonstriert diese Wiedergabe keinen Wechsel zwischen ihnen. Beenden
Sie einen Player mit q. Stoppen Sie den Server im Vordergrund mit
Ctrl+C in seinem Terminal; die Ausgabedateien bleiben auf dem Datenträger.
Ein fehlendes Segment kann die Wiedergabe zum Stillstand bringen, selbst wenn seine Playlist
geladen wird. Halten Sie den gesamten Verzeichnisbaum output zusammen und
prüfen Sie das tatsächliche Video und Audio bis zum Ende beider Streams. Das bloße Laden eines
Manifests belegt nicht, dass es auf die richtigen Inhalte verweist.
Mit einer fehlgeschlagenen Konvertierung umgehen
Ein bereits vorhandenes Ausgabeverzeichnis führt zu einem Fehler und wird nicht ersetzt. Eine fehlende Datei, eine fehlende Audiospur, ein fehlgeschlagener FFmpeg-Start, ein nativer Exit-Code ungleich null oder ein gemeldeter Decoderfehler verhindern die Abschlussmeldung und den verketteten Serverbefehl. FFmpeg kann beschädigte Medien manchmal wiederherstellen, ohne einen Fehler zu melden. Eine erfolgreiche Konvertierung beweist nicht, dass eine beliebige Quelle intakt war.
Ein Fehler nach Beginn des Encodings kann unvollständige Dateien hinterlassen, darunter auch
eine abgeschlossene HLS-Konvertierung, wenn DASH fehlschlägt. Liefern Sie dieses Verzeichnis nicht
aus. Bewahren Sie es zur Untersuchung auf und versuchen Sie die Konvertierung mit einem neuen
Ausgabenamen erneut, etwa ./output-retry. Starten Sie nach erfolgreicher
Konvertierung den Server mit demselben Namen.
Falls der Download von Abhängigkeiten oder die Kompilierung fehlschlägt, behalten Sie das Projekt
und führen Sie den Build-Befehl erneut aus, nachdem Sie die Verbindungsprobleme oder den
Compilerfehler behoben haben. Die Cargo-Befehle kompilieren vor der Ausführung, sodass nach einem
fehlgeschlagenen Build keine alte ausführbare Datei verwendet wird.
Nutzen Sie dies weiterhin als lokalen Workflow für Konvertierung und Wiedergabe. Eine separate Rust-Aufgabe mit mehreren gleichzeitigen FFmpeg-Jobs finden Sie unter Videos parallel mit Rust mit Wasserzeichen versehen.
