Divisão eficiente de vídeos em Java com FFmpeg e JavaCV
A divisão de um vídeo deve preservar seu áudio e a temporização da apresentação. Contar chamadas a
FFmpegFrameGrabber.grab() não mede a duração do vídeo: uma chamada pode retornar um bloco de áudio
ou um quadro de vídeo. Taxas de quadros variáveis também tornam os cálculos por contagem de quadros
pouco confiáveis.
Este exemplo usa o JavaCV para inspecionar a entrada e o executável do FFmpeg incluído nele para segmentar pacotes comprimidos. O FFmpeg controla os timestamps e a intercalação do áudio. Nenhum shell interpreta o nome do arquivo de entrada.
Por que dividir vídeos?
Os segmentos são úteis para edição, distribuição de downloads e processamento independente. Segmentos MP4 avulsos não constituem uma apresentação de streaming adaptativo; HLS ou DASH também precisa de um manifesto e de versões de mídia codificadas adequadamente.
Configurar seu ambiente Java
Para a configuração Linux testada, use o JDK 21.0.12.1 e o Maven 3.9.16.
Salve o conteúdo a seguir como pom.xml e coloque
VideoSplitter.java, disponível abaixo, em src/main/java/.
Essas versões fixadas usam JavaCV 1.5.14 e FFmpeg 8.1.2-1.5.14.
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>video-splitter</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<javacpp.platform>macosx-arm64</javacpp.platform>
</properties>
<dependencies>
<dependency>
<groupId>org.bytedeco</groupId><artifactId>javacv</artifactId><version>1.5.14</version>
<exclusions><exclusion><groupId>*</groupId><artifactId>*</artifactId></exclusion></exclusions>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId><artifactId>javacpp</artifactId><version>1.5.14</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId><artifactId>javacpp</artifactId><version>1.5.14</version>
<classifier>${javacpp.platform}</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId><artifactId>ffmpeg</artifactId><version>8.1.2-1.5.14</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId><artifactId>ffmpeg</artifactId><version>8.1.2-1.5.14</version>
<classifier>${javacpp.platform}</classifier>
</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>
Defina javacpp.platform de acordo com sua plataforma real, por exemplo,
linux-x86_64 em vez de macosx-arm64.
Isso baixa apenas os artefatos nativos necessários. Consulte as
instruções de instalação do JavaCV para ver as plataformas compatíveis.
Entender ffmpegframegrabber e ffmpegframerecorder
FFmpegFrameGrabber pode decodificar imagens e amostras.
FFmpegFrameRecorder.record(frame) codifica esses quadros decodificados novamente, mesmo que você selecione
o codec de entrada. Selecionar um codec não é o mesmo que copiar fluxos.
Para dividir sem edições visuais, o subprocesso abaixo usa o modo de processamento de pacotes
-c copy do FFmpeg. Ele preserva o primeiro fluxo de vídeo e, quando presente,
o primeiro fluxo de áudio. Faixas de áudio adicionais, legendas, capítulos, metadados de rotação e
metadados arbitrários do contêiner estão fora do escopo de preservação deste exemplo. Verifique esses
itens separadamente antes de ampliar o mapeamento de fluxos.
Guia passo a passo para dividir vídeos
Salve esta classe completa como VideoSplitter.java. O diretório de saída não deve existir
previamente, para que uma execução com falha não sobrescreva um resultado anterior. Arquivos parciais
permanecem nele para diagnóstico se o FFmpeg falhar. Use entradas íntegras e valide os segmentos
conforme descrito abaixo antes de usar o diretório de saída. O grabber verifica os metadados, e a cópia
de pacotes não decodifica a mídia por completo para validá-la.
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import org.bytedeco.javacpp.Loader;
import org.bytedeco.javacv.FFmpegFrameGrabber;
public final class VideoSplitter {
public static void split(Path input, Path output, double seconds) throws Exception {
if (!Double.isFinite(seconds) || seconds <= 0) {
throw new IllegalArgumentException("Segment duration must be positive and finite");
}
Path source = input.toRealPath();
if (!Files.isRegularFile(source)) throw new IOException("Input must be a regular file");
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(source.toFile())) {
grabber.start();
if (grabber.getImageWidth() <= 0) throw new IOException("Input has no video stream");
}
Files.createDirectory(output);
Path destination = output.toRealPath();
String executable = Loader.load(org.bytedeco.ffmpeg.ffmpeg.class);
Process process = new ProcessBuilder(
executable, "-hide_banner", "-nostdin", "-v", "error", "-xerror", "-n",
"-i", source.toString(), "-map", "0:v:0", "-map", "0:a:0?",
"-c", "copy", "-f", "segment", "-segment_format", "mp4",
"-segment_time", BigDecimal.valueOf(seconds).toPlainString(), "-reset_timestamps", "1",
"segment-%03d.mp4"
).directory(destination.toFile()).redirectErrorStream(true)
.redirectOutput(destination.resolve("ffmpeg.log").toFile()).start();
try {
if (!process.waitFor(10, TimeUnit.MINUTES)) throw new IOException("FFmpeg timed out");
if (process.exitValue() != 0) throw new IOException("FFmpeg failed; inspect ffmpeg.log");
} finally {
if (process.isAlive()) {
process.destroyForcibly();
process.waitFor();
}
}
}
public static void main(String[] args) throws Exception {
if (args.length != 3) throw new IllegalArgumentException("input output-directory seconds");
split(Path.of(args[0]), Path.of(args[1]), Double.parseDouble(args[2]));
}
}
No macOS ou Linux, compile e execute com:
mvn compile dependency:copy-dependencies &&
java -cp 'target/classes:target/dependency/*' VideoSplitter input.mp4 segments 10
O FFmpeg inicia um segmento em um quadro-chave de vídeo no limite solicitado ou após ele. Uma
solicitação de 10 segundos é uma duração-alvo, não um corte exato. O último
segmento pode ser mais curto, e quadros-chave muito espaçados podem produzir segmentos muito mais
longos. Os pacotes de áudio mantêm sua temporização relativa; seus limites não precisam coincidir
exatamente com os quadros de vídeo. Para obter limites de edição exatos, é necessário recodificar com
quadros-chave posicionados deliberadamente. Consulte a
documentação do multiplexador de segmentos.
Implementar multithreading para melhorar o desempenho
Mantenha o fluxo de pacotes de cada entrada sequencial. Iniciar vários decodificadores e contar
quadros mistos de áudio e vídeo pode descartar áudio ou duplicar conteúdo nos limites de
reposicionamento. Para arquivos de entrada independentes, use um executor com limites, dê a cada
tarefa seu próprio diretório de saída e chame Future.get() para cada tarefa submetida,
para que as falhas cheguem ao chamador. Meça a vazão antes de aumentar a concorrência: a cópia de
pacotes costuma ser limitada pelo armazenamento.
Dicas de otimização de desempenho
A cópia de fluxos evita o trabalho de codificação e a perda de qualidade entre gerações. Ela exige codecs compatíveis com o contêiner de saída; codecs não compatíveis devem gerar falhas visíveis em vez de perder um fluxo silenciosamente. Para redimensionamento, aplicação de marcas-d’água ou mudanças de codec, use um fluxo de trabalho de transcodificação separado e teste também seu comportamento de áudio e temporização.
Boas práticas de tratamento de erros
O exemplo rejeita durações não positivas ou não finitas, entradas ausentes e diretórios de saída já
existentes. As durações usam decimais simples independentes da localidade, incluindo valores pequenos
como 0.0001 segundos. A precisão e o intervalo de duração do FFmpeg ainda se
aplicam; um double Java positivo e finito, por si só, não garante uma duração
representável no FFmpeg. Erros e tempos limite excedidos são propagados como falhas.
A opção -xerror do FFmpeg interrompe a execução
quando detecta erros, incluindo pacotes corrompidos relatados pelo demultiplexador. Um retorno de
sucesso ainda exige verificações da saída: a cópia de fluxos pode repassar dados corrompidos que
apenas um decodificador detecta.
Mantenha ffmpeg.log privado, pois ele pode conter caminhos de arquivos.
Uma separação entre processos não é um sandbox: processe mídias não confiáveis com restrições de
sistema de arquivos, rede, CPU e memória impostas pelo ambiente dos seus processos de trabalho.
Dicas práticas para otimizar o processamento de vídeo
Inspecione as saídas reais, incluindo as de entradas com áudio, com taxas de quadros variáveis e sem
áudio. Com ffmpeg e ffprobe instalados separadamente,
execute o seguinte no diretório do projeto para inspecionar e decodificar cada segmento. O subshell
para na primeira verificação que falhar:
(
set -e
for segment in segments/segment-*.mp4; do
ffprobe -v error -show_streams -show_format -of json "$segment"
ffmpeg -v error -xerror -err_detect explode -i "$segment" \
-map 0:v:0 -map '0:a:0?' -f null -
done
)
Revise os diagnósticos mesmo quando um comando for bem-sucedido. Compare as contagens totais e os payloads dos pacotes de vídeo e áudio com os da entrada e confira o final esperado com uma fonte comprovadamente completa. Considere as durações dos segmentos alinhadas aos quadros-chave e os deslocamentos de timestamp do contêiner; verificar apenas se os arquivos existem não comprova a preservação.
Conclusão e recursos adicionais
Use o JavaCV para inspeção e o segmentador de pacotes do FFmpeg para uma divisão eficiente sem recodificação. Para streaming adaptativo, consulte o serviço de codificação de vídeo da Transloadit e o Robot 🤖 /video/adaptive.
