Convertir des vidéos en HLS et MPEG-DASH avec Rust
Utilisez Rust pour exécuter FFmpeg, convertir une vidéo locale en HLS et MPEG-DASH, puis servir les fichiers terminés via HTTP. Ce tutoriel crée un rendu HLS et deux représentations vidéo DASH, puis lit chaque format avec ffplay. Le code Rust gère les processus et les fichiers ; FFmpeg assure l’encodage. Vous n’avez donc besoin ni des fichiers d’en-tête de développement de FFmpeg ni de liaisons Rust.
Comprendre les fichiers produits
HLS utilise des listes de lecture .m3u8 ; DASH utilise un manifeste XML .mpd.
Les deux décrivent des segments multimédias qu’un lecteur récupère via HTTP. La lecture à débit
adaptatif nécessite également plusieurs représentations compatibles et un lecteur qui choisit
parmi elles selon l’évolution des conditions.
L’exemple HLS conditionne un seul encodage. L’exemple DASH propose des vidéos de 1920 × 1080 et 1280 × 720 avec une piste audio commune. Créer ces fichiers et les lire localement valide le conditionnement et la lecture, sans démontrer une adaptation à la bande passante, une réduction de la latence de diffusion ou un service de streaming de production.
Configurer un projet Rust
Utilisez Bash sous Linux, une chaîne d’outils Rust stable fournie par
rustup et une
distribution de FFmpeg contenant ffmpeg, ffprobe et
ffplay. Certaines distributions fournissent ffplay dans un paquet distinct ;
vérifiez sa présence avant de créer le projet. La lecture nécessite une session graphique et une
sortie audio. Cette procédure a été testée avec Rust et Cargo 1.98.1 ainsi qu’avec FFmpeg, ffprobe
et ffplay 9.0.1. Il s’agit des versions testées, et non de versions minimales intrinsèquement
requises ; utilisez les vérifications de fonctionnalités ci-dessous pour évaluer une autre version
maintenue.
Sous Ubuntu ou Debian, vous pouvez installer le paquet fourni par la distribution :
sudo apt-get update && sudo apt-get install -y ffmpeg
Vérifiez les versions et les fonctionnalités. Votre version compilée doit inclure les encodeurs
libx264 et AAC, les multiplexeurs et démultiplexeurs HLS et DASH, ainsi que les filtres
testsrc2, geq et aevalsrc utilisés par le générateur de l’échantillon.
cargo --version && rustc --version &&
ffmpeg -version && ffprobe -version && ffplay -version &&
ffmpeg -encoders && ffmpeg -muxers && ffmpeg -demuxers && ffmpeg -filters
Créez le projet en dehors d’un projet Cargo existant ou d’une configuration
.cargo située dans un répertoire parent.
Cargo recherche la configuration dans les répertoires parents,
et la création d’un projet peut ajouter un nouveau paquet à un espace de travail englobant.
Ce bloc à copier-coller vérifie cette limite avant de créer les fichiers et ne modifie ni le
répertoire courant ni les options de votre shell :
(
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
)
Si video_converter existe déjà, choisissez un nouveau répertoire plutôt que de supprimer
un projet existant. Tous les blocs de commandes suivants s’exécutent depuis le répertoire parent
contenant video_converter.
Remplacez le fichier video_converter/Cargo.toml généré par ce manifeste complet. Les versions des
dépendances directes sont figées pour correspondre à l’API testée ; conservez le fichier
Cargo.lock généré pour les compilations ultérieures.
[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 }
Créer une entrée connue
Générez une vidéo de 9,375 secondes en 1920 × 1080 à 24 images par seconde, avec un son dont la fréquence augmente. Le motif de test animé comporte huit blocs noirs et blancs dans sa partie supérieure ; ils encodent le numéro de l’image, à partir de zéro. Ils facilitent l’identification des contenus répétés ou désordonnés. Le sous-shell refuse une entrée existante avant l’exécution de FFmpeg :
(
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
)
La lecture vérifiée utilise cet échantillon de 9,375 secondes. Pour votre propre source, utilisez
un fichier MP4 local valide avec une vidéo sans rotation, à pixels carrés, au format 16:9, d’au
moins 1920 × 1080, et une piste audio. Cette échelle fixe de représentations cible cette entrée ;
elle ne choisit pas de dimensions ni de débits adaptés à des séquences quelconques. Seuls les
premiers flux vidéo et audio sont utilisés. Le convertisseur normalise la vidéo à 24 images par
seconde et en yuv420p. Vérifiez séparément les autres durées de séquence et les autres
lecteurs.
Intégrer FFmpeg à Rust
Command de Rust transmet les arguments
directement à FFmpeg sans passer par un shell. Le lanceur ci-dessous résout le chemin canonique
de l’entrée, exige un nouveau répertoire de sortie et attend la fin de chaque processus enfant.
Il ne signale la réussite qu’une fois les deux conversions terminées avec succès, sans diagnostic
de niveau erreur.
Implémenter la conversion vidéo en HLS et MPEG-DASH
Remplacez video_converter/src/main.rs par ce programme complet :
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(())
}
Convertir en HLS
HLS produit hls/index.m3u8 et des segments MPEG-TS. Ouvrez directement cette liste de
lecture multimédia : un exemple à rendu unique n’a pas besoin de liste de lecture principale.
CRF 23 sélectionne un contrôle du débit à qualité constante ; le débit varie donc selon les
séquences. Ce n’est ni une cible de 2 Mbit/s ni un plafond de débit de pointe.
Le multiplexeur HLS découpe aux images clés après la durée de segment cible. À 24 images par seconde, le GOP fermé de 96 images et les images clés forcées alignent les limites de quatre secondes. Le dernier segment de l’échantillon est plus court, car 9,375 secondes n’est pas un multiple de quatre.
Convertir en MPEG-DASH
Mapper la vidéo deux fois crée des représentations distinctes en 1080p et 720p dans un seul
ensemble d’adaptation vidéo ; l’audio possède son propre ensemble. Le
multiplexeur DASH écrit les fragments d’initialisation, les
fragments multimédias et leur chronologie dans dash/manifest.mpd.
L’option du conteneur MP4 use_editlist=0
désactive les listes d’édition. Dans le démultiplexeur DASH de FFmpeg testé, cela évite de réappliquer
le rognage initial AAC à chaque limite de fragment et de supprimer des échantillons entre les
segments.
Les encodeurs DASH utilisent, à titre d’illustration, des débits moyens cibles de 2 Mbit/s et 1 Mbit/s. Ils n’utilisent pas également CRF, qui sélectionnerait un autre mode de contrôle du débit dans libx264. Ces valeurs ne sont ni des limites strictes de bande passante ni une recommandation de qualité pour toutes les vidéos. Les deux représentations vidéo utilisent le même calendrier d’images clés.
Construire un serveur de streaming simple en Rust
Créez le répertoire d’un exécutable serveur distinct :
mkdir video_converter/src/bin
Enregistrez ce programme complet dans video_converter/src/bin/server.rs. Il sert le répertoire de sortie
spécifié sur l’interface de bouclage et attribue des types de contenu aux fichiers de streaming.
Le message indiquant que le serveur est prêt apparaît après une liaison réussie ; si le port est
occupé, une erreur est renvoyée à la place.
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(())
}
Les fragments DASH audio et vidéo sont tous des conteneurs MP4 ; le MPD décrit la piste contenue dans chacun. Le serveur et le lecteur utilisent la même origine locale ; cet exemple ne nécessite donc aucune configuration CORS permissive. Il ne comporte ni point de terminaison de téléversement ni authentification.
Tester la configuration de streaming
Depuis le répertoire parent, compilez et exécutez le convertisseur, puis démarrez le serveur uniquement si la conversion réussit :
(
cd video_converter &&
cargo run --bin video_converter -- ./input.mp4 ./output &&
cargo run --bin server -- ./output 3030
)
Le convertisseur affiche Conversion complete. Le serveur affiche ensuite son URL. Si le port 3030 est occupé, choisissez un autre port dans la commande du serveur et utilisez-le dans les URL ci-dessous. N’exécutez pas un autre convertisseur sur le même répertoire pendant que vous le servez.
Dans un second terminal, lisez les deux manifestes avec le lecteur ffplay installé. Le premier lecteur se ferme à la fin de la lecture avant le démarrage du second :
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
Chaque lecture devrait afficher le même motif animé et les mêmes blocs changeants pendant environ
9,375 secondes, avec un son dont la fréquence augmente régulièrement. L’entrée générée comporte
225 images vidéo. DASH annonce des représentations vidéo en 1920 × 1080 et 1280 × 720 ; ffplay choisit
un flux vidéo. Cette lecture ne démontre donc pas le passage d’une représentation à l’autre.
Quittez un lecteur avec q. Arrêtez le serveur au premier plan avec Ctrl+C
dans son terminal ; les fichiers de sortie restent sur le disque.
Un segment manquant peut bloquer la lecture même si sa liste de lecture se charge. Conservez
l’arborescence output entière et vérifiez la vidéo et l’audio effectifs jusqu’à
la fin des deux flux. Le chargement d’un manifeste seul ne prouve pas qu’il référence le bon
contenu.
Gérer un échec de conversion
Si le répertoire de sortie existe déjà, l’opération échoue sans le remplacer. Un fichier manquant, l’absence de piste audio, un échec du lancement de FFmpeg, un code de sortie natif non nul ou une erreur signalée par le décodeur empêchent l’affichage du message de fin et l’exécution de la commande serveur enchaînée. FFmpeg peut parfois récupérer un média endommagé sans signaler d’erreur ; une conversion réussie ne prouve pas qu’une source quelconque était intacte.
Un échec après le début de l’encodage peut laisser des fichiers partiels, y compris une conversion
HLS terminée si DASH échoue. Ne servez pas ce répertoire. Conservez-le pour l’examiner et relancez
le convertisseur avec un nouveau nom de sortie, tel que ./output-retry ; après la
réussite, démarrez le serveur avec ce même nom.
Si le téléchargement des dépendances ou la compilation échoue, conservez le projet et relancez
la commande de compilation après avoir résolu le problème de connectivité ou l’erreur du
compilateur. Les commandes Cargo compilent avant d’exécuter ; un ancien exécutable n’est donc pas
utilisé après un échec de compilation.
Limitez ce processus à la conversion et à la lecture locales. Pour une tâche Rust distincte impliquant plusieurs traitements FFmpeg simultanés, consultez le filigranage vidéo concurrent avec Rust.
