MP4-Streaming in Java mit Qtfaststart optimieren
Mit qtfaststart-java verschieben Sie den Wiedergabeindex einer fertiggestellten MP4-Datei
vor ihre Mediendaten, ohne sie erneut zu codieren. Diese Anleitung erstellt einen Java-Wrapper für die
Befehlszeile, beschränkt die Schreibvorgänge der Bibliothek auf ein privates temporäres Verzeichnis
und vergleicht das Ergebnis mit der Quelldatei, bevor Sie es ausliefern.
Verstehen, was das Vorziehen des Index verändert
MP4-Dateien speichern Spur- und Timing-Informationen im Atom moov und
Mediendaten in Atomen vom Typ mdat.
Wenn moov auf die Mediendaten folgt, benötigt ein Player möglicherweise eine
zusätzliche HTTP-Range-Anfrage, um den Index abzurufen. Sind Range-Anfragen nicht verfügbar, muss er
möglicherweise den Download abwarten. Wird der Index vorgezogen, steht er nahe dem Anfang eines
sequenziellen Downloads zur Verfügung.
Dies optimiert die Dateistruktur für progressives MP4-Streaming. Es verändert weder die codierte Bitrate noch die Codecs und erstellt keine Varianten für adaptives Streaming. Der Wiedergabestart hängt weiterhin vom Player, der Pufferung und dem HTTP-Server ab; dieses Beispiel enthält keinen Benchmark zur Startzeit. Die Dokumentation zum MOV/MP4-Muxer von FFmpeg unterscheidet einen vorangestellten Index von einer fragmentierten Ausgabe.
Java-Projekt einrichten
Das Upstream-Projekt implementiert die Verschiebung vollständig in
Java. Sein Tag 0.1.0 stammt aus dem Jahr 2014, und die Installationsanleitung
nennt weiterhin JCenter. Verwenden Sie das unten angegebene
JitPack-Artefakt mit festgelegter Version; ersetzen Sie es nicht durch
eine ähnlich benannte Bibliothek. Dies ist eine Legacy-Abhängigkeit, kein allgemeiner MP4-Validator.
Verwenden Sie Linux mit Bash und einem Dateisystem, das Hardlinks unterstützt. Installieren Sie eine
gepflegte Version von JDK 21,
Maven und ein Paket mit
FFmpeg und FFprobe. Das Prüfskript benötigt außerdem
cmp, mktemp und rm.
Getestet wurden OpenJDK 21.0.12.1, Maven 3.10.0 und 3.9.11 sowie FFmpeg/FFprobe 9.0.1;
dies sind getestete Versionen, keine grundsätzlich erforderlichen Mindestversionen.
Beginnen Sie in einem beschreibbaren Verzeichnis. Prüfen Sie alle ausführbaren Programme, bevor Sie das Projekt erstellen:
(
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
)
Ein vorhandenes qtfaststart-demo stoppt die Einrichtung. Behalten Sie dessen Dateien
und verwenden Sie für einen Neustart ein anderes übergeordnetes Verzeichnis. Falls der zweite
Aufruf von mkdir fehlschlägt, korrigieren Sie die Verzeichnisberechtigungen
und erstellen Sie die zwei fehlenden Unterverzeichnisse im selben Projekt, bevor Sie fortfahren.
Die Subshell lässt Ihr aktuelles Verzeichnis und Ihre Shell-Optionen unverändert.
Speichern Sie diese vollständige Datei pom.xml in
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>
Speichern Sie settings.xml daneben. Der Build verwendet diese Einstellungen
anstelle Ihrer persönlichen Maven-Einstellungen und lädt Abhängigkeiten in
.m2-repository im Projekt herunter. Sein eigenes Verzeichnis
.mvn und ein explizites Basisverzeichnis isolieren ihn von der
Maven-Konfiguration eines umgebenden Projekts.
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"/>
Wrapper speichern und ausführen
Verwenden Sie eine nachweislich intakte, fertiggestellte, nicht fragmentierte MP4-Datei mit
unkomprimierten Metadaten vom Typ moov. H.264-Video und AAC-Audio sind die
getestete Kombination, auch mit mehreren Spuren. Halten Sie die übergeordneten Verzeichnisse von
Eingabe und Ausgabe während der Verarbeitung unverändert und planen Sie Speicherplatz für eine
zweite Kopie ein. Die Bibliothek lädt moov in den Arbeitsspeicher;
dieses Beispiel ist für vertrauenswürdige lokale Dateien gedacht, nicht für nicht vertrauenswürdige
Uploads oder zum Beheben beschädigter Aufnahmen.
Speichern Sie OptimizeMp4.java in 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);
}
}
}
Kopieren Sie Ihre Quelldatei nach qtfaststart-demo/input.mp4 und fügen Sie dann im übergeordneten
Verzeichnis Folgendes ein:
(
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
)
Das Ziel zum Kopieren der Abhängigkeiten stellt die JAR-Datei bereit, die der ausgegebene Klassenpfad
verwendet. Falls die Auflösung der Abhängigkeiten oder die Kompilierung fehlschlägt, verhindert
&&, dass ein älterer Build ausgeführt wird. Beheben Sie diesen Fehler und
wiederholen Sie den Block. Bei einer Indexverschiebung erstellt das Programm
output_optimized.mp4 und endet mit Exitcode null. Exitcode zwei hinterlässt keine Ausgabe;
Exitcode eins meldet einen Verarbeitungs- oder Dateisystemfehler. Ein erneuter Aufruf lehnt die
vorhandene Ausgabe ab.
Die Implementierung von Version 0.1.0 öffnet ihre Ausgabedatei zum Schreiben und löscht sie, wenn sie eine Indexverschiebung nicht abschließt. Wird eine vorhandene Datei direkt an die Bibliothek übergeben, kann diese sie zerstören. Der Wrapper übergibt ihr nur einen privaten Pfad und verwendet anschließend Files.createLink, um die fertiggestellte Datei bereitzustellen. Auf dem getesteten Linux-Dateisystem wird ein vorhandenes Ziel abgelehnt, ohne es zu ersetzen. Das gilt auch für einen symbolischen Link ohne existierendes Ziel oder ein Ziel, das während der Verarbeitung erstellt wurde.
Dateistruktur und Medien prüfen
Ein Rückgabewert von null vom Wrapper bestätigt die Indexverschiebung, aber keine intakten Medien.
Speichern Sie Folgendes als qtfaststart-demo/verify-mp4.sh. Das Skript prüft die von FFprobe gemeldete
Reihenfolge der Atome auf oberster Ebene, vergleicht Timing und Prüfsummen der codierten Daten jedes
Streams und Pakets und vergleicht alle decodierten Videoframes und Audiosamples. Verwenden Sie für
beide Argumente explizite lokale Pfade, die mit /,
./ oder ../ beginnen.
#!/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'
Führen Sie das gespeicherte Skript als separaten Bash-Prozess aus; fügen Sie seine Anweisungen
set und exit nicht in Ihre aktuelle Shell ein:
(
cd qtfaststart-demo &&
bash verify-mp4.sh ./input.mp4 ./output_optimized.mp4
)
Bei Erfolg wird Verified: moov precedes media; stream data, timing, and decoded audio/video match. ausgegeben. Die Datei-Hashes von Eingabe und Ausgabe unterscheiden sich normalerweise, weil der Index verschoben wird und sich Chunk-Offsets ändern. Die Prüfungen vergleichen stattdessen Medien und Timing. Das Deaktivieren der automatischen Rotation sorgt für einen konsistenten Vergleich der decodierten Frames; die Prüfung der Stream-Zusatzdaten stellt fest, ob die Rotationsmetadaten erhalten bleiben. Ein übereinstimmendes Ergebnis kann nicht belegen, dass die ursprüngliche Aufnahme unbeschädigt war, und misst auch nicht die Wiedergabe im Browser.
Fehlende Ausgabe und Fehler behandeln
Die Prüfung des letzten Atoms in der Bibliothek
gibt false zurück, wenn der Scan nicht bei
moov endet. Dazu gehören Dateien, deren Index bereits vorne steht, aber
auch nicht unterstützte Dateistrukturen und einige fehlerhafte Eingaben. Deuten Sie
false nicht als „bereits optimiert“ und liefern Sie keine nicht existierende
Ausgabe aus. Untersuchen Sie die Quelldatei separat oder wählen Sie einen anderen Verarbeitungsweg.
Komprimierte Metadaten vom Typ moov und bestimmte Chunk-Offset-Überläufe
werden nicht unterstützt. Fehlerhafte Dateien können eine Bibliotheksausnahme, eine I/O-Ausnahme
oder eine ungeprüfte Ausnahme auslösen. Falls die Prüfung Decodierungsfehler oder Unterschiede
meldet, behalten Sie die Quelldatei und liefern Sie die Ausgabe nicht aus. Die Größenprüfung des
Wrappers erkennt unvollständige Kopien; gleiche Größe allein belegt keine korrekten Medien.
Ein nicht beschreibbares oder fehlendes übergeordnetes Ausgabeverzeichnis, fehlende Unterstützung
für Hardlinks oder eine vorhandene Ausgabe führen zu einem Exitcode ungleich null. Der Block
finally entfernt bei einem regulären Fehler die private temporäre Datei und
ihr Verzeichnis. Eine beendete JVM oder ein Dateisystemfehler während des Aufräumens kann ein
Verzeichnis .qtfaststart-* hinterlassen; entfernen Sie nur das temporäre Verzeichnis
dieses Aufrufs, nachdem Sie bestätigt haben, dass sein Prozess nicht mehr läuft.
Alternative Ansätze
Falls FFmpeg bereits Teil Ihrer Verarbeitungspipeline ist, können Sie mit seiner Muxer-Option zum Vorziehen des Index auf eine zusätzliche Legacy-Java-Abhängigkeit verzichten. Muxen Sie für dieselbe lokale Eingabe alle Streams neu, ohne sie erneut zu codieren:
(
cd qtfaststart-demo &&
ffmpeg -nostdin -n -i ./input.mp4 -map 0 -c copy -movflags +faststart ./output_ffmpeg.mp4
)
-n lässt eine vorhandene Ausgabe unverändert, und
-map 0 wählt jeden Stream aus. Unter FFmpeg 9.0.1 lieferte die Ablehnung einer
vorhandenen Datei den Rückgabewert null mit einer Fehlermeldung. Verwenden Sie daher ein neues Ziel
und untersuchen Sie das Ergebnis, statt sich allein auf den Status zu verlassen. Die Codecs müssen
vom MP4-Muxer unterstützt werden. Hierbei wird neu gemuxt: FFmpeg kann Container-Metadaten und Timing
umschreiben. Der strenge Vergleich oben gilt daher für die Java-Indexverschiebung, nicht als
allgemeiner Abnahmetest für FFmpeg.
Mit der MP4-Auslieferung fortfahren
Verwenden Sie das GitHub-Repository von qtfaststart-java, um die Abhängigkeit zu untersuchen, die FFmpeg-Formatreferenz für Muxer-Optionen und den Leitfaden zum MP4-Container von MDN für Hintergrundwissen zu Containern und Codecs. Für einen umfassenderen Encoding-Workflow sehen Sie sich den 🤖 Robot /video/encode von Transloadit an.
