Otimizar o streaming de MP4 em Java com Qtfaststart
Use qtfaststart-java para mover o índice de reprodução de um MP4 finalizado
para antes dos dados de mídia, sem recodificar. Este guia cria um utilitário de linha de comando em
Java, mantém as gravações da biblioteca em um diretório temporário privado e compara o resultado com
o arquivo de origem antes de você disponibilizá-lo.
Entender o que o início rápido altera
Arquivos MP4 armazenam informações de faixas e temporização no átomo
moov e os dados de mídia em átomos mdat.
Quando moov vem depois da mídia, o player pode precisar de uma requisição
HTTP adicional de intervalo para obter o índice ou aguardar o download quando requisições de
intervalo não estão disponíveis. Mover o índice para a frente o torna disponível perto do início de
um download sequencial.
Essa é uma otimização da disposição dos dados no arquivo para streaming progressivo de MP4. Ela não altera a taxa de bits codificada nem os codecs e não cria variantes de streaming adaptativo. O início da reprodução ainda depende do player, do armazenamento em buffer e do servidor HTTP; este exemplo não apresenta um benchmark do tempo de início da reprodução. A documentação do multiplexador MOV/MP4 do FFmpeg distingue o início rápido da saída fragmentada.
Configurar o projeto Java
O projeto original implementa a realocação inteiramente em
Java. Sua tag 0.1.0 é de 2014, e as instruções de instalação ainda mencionam
JCenter. Use o artefato do JitPack com versão fixada abaixo;
não o substitua por uma biblioteca com nome semelhante. Essa é uma dependência legada, não um
validador geral de MP4.
Use Linux com Bash e um sistema de arquivos que ofereça suporte a links físicos. Instale um build
com manutenção ativa do JDK 21, o
Maven e um pacote que contenha tanto o
FFmpeg quanto o FFprobe. O script de verificação também precisa de
cmp, mktemp e rm.
As versões testadas são OpenJDK 21.0.12.1, Maven 3.10.0 e 3.9.11 e FFmpeg/FFprobe 9.0.1;
essas são as versões testadas, não versões mínimas intrínsecas.
Comece em um diretório com permissão de escrita. Verifique cada executável antes de criar o projeto:
(
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
)
Se qtfaststart-demo já existir, a configuração será interrompida. Preserve seus arquivos
e use outro diretório pai para começar do zero. Se o segundo mkdir falhar,
corrija as permissões do diretório e crie os dois subdiretórios ausentes nesse mesmo projeto antes de
continuar. O subshell mantém seu diretório atual e as opções do shell inalterados.
Salve este pom.xml completo em 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>
Salve settings.xml ao lado dele. O build usa essas configurações em vez das suas
configurações pessoais do Maven e baixa as dependências para .m2-repository do
projeto. Seu próprio diretório .mvn e o diretório base explícito o isolam
da configuração do Maven de um projeto que o contenha.
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"/>
Salvar o utilitário e executá-lo
Use um MP4 comprovadamente válido, finalizado e não fragmentado, com metadados
moov não comprimidos. Vídeo H.264 e áudio AAC são a combinação testada,
incluindo múltiplas faixas. Mantenha estáveis os diretórios pais da entrada e da saída durante o
processamento e reserve espaço em disco para uma segunda cópia. A biblioteca carrega
moov na memória; este exemplo se destina a arquivos locais confiáveis,
não a uploads não confiáveis nem à recuperação de gravações danificadas.
Salve OptimizeMp4.java em 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);
}
}
}
Copie seu arquivo de origem para qtfaststart-demo/input.mp4 e cole este bloco no diretório pai:
(
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
)
O objetivo de cópia de dependências fornece o JAR usado pelo classpath mostrado no comando. Se a
resolução de dependências ou a compilação falhar, && impede a execução de
um build anterior. Corrija esse erro e repita o bloco. Quando realiza a realocação, o programa cria
output_optimized.mp4 e termina com código zero. O código de saída dois não deixa arquivo de
saída; o código um indica uma falha de processamento ou do sistema de arquivos. Uma nova execução
recusa o arquivo de saída existente.
A implementação 0.1.0 abre seu arquivo de saída para escrita e o exclui quando não conclui uma realocação. Passar um arquivo existente diretamente à biblioteca pode destruí-lo. O utilitário fornece a ela apenas um caminho privado e depois usa Files.createLink para publicar o arquivo concluído. No sistema de arquivos Linux testado, um destino existente, incluindo um link simbólico cujo alvo não existe ou um destino criado durante o processamento, é recusado sem ser substituído.
Verificar a disposição dos dados e a mídia
Um retorno zero do utilitário confirma a realocação, não a integridade da mídia. Salve o código a
seguir como qtfaststart-demo/verify-mp4.sh. Ele verifica a ordem dos átomos de nível superior
informada pelo FFprobe, compara a temporização e a soma de verificação dos dados codificados de cada
fluxo e pacote e compara todos os quadros de vídeo e amostras de áudio decodificados. Use caminhos
locais explícitos que comecem com /, ./
ou ../ para ambos os 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'
Execute o script salvo como um processo Bash separado; não cole suas instruções
set e exit no shell atual:
(
cd qtfaststart-demo &&
bash verify-mp4.sh ./input.mp4 ./output_optimized.mp4
)
Em caso de sucesso, o script imprime Verified: moov precedes media; stream data, timing, and decoded audio/video match. Os hashes dos arquivos de entrada e saída geralmente diferem porque o índice é movido e os deslocamentos dos blocos de dados mudam. As verificações comparam a mídia e a temporização. Desativar a rotação automática torna consistente a comparação dos quadros decodificados; os dados auxiliares dos fluxos permitem verificar se os metadados de rotação são preservados. Um resultado correspondente não comprova que a gravação original estava intacta e não mede a reprodução no navegador.
Lidar com a ausência de saída e com falhas
A verificação do último átomo da biblioteca
retorna false quando a varredura não termina em
moov. Isso inclui arquivos que já têm o índice no início, mas também
disposições de dados não compatíveis e algumas entradas malformadas. Não interprete
false como “já otimizado” nem disponibilize um arquivo de saída inexistente.
Inspecione o arquivo de origem separadamente ou escolha outro caminho de processamento.
Metadados moov comprimidos e certos estouros dos deslocamentos dos blocos de
dados não são compatíveis. Arquivos malformados podem gerar uma exceção da biblioteca, uma exceção de
E/S ou uma exceção não verificada. Se a verificação indicar erros de decodificação ou diferenças,
preserve o arquivo de origem e não disponibilize a saída. A verificação de tamanho do utilitário
detecta cópias incompletas; tamanhos iguais, por si só, não comprovam a integridade da mídia.
Um diretório pai da saída inexistente ou sem permissão de escrita, a falta de suporte a links
físicos ou uma saída existente fazem o processo terminar com código diferente de zero. O bloco
finally remove o arquivo e o diretório temporários privados em caso de falha
normal. Uma JVM encerrada à força ou uma falha do sistema de arquivos durante a limpeza pode deixar
um diretório .qtfaststart-*; remova apenas o diretório temporário daquela execução,
depois de confirmar que seu processo parou.
Abordagens alternativas
Se o FFmpeg já faz parte do seu fluxo de processamento, a opção de início rápido do multiplexador evita adicionar uma dependência Java legada. Para a mesma entrada local, remultiplexe todos os fluxos sem recodificar:
(
cd qtfaststart-demo &&
ffmpeg -nostdin -n -i ./input.mp4 -map 0 -c copy -movflags +faststart ./output_ffmpeg.mp4
)
-n mantém uma saída existente intacta, e
-map 0 seleciona todos os fluxos. No FFmpeg 9.0.1, a recusa de um arquivo
existente retornou zero com uma mensagem de diagnóstico de erro; por isso, use um novo destino e
inspecione o resultado em vez de confiar apenas no código de saída. Os codecs precisam ser
compatíveis com o multiplexador MP4. Essa operação é uma remultiplexação: o FFmpeg pode reescrever
os metadados do contêiner e a temporização, portanto a comparação rigorosa acima se aplica à
realocação em Java, não a um teste geral de aceitação do FFmpeg.
Continuar com a entrega de MP4
Use o repositório qtfaststart-java no GitHub para inspecionar a dependência, a referência de formatos do FFmpeg para consultar as opções do multiplexador e o guia de contêineres MP4 da MDN para entender os fundamentos de contêineres e codecs. Para um fluxo de codificação mais abrangente, consulte o 🤖 Robot /video/encode da Transloadit.
