Optimiser le streaming MP4 en Java avec Qtfaststart
Utilisez qtfaststart-java pour placer l’index de lecture d’un MP4 finalisé avant ses
données multimédias sans réencodage. Ce guide crée un programme Java en ligne de commande qui encapsule
la bibliothèque, confine ses écritures à un répertoire temporaire privé et compare le résultat à la
source avant que vous le serviez.
Comprendre les changements apportés par le démarrage rapide
Les fichiers MP4 stockent les informations sur les pistes et leur minutage dans l’atome
moov, et les données multimédias dans des atomes mdat.
Lorsque moov suit les données multimédias, un lecteur peut avoir besoin d’une
requête HTTP supplémentaire portant sur une plage d’octets pour récupérer l’index, ou devoir attendre
le téléchargement si ces requêtes ne sont pas disponibles. Déplacer l’index vers le début le rend
accessible dès les premières données d’un téléchargement séquentiel.
Cette optimisation de l’agencement du fichier concerne le streaming MP4 progressif. Elle ne change ni le débit encodé ni les codecs et ne crée pas de variantes pour le streaming adaptatif. Le démarrage de la lecture dépend toujours du lecteur, de la mise en mémoire tampon et du serveur HTTP ; cet exemple ne mesure pas le temps de démarrage. La documentation du multiplexeur MOV/MP4 de FFmpeg distingue le démarrage rapide de la sortie fragmentée.
Configurer le projet Java
Le projet amont implémente le déplacement en Java pur. Son tag
0.1.0 date de 2014, et ses instructions d’installation mentionnent encore
JCenter. Utilisez l’artefact JitPack à la version fixée ci-dessous ;
ne le remplacez pas par une bibliothèque au nom similaire. Il s’agit d’une dépendance ancienne,
pas d’un outil général de validation MP4.
Utilisez Linux avec Bash et un système de fichiers prenant en charge les liens physiques. Installez
une distribution maintenue du JDK 21,
Maven et un paquet contenant à la fois
FFmpeg et FFprobe. Le script de vérification nécessite aussi
cmp, mktemp et rm.
Les versions testées sont OpenJDK 21.0.12.1, Maven 3.10.0 et 3.9.11, et FFmpeg/FFprobe 9.0.1 ;
ce sont des versions testées, pas des versions minimales intrinsèquement requises.
Commencez dans un répertoire accessible en écriture. Vérifiez chaque exécutable avant de créer le projet :
(
for tool in java javac mvn ffmpeg ffprobe cmp mktemp rm mkdir; do
command -v "$tool" >/dev/null 2>&1 || {
printf 'Missing prerequisite: %s\n' "$tool" >&2
exit 1
}
done
java -version && javac -version &&
MAVEN_SKIP_RC=true MAVEN_BASEDIR="$PWD/qtfaststart-demo" MAVEN_ARGS= mvn -version &&
ffmpeg -version && ffprobe -version &&
mkdir qtfaststart-demo &&
mkdir -p qtfaststart-demo/.mvn qtfaststart-demo/src/main/java
)
Si qtfaststart-demo existe déjà, la configuration s’arrête. Conservez ses fichiers et
utilisez un autre répertoire parent pour repartir de zéro. Si le second
mkdir échoue, corrigez les permissions du répertoire et créez les deux
sous-répertoires manquants dans ce même projet avant de continuer. Le sous-shell conserve votre
répertoire courant et les options de votre shell sans les modifier.
Enregistrez ce pom.xml complet dans qtfaststart-demo/ :
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>local.example</groupId>
<artifactId>qtfaststart-demo</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.ypresto</groupId>
<artifactId>qtfaststart-java</artifactId>
<version>0.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</build>
</project>
Enregistrez settings.xml à côté. La compilation utilise ces paramètres à la place
de vos paramètres Maven personnels et télécharge les dépendances dans le
.m2-repository du projet. Son propre répertoire .mvn et son
répertoire de base explicite l’isolent de la configuration Maven d’un éventuel projet englobant.
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"/>
Enregistrer le programme et l’exécuter
Utilisez un MP4 finalisé et non fragmenté, dont le bon état est connu, avec des métadonnées
moov non compressées. La vidéo H.264 et l’audio AAC constituent la
combinaison testée, y compris avec plusieurs pistes. Maintenez les répertoires parents de l’entrée
et de la sortie stables pendant le traitement, et prévoyez assez d’espace disque pour une seconde
copie. La bibliothèque charge moov en mémoire ; cet exemple concerne des
fichiers locaux de confiance, pas des téléversements non fiables ni la réparation d’enregistrements
endommagés.
Enregistrez OptimizeMp4.java dans qtfaststart-demo/src/main/java/ :
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import net.ypresto.qtfaststart.QtFastStart;
import net.ypresto.qtfaststart.QtFastStart.QtFastStartException;
public class OptimizeMp4 {
public static boolean optimize(Path input, Path output)
throws IOException, QtFastStartException {
input = input.toRealPath();
if (!Files.isRegularFile(input)) {
throw new IOException("Input must be a regular file: " + input);
}
output = output.toAbsolutePath();
if (Files.exists(output, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Output already exists: " + output);
}
Path directory = Files.createTempDirectory(output.getParent(), ".qtfaststart-");
Path temporary = directory.resolve("optimized.mp4");
try {
boolean moved = QtFastStart.fastStart(input.toFile(), temporary.toFile());
if (!moved) return false;
if (Files.size(temporary) != Files.size(input)) {
throw new IOException("Relocation produced an incomplete copy");
}
Files.createLink(output, temporary);
return true;
} finally {
Files.deleteIfExists(temporary);
Files.delete(directory);
}
}
public static void main(String[] args) {
if (args.length != 2) {
System.err.println("Usage: OptimizeMp4 INPUT.mp4 OUTPUT.mp4");
System.exit(64);
}
try {
if (!optimize(Path.of(args[0]), Path.of(args[1]))) {
System.err.println("No output created: already front-indexed or unsupported layout."
+ " This does not validate the input.");
System.exit(2);
}
System.out.println("Relocated MP4 saved to " + args[1] + "; verify it before serving.");
} catch (IOException | QtFastStartException | RuntimeException e) {
System.err.println("Failed for " + args[0] + ": " + e.getMessage());
System.exit(1);
}
}
}
Copiez votre source vers qtfaststart-demo/input.mp4, puis collez ceci depuis le répertoire parent :
(
cd qtfaststart-demo &&
MAVEN_SKIP_RC=true MAVEN_BASEDIR="$PWD" MAVEN_ARGS= \
mvn -B -s settings.xml -gs settings.xml -Dmaven.repo.local=.m2-repository \
clean package dependency:copy-dependencies &&
java -cp 'target/classes:target/dependency/*' OptimizeMp4 \
./input.mp4 ./output_optimized.mp4
)
L’objectif de copie des dépendances fournit le JAR utilisé par le classpath de la commande affichée.
Si la résolution des dépendances ou la compilation échoue, && empêche
l’exécution du résultat d’une compilation antérieure. Corrigez cette erreur et répétez le bloc.
Lorsque le déplacement a lieu, le programme crée output_optimized.mp4 et se termine avec
le code zéro. Le code de sortie deux ne laisse aucune sortie ; le code de sortie un signale une
erreur de traitement ou de système de fichiers. Une nouvelle exécution refuse la sortie existante.
L’implémentation 0.1.0 ouvre sa sortie en écriture et la supprime lorsqu’elle ne termine pas le déplacement. Passer directement un fichier existant à la bibliothèque peut le détruire. Le programme ne lui fournit qu’un chemin privé, puis utilise Files.createLink pour publier le fichier finalisé. Sur le système de fichiers Linux testé, une destination existante, y compris un lien symbolique sans cible ou une destination créée pendant le traitement, est refusée sans être remplacée.
Vérifier l’agencement et les données multimédias
Un code de retour zéro du programme confirme le déplacement, pas l’intégrité des données
multimédias. Enregistrez ceci sous qtfaststart-demo/verify-mp4.sh. Le script vérifie l’ordre des
atomes de premier niveau indiqué par FFprobe, compare le minutage et la somme de contrôle des
données encodées de chaque flux et de chaque paquet, puis compare toutes les images vidéo et tous
les échantillons audio décodés. Pour les deux arguments, utilisez des chemins locaux explicites
commençant par /, ./ ou
../.
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -ne 2 ]; then
printf 'Usage: bash verify-mp4.sh INPUT.mp4 OUTPUT.mp4\n' >&2
exit 64
fi
input=$1
output=$2
for path in "$input" "$output"; do
case "$path" in
/*|./*|../*) ;;
*) printf 'Use an explicit local path: %s\n' "$path" >&2; exit 64 ;;
esac
done
work=$(mktemp -d)
trap 'rm -rf -- "$work"' EXIT
ffprobe -v trace -i "$output" -show_entries format=format_name \
-of default > /dev/null 2> "$work/atoms.log"
seen_moov=false
seen_mdat=false
while IFS= read -r line; do
case "$line" in
*"type:'moov' parent:'root'"*)
[ "$seen_moov" = false ] || exit 1
seen_moov=true ;;
*"type:'mdat' parent:'root'"*)
[ "$seen_moov" = true ] || { printf 'moov follows media\n' >&2; exit 1; }
seen_mdat=true ;;
*"type:'moof' parent:'root'"*) printf 'Fragmented MP4 is outside this example\n' >&2; exit 1 ;;
esac
done < "$work/atoms.log"
[ "$seen_moov" = true ] && [ "$seen_mdat" = true ]
entries='stream=index,codec_type,codec_name,profile,width,height,pix_fmt,sample_rate,channels,channel_layout,time_base,start_pts,start_time,duration_ts,duration,nb_frames,extradata_hash:stream_tags:stream_side_data:packet=stream_index,pts,dts,duration,size,flags,data_hash'
ffprobe -v error -show_entries "$entries" -show_data_hash sha256 \
-of json -i "$input" > "$work/input.json"
ffprobe -v error -show_entries "$entries" -show_data_hash sha256 \
-of json -i "$output" > "$work/output.json"
cmp "$work/input.json" "$work/output.json"
ffmpeg -nostdin -v error -xerror -threads 1 -noautorotate -i "$input" \
-map 0:v? -map 0:a? -c:v rawvideo -c:a pcm_s16le -threads 1 \
-f framemd5 "$work/input.framemd5"
ffmpeg -nostdin -v error -xerror -threads 1 -noautorotate -i "$output" \
-map 0:v? -map 0:a? -c:v rawvideo -c:a pcm_s16le -threads 1 \
-f framemd5 "$work/output.framemd5"
cmp "$work/input.framemd5" "$work/output.framemd5"
printf 'Verified: moov precedes media; stream data, timing, and decoded audio/video match.\n'
Exécutez le script enregistré dans un processus Bash distinct ; ne collez pas ses instructions
set et exit dans votre shell courant :
(
cd qtfaststart-demo &&
bash verify-mp4.sh ./input.mp4 ./output_optimized.mp4
)
En cas de réussite, le script affiche Verified: moov precedes media; stream data, timing, and decoded audio/video match. Les empreintes des fichiers d’entrée et de sortie diffèrent généralement, car l’index est déplacé et les décalages des blocs changent. Les vérifications comparent plutôt les données multimédias et leur minutage. La désactivation de la rotation automatique rend la comparaison des images décodées cohérente ; les données annexes des flux permettent de vérifier la conservation des métadonnées de rotation. Un résultat identique ne permet pas d’établir que l’enregistrement original était intact, et ne mesure pas la lecture dans un navigateur.
Gérer l’absence de sortie et les échecs
La vérification du dernier atome de la bibliothèque renvoie
false lorsque le parcours ne se termine pas sur
moov. Cela inclut les fichiers dont l’index est déjà placé au début, mais
aussi des agencements non pris en charge et certaines entrées mal formées. N’interprétez pas
false comme « déjà optimisé » et ne servez pas une sortie inexistante.
Examinez la source séparément ou choisissez une autre méthode de traitement.
Les métadonnées moov compressées et certains dépassements des décalages
des blocs ne sont pas pris en charge. Des fichiers mal formés peuvent provoquer une exception de
la bibliothèque, une exception d’entrée/sortie ou une exception non vérifiée. Si la vérification
signale des erreurs de décodage ou des différences, conservez la source et ne servez pas la sortie.
La vérification de taille du programme détecte les copies incomplètes ; une taille identique ne
suffit pas à établir que les données multimédias sont correctes.
Un répertoire parent de sortie absent ou inaccessible en écriture, l’absence de prise en charge des
liens physiques ou une sortie existante entraîne un code de sortie non nul. Le bloc
finally supprime le fichier et le répertoire temporaires privés lors d’un
échec ordinaire. Une JVM arrêtée de force ou une défaillance du système de fichiers pendant le
nettoyage peut laisser un répertoire .qtfaststart-* ; supprimez uniquement le
répertoire temporaire de cette invocation, après avoir confirmé que son processus est arrêté.
Autres approches
Si FFmpeg fait déjà partie de votre chaîne de traitement, son option de multiplexage pour le démarrage rapide évite d’ajouter une dépendance Java ancienne. Pour la même entrée locale, remultiplexez tous les flux sans réencodage :
(
cd qtfaststart-demo &&
ffmpeg -nostdin -n -i ./input.mp4 -map 0 -c copy -movflags +faststart ./output_ffmpeg.mp4
)
-n laisse une sortie existante intacte, et
-map 0 sélectionne chaque flux. Avec FFmpeg 9.0.1, le refus d’un fichier
existant a renvoyé zéro avec un message d’erreur ; utilisez donc une nouvelle destination et
examinez le résultat plutôt que de vous fier uniquement au code de sortie. Les codecs doivent être
pris en charge par le multiplexeur MP4. Il s’agit d’un remultiplexage : FFmpeg peut réécrire les
métadonnées du conteneur et le minutage. La comparaison stricte ci-dessus concerne donc le
déplacement effectué en Java, pas un test général de validation des résultats de FFmpeg.
Poursuivre avec la diffusion MP4
Consultez le dépôt GitHub de qtfaststart-java pour examiner la dépendance, la référence des formats de FFmpeg pour les options des multiplexeurs, et le guide du conteneur MP4 de MDN pour comprendre les conteneurs et les codecs. Pour un processus d’encodage plus large, consultez le 🤖 Robot /video/encode de Transloadit.
