Filigranage vidéo en parallèle avec Rust et FFmpeg
Utilisez Rust pour exécuter un petit lot de tâches de filigranage FFmpeg, avec au plus deux processus enfants à la fois. L’outil ci-dessous ajoute un PNG à chaque vidéo, conserve sa première piste audio et renvoie un statut de sortie non nul si une tâche échoue. Les vidéos traitées avec succès restent disponibles même lorsqu’une autre tâche échoue.
Prérequis
- Bash sous Linux pour les commandes ci-dessous.
- Rust 1.85.1 ou plus récent, avec
rustcdans votrePATH. - FFmpeg et ffprobe 6.1.1 ou plus récents, avec l’encodeur
libx264et les filtresoverlay,coloretsine. Les commandes de création des fichiers de test nécessitent aussi les encodeurs AAC et PNG.
L’exemple utilise des vidéos MP4 dont le flux vidéo n’est pas pivoté et a des dimensions paires, avec un audio AAC facultatif, ainsi qu’un filigrane PNG 8 bits assez petit pour tenir dans chaque image. Il ne traite que le premier flux vidéo et le premier flux audio, sans les sous-titres ni les pistes supplémentaires. La procédure shell est propre à Linux ; il ne s’agit pas d’un guide d’installation testé pour Windows ou macOS.
Limiter le nombre de processus FFmpeg
Rust gère l’ordonnancement et les statuts de sortie ; FFmpeg gère le décodage, la composition et l’encodage. Il n’y a ni bindings FFmpeg ni crates Rust externes. Deux tâches s’exécutent ensemble, puis toutes deux se terminent avant que le groupe suivant ne démarre. Ce regroupement volontairement simple peut laisser un emplacement inactif pendant que la tâche la plus lente se termine. Il s’agit d’une limite de concurrence, pas d’un benchmark de débit.
Deux processus enfants ne signifient pas deux threads CPU. Le code demande un thread de codec pour chaque entrée et pour l’encodeur vidéo, ainsi qu’un thread pour le graphe de filtres complexe. FFmpeg peut tout de même créer d’autres threads internes. Ces réglages n’imposent aucune limite totale de CPU ou de mémoire ; consultez la référence des options FFmpeg.
Configuration de l’environnement
Installez une chaîne d’outils Rust stable et maintenue via rustup. Sous Ubuntu ou Debian, installez le paquet FFmpeg de la distribution :
sudo apt-get update && sudo apt-get install -y ffmpeg
Vérifiez rustc --version, ffmpeg -version et ffprobe -version. Cet exemple a été testé avec
Rust 1.85.1 et 1.98.1, ainsi qu’avec FFmpeg 6.1.1 et 9.0.1. Consultez ffmpeg -encoders et
ffmpeg -filters s’il manque un prérequis à votre build.
Créez un nouveau répertoire depuis votre répertoire de travail actuel. S’il existe déjà, choisissez un autre nom plutôt que de le supprimer :
mkdir rust-watermark
Enregistrez le code source complet ci-dessous sous le nom rust-watermark/watermark.rs. Nous compilons ce fichier
autonome avec rustc, en précisant explicitement l’édition et le chemin de l’exécutable. Aucun projet
Cargo n’est créé : un espace de travail Cargo englobant ou son répertoire cible configuré ne
contrôle donc pas cette compilation.
Création d’un outil de filigranage simple
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
}
}
}
Le filtre overlay place le coin supérieur gauche du filigrane à
10 pixels des bords supérieur et gauche de la vidéo. La répétition de la dernière image du PNG le
maintient visible après la fin de cette entrée à image unique. La vidéo est réencodée en H.264 avec
yuv420p ; le mappage facultatif 0:a:0? copie la première piste audio sans la réencoder. Les
chemins d’entrée absolus évitent que des tirets initiaux soient interprétés comme des options, et
-pattern_type none traite le nom du PNG littéralement, y compris les caractères %.
Créer deux vidéos d’exemple et un filigrane
Depuis le répertoire qui contient rust-watermark, collez ce bloc. Il crée une vidéo bleue de
2,32 secondes avec une tonalité à 440 Hz, une vidéo rouge avec une tonalité à 880 Hz et un PNG
blanc de 48 × 24 pixels. Le sous-shell laisse inchangés votre répertoire courant et vos options
de shell, y compris en cas d’échec. La vérification préalable rejette les noms de fichiers de test
existants avant de générer le moindre fichier. -n constitue une vérification supplémentaire contre
l’écrasement, mais FFmpeg peut signaler un refus d’écrasement avec le statut 0 ; ce n’est donc pas
la vérification de préparation. Utilisez un nouveau répertoire de projet pour toute autre
réexécution complète.
(
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
)
Compiler et exécuter le lot
Toujours depuis le répertoire parent, compilez la source enregistrée et exécutez exactement cet exécutable :
(
cd rust-watermark &&
rustc --edition=2021 watermark.rs -o watermark-batch &&
./watermark-batch ./watermark.png ./output ./video1.mp4 ./video2.mp4
)
L’enchaînement && empêche d’exécuter un ancien exécutable après un échec de compilation. Les
sorties sont rust-watermark/output/job-1.mp4 et rust-watermark/output/job-2.mp4, numérotées dans l’ordre des entrées.
Le lot affiche Batch: 2 succeeded, 0 failed et se termine avec
le statut 0. Lisez les deux fichiers : leurs dimensions, couleurs de fond et tonalités doivent
rester distinctes, avec le rectangle blanc en (10, 10) tout au long de chaque vidéo.
Le répertoire de sortie ne doit pas déjà exister. Une réexécution avec ./output échoue avant de lancer
le moindre processus enfant FFmpeg et laisse les fichiers précédents inchangés. Utilisez un nouveau
répertoire de sortie pour un autre lot, et ne laissez aucun autre processus y écrire. Les fichiers
réussis sont visibles pendant l’exécution du lot ; attendez son statut final avant de les utiliser.
Il ne s’agit pas d’un mécanisme de publication atomique.
Gestion des erreurs et journalisation
Command::output()
attend chaque processus enfant FFmpeg et capture ses diagnostics. Une erreur de lancement, un statut
de sortie non nul ou toute sortie non vide sur stderr, FFmpeg s’exécutant avec -loglevel error, fait
échouer la tâche. Cette dernière vérification compte : FFmpeg 6.1.1 peut signaler une erreur de
décodeur tout en renvoyant le statut 0, même avec -xerror.
L’exécuteur joint chaque thread de travail d’un groupe, enregistre les deux résultats et continue de traiter les groupes suivants. Le nombre cumulé d’échecs détermine le statut final, même lorsque la dernière tâche réussit.
Testez une entrée manquante entre les deux vidéos valides, en utilisant un autre nouveau répertoire de sortie :
(
cd rust-watermark &&
./watermark-batch ./watermark.png ./mixed-output ./video1.mp4 ./missing.mp4 ./video2.mp4
)
Cela affiche Batch: 2 succeeded, 1 failed et se termine avec le
statut 1. mixed-output/job-1.mp4 et mixed-output/job-3.mp4 subsistent ; il n’y a pas de seconde sortie réussie.
Si FFmpeg échoue ou signale une erreur après avoir créé un fichier partiel, le thread de travail
supprime ce fichier. Une erreur de suppression est signalée comme un échec de tâche et peut laisser
le fichier partiel à nettoyer manuellement. Les échecs de préparation, comme un filigrane manquant
ou un répertoire de sortie existant, surviennent avant le démarrage de toute tâche et ne suppriment
pas les résultats précédents.
Problèmes courants et solutions
- FFmpeg est indisponible : les tâches concernées ne parviennent pas à démarrer. Vérifiez le
PATHhérité par l’exécutable Rust, pas seulement celui de votre shell interactif. - Un codec ou un filtre manque : examinez l’erreur sur stderr ainsi que les encodeurs et filtres installés. Les diagnostics de processus FFmpeg concurrents peuvent s’entremêler ; les messages Rust nomment chaque entrée.
- Une tâche est bloquée : il n’y a ici ni délai d’expiration ni gestionnaire d’annulation. Lors d’une exécution normale menée à terme, les processus enfants se sont terminés et les threads de travail ont été joints. Tuer le processus Rust ne garantit ni l’arrêt des processus enfants ni le nettoyage des fichiers partiels ; un service nécessite une politique de supervision distincte.
- Un fichier endommagé produit tout de même une sortie : l’exécuteur rejette les erreurs signalées, mais un décodeur peut masquer des dommages sans en signaler. Une tâche réussie ne prouve pas que sa source était intacte. Examinez la vidéo et l’audio réels avant de vous fier au résultat.
- Les diagnostics consomment de la mémoire : ce petit exemple capture en mémoire la sortie d’erreur de chaque processus enfant. Il ne limite pas la taille des journaux. Un service nécessite des diagnostics bornés ou diffusés en continu, ainsi qu’une supervision des processus.
Utilisez cet outil comme un exécuteur de lots local, pas comme un service de téléversement. Pour une
file d’attente plus importante, mesurez l’utilisation des ressources avant de modifier MAX_CHILDREN, et
décidez comment les appelants gèrent les réussites conservées et les tâches échouées. La
concurrence de Rust n’établit pas à elle seule un traitement plus rapide ni la scalabilité en
production.
