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 conteo de fotogramas sean poco fiables.
Este ejemplo usa JavaCV para inspeccionar la entrada y el ejecutable de FFmpeg incluido para dividir paquetes comprimidos. FFmpeg se encarga de las marcas de tiempo y del 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 editar, distribuir descargas y procesar contenido de forma independiente. Los segmentos MP4 independientes no constituyen una presentación de streaming adaptativo; HLS o DASH requieren también un manifiesto y variantes con el encoding adecuado.
Configura tu entorno Java
Para la configuración probada en Linux, usa JDK 21.0.12.1 y Maven 3.9.16.
Guarda esto como pom.xml y luego coloca
VideoSplitter.java, que aparece más abajo, en src/main/java/.
Estas versiones fijas 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.
Así se descargan 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, cuando 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 se
compromete a conservar. Verifica esos elementos 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, de modo que una ejecución fallida no pueda sobrescribir un resultado anterior. Si FFmpeg
falla, los archivos parciales permanecen allí para el diagnóstico. Usa entradas intactas y valida los
segmentos como se indica más abajo antes de usar el directorio de salida.
El capturador comprueba los metadatos, y la copia de paquetes no decodifica por completo el contenido
multimedia para validarlo.
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]));
}
}
En macOS o Linux, compílala y ejecútala 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 del video situado en el límite solicitado o después
de él. 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 que
coincidir exactamente con los fotogramas del 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 pérdida de audio o duplicación de contenido en
los límites de los saltos temporales. 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
la invoca. Mide la tasa de procesamiento 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 encoding y la pérdida de calidad por generació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 separado y prueba también su comportamiento con respecto al audio y los tiempos.
Buenas prácticas para gestionar errores
El ejemplo rechaza las duraciones no positivas o no finitas, las entradas que no existen y los
directorios de salida existentes. 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 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.
La opción -xerror de FFmpeg detiene el proceso
ante errores detectados, incluidos los paquetes dañados que notifica el demultiplexor. Aunque el
proceso termine correctamente, sigue siendo necesario comprobar la salida: la copia de flujos puede
transferir datos dañados que solo detecta un decodificador.
Mantén ffmpeg.log privado porque puede contener rutas de archivos.
Un límite de proceso no es un entorno aislado de seguridad: procesa el contenido multimedia no
confiable con restricciones de sistema de archivos, red, CPU y memoria impuestas por tu entorno de
procesamiento.
Consejos prácticos para optimizar el procesamiento de video
Inspecciona las salidas reales, incluidas las de entradas con audio, con tasas de fotogramas variables
y sin audio. Con ffmpeg y ffprobe instalados por
separado, ejecuta lo siguiente desde el directorio del proyecto para inspeccionar y decodificar cada
segmento. El subintérprete de comandos se detiene en la primera comprobación fallida:
(
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
)
Revisa los diagnósticos incluso cuando un comando termine correctamente. Compara los conteos totales de paquetes de video y audio y sus cargas útiles con los de la entrada, y verifica el final esperado con una fuente que sepas que está completa. Ten en cuenta las duraciones de los segmentos alineados con fotogramas clave y los desplazamientos de las marcas de tiempo del contenedor; comprobar únicamente que los archivos existen no permite determinar que el contenido se haya conservado.
Conclusión y recursos adicionales
Usa JavaCV para inspeccionar 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.
