Optimiza el streaming MP4 en Java con Qtfaststart
Usa qtfaststart-java para mover el índice de reproducción de un MP4 finalizado delante de
sus datos multimedia sin volver a codificarlo. Este tutorial crea un programa Java de línea de
comandos que envuelve la biblioteca, mantiene sus escrituras en un directorio temporal privado y
compara el resultado con el original antes de que lo sirvas.
Comprende qué cambia el inicio rápido
Los archivos MP4 almacenan la información de las pistas y los tiempos en el átomo
moov y los datos multimedia en átomos mdat.
Cuando moov está después de los datos multimedia, un reproductor puede
necesitar una solicitud de rango HTTP adicional para recuperar el índice, o esperar a que se
descargue el archivo si las solicitudes de rango no están disponibles. Adelantar el índice permite
que esté disponible cerca del comienzo de una descarga secuencial.
Esta es una optimización de la disposición del archivo para el streaming progresivo de MP4. No cambia la tasa de bits de los datos codificados ni los códecs, y no crea variantes de streaming adaptativo. El inicio de la reproducción sigue dependiendo del reproductor, del almacenamiento en búfer y del servidor HTTP; este ejemplo no mide el tiempo de inicio. La documentación del multiplexor MOV/MP4 de FFmpeg distingue el inicio rápido de la salida fragmentada.
Configura el proyecto Java
El proyecto original implementa la reubicación íntegramente en Java.
Su etiqueta 0.1.0 data de 2014 y sus instrucciones de instalación aún
mencionan JCenter. Usa el artefacto de JitPack con la versión fijada
que se indica a continuación; no lo sustituyas por una biblioteca de nombre similar. Esta es una
dependencia heredada, no un validador general de MP4.
Usa Linux con Bash y un sistema de archivos que admita enlaces duros. Instala una distribución de
JDK 21 que reciba mantenimiento,
Maven y un paquete que contenga tanto
FFmpeg como FFprobe. El script de verificación también necesita
cmp, mktemp y rm.
Las versiones probadas son OpenJDK 21.0.12.1, Maven 3.10.0 y 3.9.11, y FFmpeg/FFprobe 9.0.1;
son versiones probadas, no versiones mínimas intrínsecamente necesarias.
Comienza en un directorio donde puedas escribir. Comprueba cada ejecutable antes de crear el proyecto:
(
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 ya existe qtfaststart-demo, la configuración se detiene. Conserva sus archivos y usa
otro directorio padre para empezar desde cero. Si falla el segundo mkdir,
corrige los permisos del directorio y crea los dos subdirectorios que faltan en ese mismo proyecto
antes de continuar. El subshell mantiene tu directorio actual y las opciones del shell sin cambios.
Guarda este pom.xml completo en 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>
Guarda settings.xml junto a él. La compilación usa esta configuración en lugar de
tu configuración personal de Maven y descarga las dependencias en el
.m2-repository del proyecto. Su propio directorio .mvn
y el directorio base explícito lo aíslan de la configuración de Maven de un proyecto que lo contenga.
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"/>
Guarda el programa y ejecútalo
Usa un MP4 finalizado, no fragmentado y que sepas que está en buen estado, con metadatos
moov sin comprimir. La combinación probada es video H.264 y audio AAC,
incluidas varias pistas. Mantén estables los directorios padre de la entrada y la salida durante el
procesamiento y deja espacio en disco para una segunda copia. La biblioteca carga
moov en memoria; este ejemplo es para archivos locales de confianza, no
para subidas no confiables ni para reparar grabaciones dañadas.
Guarda OptimizeMp4.java en 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);
}
}
}
Copia tu archivo original a qtfaststart-demo/input.mp4 y luego pega esto desde el directorio padre:
(
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
)
El objetivo de copia de dependencias proporciona el JAR que usa la ruta de clases mostrada. Si falla
la resolución de dependencias o la compilación, && impide que se ejecute
una compilación anterior. Corrige ese error y repite el bloque. Cuando reubica el índice, el programa
crea output_optimized.mp4 y termina con código cero. El código de salida dos no deja ningún
archivo de salida; el código uno indica un fallo de procesamiento o del sistema de archivos. Al
volver a ejecutarlo, rechaza la salida existente.
La implementación 0.1.0 abre su archivo de salida para escribir y lo elimina si no termina una reubicación. Pasar un archivo existente directamente a la biblioteca puede destruirlo. El programa que la envuelve solo le proporciona una ruta privada y luego usa Files.createLink para publicar el archivo terminado. En el sistema de archivos Linux probado, se rechaza un destino existente sin reemplazarlo, incluido un enlace simbólico cuyo destino no existe o un destino creado durante el procesamiento.
Verifica la disposición y el contenido multimedia
Un código de retorno cero del programa confirma la reubicación, pero no que el contenido multimedia
esté intacto. Guarda esto como qtfaststart-demo/verify-mp4.sh. Comprueba el orden de los átomos de
nivel superior que informa FFprobe, compara los tiempos y la suma de comprobación de los datos
codificados de cada flujo y paquete, y compara todos los fotogramas de video y las muestras de audio
decodificados. Usa rutas locales explícitas que comiencen con /,
./ o ../ para ambos argumentos.
#!/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'
Ejecuta el script guardado como un proceso Bash independiente; no pegues sus instrucciones
set y exit en tu shell actual:
(
cd qtfaststart-demo &&
bash verify-mp4.sh ./input.mp4 ./output_optimized.mp4
)
Si la verificación tiene éxito, imprime Verified: moov precedes media; stream data, timing, and decoded audio/video match. Los hashes de los archivos de entrada y salida suelen diferir porque el índice se mueve y cambian los desplazamientos de los bloques de datos. Las comprobaciones comparan el contenido multimedia y los tiempos. Desactivar la rotación automática hace que la comparación de fotogramas decodificados sea consistente; los datos auxiliares del flujo permiten comprobar que se conservan los metadatos de rotación. Un resultado coincidente no puede confirmar que la grabación original no estuviera dañada, y tampoco mide la reproducción en el navegador.
Gestiona la ausencia de salida y los fallos
La comprobación del último átomo de la biblioteca
devuelve false cuando el recorrido no termina en
moov. Esto incluye archivos que ya tienen el índice al principio, pero
también disposiciones no compatibles y algunas entradas malformadas. No interpretes
false como «ya optimizado» ni sirvas una salida inexistente. Inspecciona el
original por separado o elige otra vía de procesamiento.
No se admiten metadatos moov comprimidos ni ciertos desbordamientos de los
desplazamientos de los bloques de datos. Los archivos malformados pueden provocar una excepción de
la biblioteca, una excepción de E/S o una excepción no comprobada. Si la verificación informa de
errores de decodificación o diferencias, conserva el original y no sirvas la salida. La comprobación
de tamaño del programa detecta copias incompletas; que los tamaños sean iguales no basta para
confirmar que el contenido multimedia sea correcto.
Un directorio padre de salida que no exista o no permita escribir, la falta de soporte para enlaces
duros o una salida existente provocan un código de salida distinto de cero. El bloque
finally elimina el archivo y el directorio temporales privados ante un fallo
normal. Una JVM terminada de forma forzada o un fallo del sistema de archivos durante la limpieza
pueden dejar un directorio .qtfaststart-*; elimina únicamente el directorio temporal
de esa invocación después de confirmar que su proceso se ha detenido.
Enfoques alternativos
Si FFmpeg ya forma parte de tu pipeline de procesamiento, su opción de inicio rápido del multiplexor evita añadir una dependencia Java heredada. Para la misma entrada local, vuelve a multiplexar todos los flujos sin volver a codificarlos:
(
cd qtfaststart-demo &&
ffmpeg -nostdin -n -i ./input.mp4 -map 0 -c copy -movflags +faststart ./output_ffmpeg.mp4
)
-n deja intacta una salida existente y -map 0
selecciona todos los flujos. En FFmpeg 9.0.1, el rechazo de un archivo existente devolvió cero junto
con un diagnóstico de error, así que usa un destino nuevo e inspecciona el resultado en lugar de
confiar únicamente en el estado. El multiplexor MP4 debe admitir los códecs. Esto es una
remultiplexación: FFmpeg puede reescribir los metadatos del contenedor y los tiempos, por lo que la
comparación estricta anterior sirve para la reubicación en Java, no como prueba general de
aceptación de FFmpeg.
Continúa con la entrega de MP4
Usa el repositorio de qtfaststart-java en GitHub para inspeccionar la dependencia, la referencia de formatos de FFmpeg para consultar las opciones del multiplexor y la guía del contenedor MP4 de MDN para conocer los fundamentos de los contenedores y los códecs. Para un flujo de codificación más amplio, consulta el Robot 🤖 /video/encode de Transloadit.
