División eficiente de videos en Java con FFmpeg y JavaCV
Al dividir un video, se deben conservar su audio y sus tiempos de presentación. Contar las llamadas a
FFmpegFrameGrabber.grab() no mide la duración del video: una llamada puede devolver un bloque de
audio o un fotograma de video. Las tasas de fotogramas variables también hacen que los cálculos
basados en el número de fotogramas sean poco fiables.
Este ejemplo usa JavaCV para inspeccionar la entrada y el ejecutable de FFmpeg incluido para dividir los paquetes comprimidos. FFmpeg gestiona las marcas de tiempo y el intercalado del audio. Ningún intérprete de comandos interpreta el nombre del archivo de entrada.
¿Por qué dividir videos?
Los segmentos son útiles para la edición, la distribución de descargas y el procesamiento independiente. Los segmentos MP4 independientes no constituyen una presentación de streaming adaptativo; HLS o DASH también requieren un manifiesto y variantes codificadas adecuadamente.
Configura tu entorno Java
Usa JDK 21 y Maven. Guarda esto como pom.xml y luego coloca
VideoSplitter.java, que aparece más abajo, en src/main/java/.
Estas versiones fijadas usan JavaCV 1.5.14 y 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>
Establece javacpp.platform según tu plataforma real, por ejemplo,
linux-x86_64 en lugar de macosx-arm64.
Esto descarga solo los artefactos nativos necesarios. Consulta las
instrucciones de instalación de JavaCV para conocer las plataformas compatibles.
Comprende ffmpegframegrabber y ffmpegframerecorder
FFmpegFrameGrabber puede decodificar imágenes y muestras. FFmpegFrameRecorder.record(frame)
vuelve a codificar esos fotogramas decodificados, incluso si seleccionas el códec de entrada.
Seleccionar un códec no equivale a copiar flujos.
Para dividir sin modificaciones visuales, el subproceso siguiente usa la ruta de procesamiento de
paquetes -c copy de FFmpeg. Conserva el primer flujo de video y, si existe,
el primer flujo de audio. Las pistas de audio adicionales, los subtítulos, los capítulos, los
metadatos de rotación y los metadatos arbitrarios del contenedor quedan fuera de lo que este ejemplo
garantiza conservar. Verifícalos por separado antes de ampliar el mapeo de flujos.
Guía paso a paso para dividir videos
Guarda esta clase completa como VideoSplitter.java. El directorio de salida no debe
existir previamente, para que una ejecución fallida no pueda sobrescribir un resultado anterior.
Si FFmpeg falla, los archivos parciales permanecen allí para facilitar el diagnóstico; usa el
directorio solo después de que este método finalice correctamente.
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]));
}
}
En macOS o Linux, compila y ejecuta la clase con:
mvn compile dependency:copy-dependencies
java -cp 'target/classes:target/dependency/*' VideoSplitter input.mp4 segments 10
FFmpeg inicia un segmento en un fotograma clave de video situado en el límite solicitado o después.
Una solicitud de 10 segundos es un objetivo, no un corte exacto.
El segmento final puede ser más corto, y los fotogramas clave muy separados pueden producir segmentos
mucho más largos. Los paquetes de audio conservan sus tiempos relativos; sus límites no tienen por
qué coincidir exactamente con los fotogramas de video. Para obtener límites de edición exactos,
es necesario volver a codificar con fotogramas clave colocados deliberadamente. Consulta la
documentación del multiplexor de segmentos.
Implementa múltiples hilos para mejorar el rendimiento
Mantén secuencial el flujo de paquetes de cada entrada. Iniciar varios decodificadores y contar
fotogramas que mezclan audio y video puede provocar la pérdida de audio o la duplicación de contenido
en los límites de los saltos de posición. Para archivos de entrada independientes, usa un ejecutor
con concurrencia limitada, asigna a cada tarea su propio directorio de salida y llama a
Future.get() para cada tarea enviada, de modo que los fallos lleguen al código
que hizo la llamada. Mide el rendimiento antes de aumentar la concurrencia: el almacenamiento suele
limitar la copia de paquetes.
Consejos para optimizar el rendimiento
La copia de flujos evita el trabajo de codificación y la pérdida de calidad por recodificación. Requiere códecs compatibles con el contenedor de salida; los códecs no compatibles deben provocar un fallo visible en lugar de perder un flujo silenciosamente. Para cambiar el tamaño, añadir marcas de agua o cambiar de códec, usa un flujo de trabajo de transcodificación independiente y comprueba también cómo maneja el audio y los tiempos.
Buenas prácticas para gestionar errores
El ejemplo rechaza duraciones no positivas o no finitas, entradas inexistentes y directorios de
salida que ya existen. Las duraciones usan decimales simples independientes de la configuración
regional, incluidos valores pequeños como 0.0001 segundos.
Siguen aplicándose la precisión y el rango de duración de FFmpeg; un valor
double de Java positivo y finito no garantiza por sí solo una duración
representable en FFmpeg. Los errores y los tiempos de espera agotados se propagan como fallos.
Mantén ffmpeg.log privado porque puede contener rutas de archivos.
Separar un proceso no crea un entorno aislado: ejecuta contenido multimedia no confiable con
restricciones de sistema de archivos, red, CPU y memoria impuestas por tu entorno de ejecución.
Consejos prácticos para optimizar el procesamiento de video
Inspecciona los resultados reales, incluidos los de entradas con audio, con tasas de fotogramas variables y sin audio. Con una CLI de FFmpeg instalada por separado:
ffprobe -v error -show_streams -show_format -of json segments/segment-000.mp4
ffmpeg -v error -i segments/segment-000.mp4 -f null -
Inspecciona cada segmento y compara los recuentos totales de paquetes de video y audio, así como sus datos de carga útil, con los de la entrada. Ten en cuenta las duraciones de los segmentos alineados con fotogramas clave y los desplazamientos de las marcas de tiempo del contenedor; comprobar solo que los archivos existen no permite confirmar que se haya conservado el contenido.
Conclusión y recursos adicionales
Usa JavaCV para la inspección y el segmentador de paquetes de FFmpeg para dividir de forma eficiente sin volver a codificar. Para streaming adaptativo, consulta el servicio de encoding de video de Transloadit y el 🤖 Robot /video/adaptive.
