Video en Java: transcodificación, redimensionado y marcas de agua
En el panorama digital actual, el contenido de video es fundamental. Ya sea que estés desarrollando una aplicación multimedia o administrando una plataforma de video, procesar videos de forma eficiente es esencial. Esta guía te muestra cómo transcodificar, redimensionar y agregar marcas de agua a videos en Java con la biblioteca de código abierto JavaCV, que ofrece envoltorios de Java para FFmpeg.
Requisitos previos
Antes de comenzar, asegúrate de tener instalado lo siguiente:
- Java Development Kit (JDK): versión 11 o superior
- Maven: para gestionar las dependencias del proyecto
- Las bibliotecas nativas de FFmpeg y OpenCV, incluidas por la dependencia
javacv-platformque aparece más abajo
Agrega estas dependencias y la configuración del plugin de pruebas a tu pom.xml:
<dependencies>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.11</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
</plugins>
</build>
Transcodificar videos en Java
A continuación se muestra un ejemplo de cómo transcodificar un video de un formato a otro con JavaCV. El código codifica video H.264 y audio AAC en un contenedor MP4. Estos ejemplos requieren un video local legible y una ruta de salida nueva; los archivos existentes nunca se sobrescriben. Se procesan los primeros streams de video y de audio, mientras que los subtítulos y las pistas adicionales no se copian.
Los tres ejemplos de H.264 solicitan una tasa de bits de video de 5 Mbps en lugar de depender del valor predeterminado de 400 kbps de JavaCV. Ajusta ese presupuesto según tu resolución y tus requisitos de calidad; la tasa de bits real de salida depende del codificador y del contenido. Los binarios estándar de JavaCV usan OpenH264, por lo que estos ejemplos no dependen de la opción CRF específica de x264.
import static org.bytedeco.ffmpeg.global.avcodec.AV_CODEC_ID_AAC;
import static org.bytedeco.ffmpeg.global.avcodec.AV_CODEC_ID_H264;
import static org.bytedeco.ffmpeg.global.avutil.AV_PIX_FMT_YUV420P;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import java.nio.file.Files;
import java.nio.file.Path;
public class VideoTranscoder {
public static void transcode(String inputFile, String outputFile) throws Exception {
if (!Files.isRegularFile(Path.of(inputFile))) {
throw new IllegalArgumentException("Video file not found");
}
Path output = Files.createFile(Path.of(outputFile));
boolean completed = false;
try {
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputFile)) {
grabber.start();
if (grabber.getImageWidth() <= 0 || grabber.getImageHeight() <= 0) {
throw new IllegalArgumentException("Input must contain a video stream");
}
try (FFmpegFrameRecorder recorder =
new FFmpegFrameRecorder(
outputFile,
grabber.getImageWidth(),
grabber.getImageHeight(),
grabber.getAudioChannels())) {
recorder.setVideoCodec(AV_CODEC_ID_H264);
recorder.setVideoBitrate(5_000_000);
recorder.setFormat("mp4");
recorder.setFrameRate(grabber.getFrameRate());
recorder.setPixelFormat(AV_PIX_FMT_YUV420P);
if (grabber.getAudioChannels() > 0) {
recorder.setAudioChannels(grabber.getAudioChannels());
recorder.setAudioCodec(AV_CODEC_ID_AAC);
recorder.setSampleRate(grabber.getSampleRate());
}
recorder.start();
Frame frame;
while ((frame = grabber.grab()) != null) {
if (frame.image != null) recorder.setTimestamp(frame.timestamp);
recorder.record(frame);
}
}
}
completed = true;
} finally {
if (!completed) Files.deleteIfExists(output);
}
}
}
Redimensionar videos con JavaCV
Para redimensionar videos, procesa cada fotograma y ajusta sus dimensiones. Este ejemplo usa OpenCV con JavaCV para redimensionar los fotogramas del video y liberar correctamente los recursos.
import static org.bytedeco.ffmpeg.global.avcodec.AV_CODEC_ID_AAC;
import static org.bytedeco.ffmpeg.global.avcodec.AV_CODEC_ID_H264;
import static org.bytedeco.ffmpeg.global.avutil.AV_PIX_FMT_YUV420P;
import static org.bytedeco.opencv.global.opencv_imgproc.resize;
import org.bytedeco.javacv.*;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Size;
import java.nio.file.Files;
import java.nio.file.Path;
public class VideoResizer {
public static void resizeVideo(String inputFile, String outputFile, int width, int height)
throws Exception {
if (!Files.isRegularFile(Path.of(inputFile))) {
throw new IllegalArgumentException("Video file not found");
}
if (width <= 0 || height <= 0 || width % 2 != 0 || height % 2 != 0) {
throw new IllegalArgumentException("H.264 dimensions must be positive and even");
}
Path output = Files.createFile(Path.of(outputFile));
boolean completed = false;
try {
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputFile);
OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();
Mat resizedMat = new Mat();
Size targetSize = new Size(width, height)) {
grabber.start();
if (grabber.getImageWidth() <= 0 || grabber.getImageHeight() <= 0) {
throw new IllegalArgumentException("Input must contain a video stream");
}
try (FFmpegFrameRecorder recorder =
new FFmpegFrameRecorder(
outputFile, width, height, grabber.getAudioChannels())) {
recorder.setVideoCodec(AV_CODEC_ID_H264);
recorder.setVideoBitrate(5_000_000);
recorder.setFormat("mp4");
recorder.setFrameRate(grabber.getFrameRate());
recorder.setPixelFormat(AV_PIX_FMT_YUV420P);
if (grabber.getAudioChannels() > 0) {
recorder.setAudioChannels(grabber.getAudioChannels());
recorder.setAudioCodec(AV_CODEC_ID_AAC);
recorder.setSampleRate(grabber.getSampleRate());
}
recorder.start();
Frame frame;
while ((frame = grabber.grab()) != null) {
if (frame.image != null) {
resize(converter.convert(frame), resizedMat, targetSize);
recorder.setTimestamp(frame.timestamp);
recorder.record(converter.convert(resizedMat));
} else {
recorder.record(frame);
}
}
}
}
completed = true;
} finally {
if (!completed) Files.deleteIfExists(output);
}
}
}
Agregar marcas de agua a los videos
La superposición de una marca de agua de texto en cada fotograma se puede lograr dibujando con las funciones de OpenCV. El ejemplo siguiente muestra cómo agregar una marca de agua de texto simple a un video con JavaCV.
import static org.bytedeco.ffmpeg.global.avcodec.AV_CODEC_ID_AAC;
import static org.bytedeco.ffmpeg.global.avcodec.AV_CODEC_ID_H264;
import static org.bytedeco.ffmpeg.global.avutil.AV_PIX_FMT_YUV420P;
import static org.bytedeco.opencv.global.opencv_imgproc.*;
import org.bytedeco.javacv.*;
import org.bytedeco.opencv.opencv_core.*;
import java.nio.file.Files;
import java.nio.file.Path;
public class VideoWatermarker {
public static void addWatermark(String inputFile, String outputFile, String watermarkText)
throws Exception {
if (!Files.isRegularFile(Path.of(inputFile))) {
throw new IllegalArgumentException("Video file not found");
}
Path output = Files.createFile(Path.of(outputFile));
boolean completed = false;
try {
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputFile);
OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();
Scalar color = new Scalar(255, 255, 255, 0);
Point position = new Point(50, 50)) {
grabber.start();
if (grabber.getImageWidth() <= 0 || grabber.getImageHeight() <= 0) {
throw new IllegalArgumentException("Input must contain a video stream");
}
try (FFmpegFrameRecorder recorder =
new FFmpegFrameRecorder(
outputFile,
grabber.getImageWidth(),
grabber.getImageHeight(),
grabber.getAudioChannels())) {
recorder.setVideoCodec(AV_CODEC_ID_H264);
recorder.setVideoBitrate(5_000_000);
recorder.setFormat("mp4");
recorder.setFrameRate(grabber.getFrameRate());
recorder.setPixelFormat(AV_PIX_FMT_YUV420P);
if (grabber.getAudioChannels() > 0) {
recorder.setAudioChannels(grabber.getAudioChannels());
recorder.setAudioCodec(AV_CODEC_ID_AAC);
recorder.setSampleRate(grabber.getSampleRate());
}
recorder.start();
int font = FONT_HERSHEY_SIMPLEX;
Frame frame;
while ((frame = grabber.grab()) != null) {
if (frame.image != null) {
Mat mat = converter.convert(frame);
putText(
mat,
watermarkText,
position,
font,
1.0,
color,
2,
LINE_AA,
false);
Frame watermarkedFrame = converter.convert(mat);
recorder.setTimestamp(frame.timestamp);
recorder.record(watermarkedFrame);
} else {
recorder.record(frame);
}
}
}
}
completed = true;
} finally {
if (!completed) Files.deleteIfExists(output);
}
}
}
Gestión de memoria y optimización del rendimiento
El procesamiento eficiente de videos requiere tanto un rendimiento optimizado como una gestión de memoria eficaz. En el ejemplo siguiente, usamos ExecutorService de Java para procesar varios videos de forma concurrente, aprovechando todos los núcleos de procesador disponibles.
import java.util.*;
import java.util.concurrent.*;
public class VideoProcessingService {
private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors();
private final ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);
public void processVideosInParallel(List<String> videoFiles) throws Exception {
List<Future<?>> futures = new ArrayList<>();
for (String file : videoFiles) {
futures.add(
executor.submit(
() -> {
try {
processVideo(file);
} catch (Exception e) {
throw new RuntimeException(
"Failed to process video: " + file, e);
}
}));
}
// Wait for each task to complete
for (Future<?> future : futures) {
future.get();
}
}
private void processVideo(String file) throws Exception {
VideoTranscoder.transcode(file, file + ".processed.mp4");
}
public void shutdown() {
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
Probar el procesamiento de videos
Implementa pruebas para verificar que tu código de procesamiento de video funciona como se espera.
Las siguientes pruebas de JUnit comprueban que un video se transcodifica sin errores y que se lanzan
las excepciones adecuadas para archivos de entrada no válidos. La configuración de Maven anterior
incluye JUnit Jupiter y Surefire. Coloca esta clase en src/test/java/VideoProcessingTest.java, proporciona un pequeño archivo de prueba H.264/AAC en test.mp4
y ejecuta mvn test. Llama a shutdown() en un bloque finally cuando uses VideoProcessingService.
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
public class VideoProcessingTest {
@TempDir File temporaryDirectory;
@Test
public void testVideoTranscoding() {
File input = new File("test.mp4");
File output = new File(temporaryDirectory, "output.mp4");
assertDoesNotThrow(
() -> {
VideoTranscoder.transcode(input.getPath(), output.getPath());
assertTrue(output.exists());
assertTrue(output.length() > 0);
});
}
@Test
public void testInvalidVideoFile() {
assertThrows(
IllegalArgumentException.class,
() -> {
VideoTranscoder.transcode(
new File(temporaryDirectory, "nonexistent.mp4").getPath(),
new File(temporaryDirectory, "output.mp4").getPath());
});
}
}
Conclusión
Procesar videos en Java con JavaCV ofrece capacidades potentes para transcodificar, redimensionar y agregar marcas de agua. Los ejemplos anteriores muestran técnicas esenciales como la gestión adecuada de los recursos, el manejo de errores y la optimización del rendimiento. Para el procesamiento de videos de alto volumen o a escala empresarial, considera explorar el servicio de Encoding de video de Transloadit, que ofrece soluciones escalables y eficientes.
