Divisão eficiente de vídeos em Java com FFmpeg e JavaCV
Dividir um vídeo exige preservar o áudio e a temporização de 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 pouco confiável o cálculo
baseado na contagem de quadros.
Este exemplo usa o JavaCV para inspecionar a entrada e o executável do FFmpeg incluído nele para dividir pacotes comprimidos. O FFmpeg cuida dos timestamps e do entrelaçamento do áudio. Nenhum shell interpreta o nome do arquivo de entrada.
Por que dividir vídeos?
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 precisam de um manifesto e de versões codificadas adequadamente.
Configurando seu ambiente Java
Use o JDK 21 e o Maven. Salve isto como pom.xml e depois coloque o VideoSplitter.java mostrado abaixo em
src/main/java/. Essas versões fixadas usam o JavaCV 1.5.14 e o 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 para a sua plataforma real, por exemplo linux-x86_64 em vez de
macosx-arm64. Assim, apenas os artefatos nativos necessários são baixados. Consulte as
instruções de instalação do JavaCV para ver as plataformas compatíveis.
Entendendo o ffmpegframegrabber e o ffmpegframerecorder
O FFmpegFrameGrabber pode decodificar imagens e amostras. O FFmpegFrameRecorder.record(frame) codifica
novamente esses quadros decodificados, mesmo que você selecione o codec da entrada. Selecionar um
codec não é o mesmo que copiar o fluxo.
Para dividir sem edições visuais, o subprocesso abaixo usa o caminho de pacotes -c copy do FFmpeg. Ele
preserva o primeiro fluxo de vídeo e, quando existir, o primeiro fluxo de áudio. Faixas de áudio
adicionais, legendas, capítulos, metadados de rotação e metadados arbitrários do contêiner ficam
fora do que este exemplo se compromete a preservar. 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 pode existir previamente, para
que uma execução com falha não sobrescreva um resultado anterior. Se o FFmpeg falhar, os arquivos
parciais permanecem lá para diagnóstico; use o diretório somente depois que este método retornar
com sucesso.
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", "-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 no 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 depois dele. Uma
solicitação de 10 segundos é uma meta, não um corte exato. O segmento final pode ser mais
curto, e quadros-chave muito espaçados podem gerar segmentos bem 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 limites editoriais exatos, é necessário recodificar com quadros-chave posicionados
deliberadamente. Consulte a
documentação do muxer segment.
Implementando multithreading para melhorar o desempenho
Mantenha sequencial o fluxo de pacotes de cada entrada. Iniciar vários decodificadores e contar
quadros mistos de áudio e vídeo pode descartar áudio ou duplicar conteúdo nos limites de
posicionamento (seek). Para arquivos de entrada independentes, use um executor limitado, dê a cada
tarefa seu próprio diretório de saída e chame Future.get() para cada tarefa enviada, para que as falhas
cheguem a quem fez a chamada. 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 fluxo evita o trabalho de codificação e a perda de qualidade causada pela recodificação. Ela exige codecs compatíveis com o contêiner de saída; codecs sem suporte devem falhar de forma visível em vez de perder um fluxo silenciosamente. Para redimensionamento, aplicação de marca-d’água ou troca de codec, use um processo de transcodificação separado e teste também o comportamento de áudio e de temporização dele.
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 de localidade, inclusive valores
pequenos como 0.0001 segundos. A precisão e o intervalo de duração do FFmpeg continuam valendo; um double
Java positivo e finito, por si só, não garante uma duração representável no FFmpeg. Erros e timeouts
se propagam como falhas. Mantenha ffmpeg.log privado, porque ele pode conter caminhos de arquivos.
Executar o FFmpeg como um processo separado não é, por si só, um sandbox de segurança: execute
mídias não confiáveis com restrições de sistema de arquivos, rede, CPU e memória impostas pelo
ambiente de execução dos seus workers.
Dicas práticas para otimizar o processamento de vídeo
Inspecione as saídas reais, inclusive de entradas com áudio, com taxa de quadros variável e sem áudio. Com uma CLI do FFmpeg instalada separadamente:
ffprobe -v error -show_streams -show_format -of json segments/segment-000.mp4
ffmpeg -v error -i segments/segment-000.mp4 -f null -
Inspecione cada segmento e compare a contagem total de pacotes de vídeo e áudio e seus payloads com a entrada. Leve em conta durações de segmento alinhadas a quadros-chave e 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, conheça o serviço de codificação de vídeo e o Robot 🤖 /video/adaptive da Transloadit.
