Videos parallel mit Rust & FFmpeg mit Wasserzeichen versehen
Führen Sie mit Rust einen kleinen Batch von FFmpeg-Wasserzeichen-Jobs aus, mit höchstens zwei Kindprozessen gleichzeitig. Das folgende Tool fügt jedem Video ein PNG hinzu, erhält die erste Audiospur und liefert einen Exit-Status ungleich null, wenn ein Job fehlschlägt. Erfolgreich erzeugte Videos bleiben verfügbar, auch wenn ein anderer Job fehlschlägt.
Voraussetzungen
- Bash unter Linux für die folgenden Befehle.
- Rust 1.85.1 oder neuer, mit
rustcin IhremPATH. - FFmpeg und ffprobe 6.1.1 oder neuer, mit dem Encoder
libx264und den Filternoverlay,colorundsine. Die Befehle für die Fixtures benötigen auch die AAC- und PNG-Encoder.
Das Beispiel verwendet MP4-Videos mit einem nicht gedrehten Videostream mit geradzahligen Abmessungen und optionalem AAC-Audio sowie ein 8-Bit-PNG-Wasserzeichen, das in jeden Frame passt. Es verarbeitet nur den ersten Videostream und den ersten Audiostream, keine Untertitel oder zusätzlichen Spuren. Der Shell-Ablauf ist Linux-spezifisch; er ist keine getestete Installationsanleitung für Windows oder macOS.
Anzahl der FFmpeg-Prozesse begrenzen
Rust übernimmt die Ablaufplanung und die Exit-Statuswerte; FFmpeg übernimmt das Decodieren, Compositing und Encoding. Es gibt keine FFmpeg-Bindings oder externen Rust-Crates. Zwei Jobs laufen parallel und werden beide beendet, bevor die nächste Gruppe startet. Diese bewusst einfache Gruppierung kann einen Slot ungenutzt lassen, während der langsamere Job noch läuft. Sie begrenzt die Parallelität und ist kein Durchsatz-Benchmark.
Zwei Kindprozesse bedeuten nicht zwei CPU-Threads. Der Code fordert für jede Eingabe und den Video-Encoder jeweils einen Codec-Thread sowie einen Thread für den komplexen Filtergraphen an. FFmpeg kann dennoch weitere interne Threads erzeugen. Diese Einstellungen begrenzen weder die gesamte CPU- noch die gesamte Speichernutzung; siehe die FFmpeg-Optionsreferenz.
Umgebung einrichten
Installieren Sie eine gepflegte stabile Rust-Toolchain mit rustup. Installieren Sie unter Ubuntu oder Debian das FFmpeg-Paket der Distribution:
sudo apt-get update && sudo apt-get install -y ffmpeg
Prüfen Sie rustc --version, ffmpeg -version und ffprobe -version.
Dieses Beispiel wurde mit Rust 1.85.1 und 1.98.1 sowie FFmpeg 6.1.1 und 9.0.1 getestet.
Prüfen Sie ffmpeg -encoders und
ffmpeg -filters, falls Ihrem Build eine Voraussetzung fehlt.
Erstellen Sie ausgehend vom aktuellen Arbeitsverzeichnis ein neues Verzeichnis. Falls es bereits existiert, wählen Sie einen anderen Namen, statt es zu löschen:
mkdir rust-watermark
Speichern Sie den vollständigen Quellcode unten als rust-watermark/watermark.rs.
Wir kompilieren diese eigenständige Datei mit rustc und geben dabei die
Edition und den Pfad der ausführbaren Datei explizit an. Es wird kein Cargo-Projekt erstellt.
Ein übergeordneter Cargo-Workspace oder dessen konfiguriertes Zielverzeichnis steuert diesen Build
also nicht.
Ein einfaches Wasserzeichen-Tool bauen
use std::env;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode, Stdio};
use std::thread;
const MAX_CHILDREN: usize = 2;
fn watermark_video(input: &Path, watermark: &Path, output: &Path) -> io::Result<()> {
let input = fs::canonicalize(input)?;
let result = Command::new("ffmpeg")
.args(["-hide_banner", "-loglevel", "error", "-nostdin", "-n", "-xerror"])
.args(["-threads", "1", "-i"])
.arg(input)
.args(["-threads", "1", "-f", "image2", "-pattern_type", "none", "-i"])
.arg(watermark)
.args([
"-filter_complex_threads", "1",
"-filter_complex", "[0:v:0][1:v:0]overlay=10:10:eof_action=repeat:repeatlast=1[v]",
"-map", "[v]", "-map", "0:a:0?",
"-c:v", "libx264", "-threads:v", "1", "-crf", "20",
"-pix_fmt", "yuv420p", "-c:a", "copy",
"-movflags", "+faststart", "-f", "mp4",
])
.arg(output)
.stdin(Stdio::null())
.stdout(Stdio::null())
.output()?;
if result.status.success() && result.stderr.is_empty() {
return Ok(());
}
if output.exists() {
fs::remove_file(output)?;
}
io::stderr().write_all(&result.stderr)?;
Err(io::Error::other(format!("FFmpeg failed ({})", result.status)))
}
fn run() -> io::Result<ExitCode> {
let arguments: Vec<_> = env::args_os().skip(1).collect();
if arguments.len() < 3 {
return Err(io::Error::other(
"Usage: watermark-batch WATERMARK.png NEW_OUTPUT_DIR INPUT.mp4 [INPUT.mp4 ...]",
));
}
let watermark = fs::canonicalize(&arguments[0])?;
let output_directory = PathBuf::from(&arguments[1]);
fs::create_dir(&output_directory)?;
let mut next_job = 1;
let mut succeeded = 0;
let mut failed = 0;
for group in arguments[2..].chunks(MAX_CHILDREN) {
let mut handles = Vec::new();
for input in group {
let input = PathBuf::from(input);
let output = output_directory.join(format!("job-{next_job}.mp4"));
next_job += 1;
let job_input = input.clone();
let job_output = output.clone();
let job_watermark = watermark.clone();
let handle = thread::spawn(move || {
watermark_video(&job_input, &job_watermark, &job_output)
});
handles.push((input, output, handle));
}
for (input, output, handle) in handles {
match handle.join() {
Ok(Ok(())) => {
succeeded += 1;
println!("OK {} -> {}", input.display(), output.display());
}
outcome => {
failed += 1;
match outcome {
Ok(Err(error)) => eprintln!("FAIL {}: {error}", input.display()),
_ => eprintln!("FAIL {}: worker panicked", input.display()),
}
}
}
}
}
eprintln!("Batch: {succeeded} succeeded, {failed} failed");
Ok(if failed == 0 { ExitCode::SUCCESS } else { ExitCode::FAILURE })
}
fn main() -> ExitCode {
match run() {
Ok(status) => status,
Err(error) => {
eprintln!("Batch setup failed: {error}");
ExitCode::FAILURE
}
}
}
Der Overlay-Filter platziert die linke obere Ecke des Wasserzeichens
10 Pixel vom oberen und linken Videorand entfernt. Durch Wiederholen des letzten PNG-Frames bleibt
das Wasserzeichen sichtbar, nachdem diese Eingabe aus einem einzelnen Frame endet. Das Video wird
mit yuv420p als H.264 neu codiert; die optionale Zuordnung
0:a:0? kopiert die erste Audiospur ohne erneutes Encoding. Absolute
Eingabepfade verhindern, dass führende Bindestriche als Optionen interpretiert werden, und
-pattern_type none behandelt den PNG-Namen wörtlich, einschließlich der Zeichen
%.
Zwei Beispielvideos und ein Wasserzeichen erstellen
Fügen Sie diesen Block im Verzeichnis mit rust-watermark ein. Er erstellt ein
2,32 Sekunden langes blaues Video mit einem 440 Hz-Ton, ein rotes Video mit einem 880 Hz-Ton und ein
weißes PNG mit 48 × 24 Pixeln. Die Subshell lässt Ihr aktuelles Verzeichnis und Ihre Shell-Optionen
unverändert, auch bei Fehlern. Die Schutzprüfung lehnt vorhandene Fixture-Namen ab, bevor Dateien
erzeugt werden. -n ist eine zusätzliche Überschreibprüfung, aber FFmpeg
kann ein verweigertes Überschreiben mit Status 0 melden. Deshalb dient diese Option nicht als
Schutzprüfung beim Einrichten. Verwenden Sie für eine weitere vollständige Ausführung ein neues
Projektverzeichnis.
(
set -eu
cd rust-watermark
for fixture in video1.mp4 video2.mp4 watermark.png; do
if [ -e "$fixture" ] || [ -L "$fixture" ]; then
printf 'Fixture already exists: %s\n' "$fixture" >&2
exit 1
fi
done
ffmpeg -hide_banner -loglevel error -nostdin -n \
-f lavfi -i 'color=c=blue:s=320x180:r=25:d=2.32' \
-f lavfi -i 'sine=frequency=440:sample_rate=48000:duration=2.32' \
-c:v libx264 -threads 1 -pix_fmt yuv420p -c:a aac -shortest video1.mp4
ffmpeg -hide_banner -loglevel error -nostdin -n \
-f lavfi -i 'color=c=red:s=400x240:r=25:d=2.32' \
-f lavfi -i 'sine=frequency=880:sample_rate=48000:duration=2.32' \
-c:v libx264 -threads 1 -pix_fmt yuv420p -c:a aac -shortest video2.mp4
ffmpeg -hide_banner -loglevel error -nostdin -n \
-f lavfi -i 'color=c=white:s=48x24:r=1:d=1' \
-frames:v 1 -c:v png -threads 1 -f image2 -update 1 watermark.png
)
Batch kompilieren und ausführen
Kompilieren Sie weiterhin vom übergeordneten Verzeichnis aus den gespeicherten Quellcode und starten Sie genau diese ausführbare Datei:
(
cd rust-watermark &&
rustc --edition=2021 watermark.rs -o watermark-batch &&
./watermark-batch ./watermark.png ./output ./video1.mp4 ./video2.mp4
)
Die Verkettung mit && verhindert, dass nach einer fehlgeschlagenen
Kompilierung eine alte ausführbare Datei gestartet wird. Die Ausgaben sind
rust-watermark/output/job-1.mp4 und rust-watermark/output/job-2.mp4, nummeriert in der Reihenfolge
der Eingaben. Der Batch gibt Batch: 2 succeeded, 0 failed aus
und endet mit Status 0. Spielen Sie beide Dateien ab: Ihre Abmessungen, Hintergrundfarben und Töne
sollten sich weiterhin unterscheiden, und das weiße Rechteck sollte während des gesamten Videos
jeweils bei (10, 10) bleiben.
Das Ausgabeverzeichnis darf noch nicht existieren. Eine erneute Ausführung mit
./output schlägt fehl, bevor FFmpeg-Kindprozesse gestartet werden, und lässt
die bisherigen Dateien unverändert. Verwenden Sie für einen weiteren Batch ein neues
Ausgabeverzeichnis und lassen Sie keinen anderen Prozess darin schreiben. Erfolgreich erzeugte
Dateien sind bereits sichtbar, während der Batch läuft; warten Sie vor ihrer Nutzung auf den
abschließenden Status. Dies ist kein Verfahren zur atomaren Veröffentlichung.
Fehlerbehandlung und Protokollierung
Command::output()
wartet auf jeden FFmpeg-Kindprozess und erfasst dessen Diagnoseausgaben. Ein Startfehler, ein
Exit-Status ungleich null oder beliebiger Text auf stderr bei Verwendung von
-loglevel error lässt den Job fehlschlagen. Die letzte Prüfung ist wichtig:
FFmpeg 6.1.1 kann einen Decoderfehler melden und dennoch Status 0 zurückgeben, selbst mit
-xerror.
Der Runner wartet per Join auf jeden Worker einer Gruppe, erfasst beide Ergebnisse und verarbeitet die folgenden Gruppen weiter. Die aufsummierte Fehlerzahl bestimmt den abschließenden Status, auch wenn der letzte Job erfolgreich ist.
Testen Sie eine fehlende Eingabe zwischen den beiden gültigen Videos und verwenden Sie dabei ein weiteres neues Ausgabeverzeichnis:
(
cd rust-watermark &&
./watermark-batch ./watermark.png ./mixed-output ./video1.mp4 ./missing.mp4 ./video2.mp4
)
Dies gibt Batch: 2 succeeded, 1 failed aus und endet mit Status 1.
mixed-output/job-1.mp4 und mixed-output/job-3.mp4 bleiben erhalten; eine erfolgreiche
zweite Ausgabe gibt es nicht. Wenn FFmpeg nach dem Erstellen einer Teildatei fehlschlägt oder einen
Fehler meldet, entfernt der Worker diese Datei. Ein Fehler beim Entfernen wird als Jobfehler
gemeldet und kann die Teildatei zurücklassen, sodass sie manuell bereinigt werden muss.
Einrichtungsfehler, etwa ein fehlendes Wasserzeichen oder ein vorhandenes Ausgabeverzeichnis,
treten vor dem Start jeglicher Jobs auf und entfernen keine bisherigen Ergebnisse.
Häufige Probleme und Lösungen
- FFmpeg ist nicht verfügbar: Die betroffenen Jobs können nicht starten. Prüfen Sie den Wert von
PATH, den die ausführbare Rust-Datei erbt, nicht nur den Ihrer interaktiven Shell. - Ein Codec oder Filter fehlt: Prüfen Sie den Fehler auf stderr sowie die installierten Encoder und Filter. Gleichzeitige FFmpeg-Diagnoseausgaben können sich vermischen; die Rust-Meldungen benennen jede Eingabe.
- Ein Job bleibt hängen: Hier gibt es weder ein Timeout noch eine Abbruchbehandlung. Bei einer regulär abgeschlossenen Ausführung sind die Kindprozesse beendet und alle Worker per Join zusammengeführt. Das Beenden von Rust garantiert weder das Ende der Kindprozesse noch das Bereinigen von Teildateien; ein Dienst benötigt eine separate Überwachungsstrategie.
- Eine beschädigte Datei erzeugt dennoch eine Ausgabe: Der Runner weist gemeldete Fehler zurück, aber ein Decoder kann Schäden kaschieren, ohne einen Fehler zu melden. Ein erfolgreicher Job beweist nicht, dass seine Quelle intakt war. Prüfen Sie das tatsächliche Video und Audio, bevor Sie sich auf das Ergebnis verlassen.
- Diagnoseausgaben verbrauchen Arbeitsspeicher: Dieses kleine Beispiel erfasst die Fehlerausgabe jedes Kindprozesses im Arbeitsspeicher. Es begrenzt die Protokollgröße nicht. Ein Dienst benötigt neben der Prozessüberwachung auch größenbegrenzte oder fortlaufend gestreamte Diagnoseausgaben.
Nutzen Sie dies als lokalen Batch-Runner, nicht als Upload-Dienst. Messen Sie bei einer größeren
Warteschlange den Ressourcenverbrauch, bevor Sie MAX_CHILDREN ändern, und legen
Sie fest, wie Aufrufer mit erhaltenen erfolgreichen Ergebnissen und fehlgeschlagenen Jobs umgehen.
Rusts Parallelität allein belegt weder eine schnellere Verarbeitung noch Skalierbarkeit im
Produktivbetrieb.
