Processing videos in Java: transcode, resize, and watermark
In today's digital landscape, video content is crucial. Whether you're developing a media application or managing a video platform, efficiently processing videos is essential. This guide walks you through transcoding, resizing, and watermarking videos in Java using the open source library JavaCV, which provides Java wrappers for FFmpeg.
Prerequisites
Before we begin, ensure you have the following installed:
- Java Development Kit (JDK): Version 11 or higher
- Maven: For managing project dependencies
- FFmpeg and OpenCV native libraries, included by the
javacv-platformdependency below
Add these dependencies and test plugin configuration to your 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>
Transcoding videos in Java
Below is an example of how to transcode a video from one format to another using JavaCV. The code encodes H.264 video and AAC audio in an MP4 container. These examples require a readable local video and a new output path; existing files are never overwritten. The first video and audio streams are processed, while subtitles and additional tracks are not copied.
All three H.264 examples request a video bitrate of 5 Mbps instead of relying on JavaCV's 400 kbps default. Adjust that budget for your resolution and quality requirements; actual output bitrate depends on the encoder and content. The standard JavaCV binaries use OpenH264, so these examples do not rely on the x264-specific CRF option.
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);
}
}
}
Resizing videos using JavaCV
To resize videos, process each frame and adjust its dimensions. This example uses OpenCV with JavaCV to resize video frames while ensuring resources are properly released.
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);
}
}
}
Adding watermarks to videos
Overlaying a text watermark onto each frame can be achieved by drawing with OpenCV's functions. The example below demonstrates how to add a simple text watermark to a video using 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);
}
}
}
Memory management and performance optimization
Efficient video processing requires both optimized performance and effective memory management. In the example below, we use Java's ExecutorService to process multiple videos concurrently, taking advantage of all available processor cores.
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();
}
}
}
Testing video processing
Implement tests to verify your video processing code functions as expected. The following JUnit
tests check that a video is transcoded without errors and that proper exceptions are thrown for
invalid input files. The Maven configuration above includes JUnit Jupiter and Surefire. Put this
class in src/test/java/VideoProcessingTest.java, provide a small H.264/AAC fixture at test.mp4,
and run mvn test. Call shutdown() in a finally block when using 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());
});
}
}
Conclusion
Processing videos in Java with JavaCV provides powerful capabilities for transcoding, resizing, and watermarking. The examples above demonstrate essential techniques such as proper resource management, error handling, and performance optimization. For high-volume or enterprise-scale video processing, consider exploring Transloadit's Video Encoding service, which offers scalable and efficient solutions.
