Efficient video splitting in Java with FFmpeg and JavaCV

Splitting videos into segments is a common requirement in video processing workflows. Whether you're preparing videos for streaming, creating highlights, or managing large video files, efficient splitting can significantly enhance your application's performance.
In this DevTip, we'll explore how to efficiently split videos using Java, leveraging the powerful combination of FFmpeg and JavaCV. We'll also demonstrate how multithreading can further boost performance in your Java video processing tasks.
Why split videos?
Splitting videos into smaller segments offers several advantages:
- Improved streaming performance: Enables adaptive bitrate streaming by providing smaller chunks that can be switched based on network conditions.
- Easier editing and processing: Smaller files are simpler to manage and manipulate.
- Parallel processing: Allows multiple segments to be processed simultaneously, significantly reducing overall processing time.
Setting up your Java environment
We'll use Maven to manage dependencies. Add the latest stable version of JavaCV (1.5.11 as of
late 2024) to your pom.xml
:
<dependencies>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.11</version> <!-- Use the latest stable version -->
</dependency>
</dependencies>
Ensure you have a compatible Java Development Kit (JDK) installed (Java 8 or later is recommended
for JavaCV 1.5.x). FFmpeg libraries are bundled with javacv-platform
, so a separate FFmpeg
installation is typically not required.
Understanding ffmpegframegrabber and ffmpegframerecorder
JavaCV provides convenient Java wrappers around FFmpeg's libraries:
FFmpegFrameGrabber
: Used to read and decode video files, allowing you to extract individual frames or access stream information (like codecs, frame rate, dimensions).FFmpegFrameRecorder
: Used to encode and write frames to new video files, enabling the creation of video segments with specified formats and codecs.
Step-by-step guide to splitting videos
Here's a practical example demonstrating how to split a video into segments of a specified duration (e.g., 10 seconds) using JavaCV. This implementation includes proper resource management using try-with-resources and copies existing codec information.
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import java.io.File;
public class VideoSplitter {
public static void splitVideo(String inputPath, String outputPrefix, int segmentDurationSeconds) throws Exception {
// Basic input validation
File inputFile = new File(inputPath);
if (!inputFile.exists() || !inputFile.canRead()) {
throw new IllegalArgumentException("Input file does not exist or cannot be read: " + inputPath);
}
if (segmentDurationSeconds <= 0) {
throw new IllegalArgumentException("Segment duration must be positive.");
}
File outputDir = new File(outputPrefix).getParentFile();
if (outputDir != null && !outputDir.exists()) {
if (!outputDir.mkdirs()) {
System.err.println("Warning: Could not create output directory: " + outputDir.getAbsolutePath());
// Decide if this is a critical error
}
}
// Use try-with-resources for automatic resource management
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputPath)) {
grabber.start(); // Open the input video file
double frameRate = grabber.getFrameRate();
// Fallback if frame rate is not directly available
if (frameRate <= 0) {
frameRate = (double) grabber.getLengthInFrames() / (grabber.getLengthInTime() / 1_000_000.0);
if (frameRate <= 0) {
throw new RuntimeException("Could not determine frame rate for input video.");
}
}
int framesPerSegment = (int) Math.round(frameRate * segmentDurationSeconds);
if (framesPerSegment <= 0) {
throw new IllegalArgumentException("Calculated frames per segment is zero or negative. Check frame rate and segment duration.");
}
int segmentCount = 0;
int frameCount = 0;
FFmpegFrameRecorder recorder = null;
Frame frame;
try {
// Grab frames one by one
while ((frame = grabber.grab()) != null) {
// Start a new segment recorder if needed
if (frameCount % framesPerSegment == 0) {
if (recorder != null) {
recorder.stop();
recorder.close(); // Close previous recorder
}
String segmentFile = String.format("%s_segment_%d.mp4", outputPrefix, segmentCount++);
System.out.println("Creating segment: " + segmentFile);
// Create and configure the recorder for the new segment
recorder = new FFmpegFrameRecorder(segmentFile,
grabber.getImageWidth(),
grabber.getImageHeight(),
grabber.getAudioChannels());
// Copy settings from grabber to recorder
recorder.setFormat("mp4");
recorder.setFrameRate(frameRate);
recorder.setSampleRate(grabber.getSampleRate());
recorder.setVideoCodec(grabber.getVideoCodec());
recorder.setAudioCodec(grabber.getAudioCodec());
recorder.setVideoBitrate(grabber.getVideoBitrate());
recorder.setAudioBitrate(grabber.getAudioBitrate());
// Add other relevant settings if needed (e.g., pixel format)
recorder.start();
}
// Record the grabbed frame to the current segment
if (recorder != null) {
// Important: record() can throw exceptions
recorder.record(frame);
}
frameCount++;
}
} finally {
// Ensure the last recorder is stopped and closed properly
if (recorder != null) {
try {
recorder.stop();
recorder.close();
} catch (FFmpegFrameRecorder.Exception e) {
System.err.println("Error closing the final recorder: " + e.getMessage());
}
}
}
} // grabber is closed automatically by try-with-resources
}
public static void main(String[] args) {
try {
// Example usage: Ensure input.mp4 exists and output directory is writable
splitVideo("input.mp4", "output/segment", 10);
System.out.println("Video splitting completed successfully.");
} catch (Exception e) {
System.err.println("Video splitting failed: " + e.getMessage());
e.printStackTrace();
}
}
}
Note: This basic sequential approach is simple but can be slow for long videos. For better performance, consider the multithreaded approach below.
Implementing multithreading for improved performance
Multithreading allows processing multiple segments concurrently, which can drastically reduce the total processing time, especially on multi-core systems. Each thread handles the grabbing and recording for a specific segment range.
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import java.io.File;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
public class MultithreadedVideoSplitter {
public static void splitVideoMultithreaded(String inputPath, String outputPrefix, int segmentDurationSeconds) throws Exception {
// Input validation (similar to the basic splitter)
File inputFile = new File(inputPath);
if (!inputFile.exists() || !inputFile.canRead()) {
throw new IllegalArgumentException("Input file does not exist or cannot be read: " + inputPath);
}
if (segmentDurationSeconds <= 0) {
throw new IllegalArgumentException("Segment duration must be positive.");
}
File outputDir = new File(outputPrefix).getParentFile();
if (outputDir != null && !outputDir.exists()) {
if (!outputDir.mkdirs()) {
System.err.println("Warning: Could not create output directory: " + outputDir.getAbsolutePath());
}
}
int totalFrames;
double frameRate;
int imageWidth, imageHeight, audioChannels, videoCodec, audioCodec, sampleRate, audioBitrate, videoBitrate;
// First pass to get essential video properties using try-with-resources
try (FFmpegFrameGrabber initialGrabber = new FFmpegFrameGrabber(inputPath)) {
initialGrabber.start();
totalFrames = initialGrabber.getLengthInFrames();
frameRate = initialGrabber.getFrameRate();
if (frameRate <= 0) { // Fallback calculation
frameRate = (double) totalFrames / (initialGrabber.getLengthInTime() / 1_000_000.0);
if (frameRate <= 0) {
throw new RuntimeException("Could not determine frame rate for input video.");
}
}
imageWidth = initialGrabber.getImageWidth();
imageHeight = initialGrabber.getImageHeight();
audioChannels = initialGrabber.getAudioChannels();
videoCodec = initialGrabber.getVideoCodec();
audioCodec = initialGrabber.getAudioCodec();
sampleRate = initialGrabber.getSampleRate();
audioBitrate = initialGrabber.getAudioBitrate();
videoBitrate = initialGrabber.getVideoBitrate();
}
int framesPerSegment = (int) Math.round(frameRate * segmentDurationSeconds);
if (framesPerSegment <= 0) {
throw new IllegalArgumentException("Calculated frames per segment is zero or negative.");
}
int totalSegments = (int) Math.ceil((double) totalFrames / framesPerSegment);
// Determine optimal thread pool size
int numThreads = Math.min(Runtime.getRuntime().availableProcessors(), totalSegments > 0 ? totalSegments : 1);
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
System.out.println("Using " + numThreads + " threads for " + totalSegments + " segments.");
List<Future<?>> futures = new ArrayList<>();
try {
for (int i = 0; i < totalSegments; i++) {
final int segmentIndex = i;
final int startFrame = segmentIndex * framesPerSegment;
// Calculate the number of frames for this specific segment (last segment might be shorter)
final int framesInThisSegment = Math.min(framesPerSegment, totalFrames - startFrame);
// Submit a task for each segment
Future<?> future = executor.submit(() -> {
try {
// Pass all necessary parameters to the segment processing method
splitSegment(inputPath, outputPrefix, segmentIndex, startFrame, framesInThisSegment,
frameRate, imageWidth, imageHeight, audioChannels, videoCodec, audioCodec,
sampleRate, audioBitrate, videoBitrate);
} catch (Exception e) {
System.err.println("ERROR: Failed to process segment " + segmentIndex + ": " + e.getMessage());
// Wrap exception to be caught later when calling future.get()
throw new RuntimeException("Segment processing failed for index " + segmentIndex, e);
}
});
futures.add(future);
}
// Wait for all tasks to complete and check for errors
int failedSegments = 0;
for (Future<?> f : futures) {
try {
f.get(); // Wait for completion and throw exception if the task failed
} catch (Exception e) {
System.err.println("Caught exception from segment task: " + e.getMessage());
failedSegments++;
// Optionally, decide to stop all processing if one segment fails critically
}
}
if (failedSegments > 0) {
System.err.println(failedSegments + " segment(s) failed to process.");
// Optionally throw an exception here to indicate overall failure
// throw new RuntimeException(failedSegments + " segment(s) failed.");
}
} finally {
// Properly shut down the executor service
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.MINUTES)) { // Wait up to an hour
System.err.println("Executor did not terminate in the specified time.");
List<Runnable> droppedTasks = executor.shutdownNow();
System.err.println("Executor was forcefully shut down. " + droppedTasks.size() + " tasks were not started.");
}
} catch (InterruptedException ie) {
System.err.println("Executor termination interrupted.");
executor.shutdownNow();
Thread.currentThread().interrupt(); // Preserve interrupt status
}
}
System.out.println("Multithreaded video splitting process finished.");
}
// Method to process a single segment
private static void splitSegment(String inputPath, String outputPrefix, int segmentIndex,
int startFrame, int framesToProcess, double frameRate,
int imageWidth, int imageHeight, int audioChannels,
int videoCodec, int audioCodec, int sampleRate,
int audioBitrate, int videoBitrate) throws Exception {
String segmentFile = String.format("%s_segment_%d.mp4", outputPrefix, segmentIndex);
System.out.println("Processing segment " + segmentIndex + " -> " + segmentFile + " (Frames: " + startFrame + " to " + (startFrame + framesToProcess - 1) + ")");
// Each thread needs its own grabber and recorder instance
// Use try-with-resources for automatic cleanup
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputPath);
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(segmentFile, imageWidth, imageHeight, audioChannels)) {
grabber.start();
// Seek to the start frame. Note: Seeking might not be perfectly accurate.
// For precise splitting, grabbing from the start might be needed, but is slower.
try {
grabber.setFrameNumber(startFrame);
} catch (Exception e) {
System.err.println("Warning: Failed to seek to frame " + startFrame + " for segment " + segmentIndex + ". Proceeding by grabbing frames.");
// Fallback: Grab frames until startFrame is reached (less efficient)
for (int i = 0; i < startFrame; i++) {
if (grabber.grab() == null) {
throw new RuntimeException("Reached end of stream before reaching start frame for segment " + segmentIndex);
}
}
}
// Configure the recorder
recorder.setFormat("mp4");
recorder.setFrameRate(frameRate);
recorder.setSampleRate(sampleRate);
recorder.setVideoCodec(videoCodec);
recorder.setAudioCodec(audioCodec);
recorder.setVideoBitrate(videoBitrate);
recorder.setAudioBitrate(audioBitrate);
recorder.start();
Frame frame;
for (int i = 0; i < framesToProcess; i++) {
frame = grabber.grab(); // Grab video or audio frame
if (frame == null) {
System.err.println("Warning: Reached end of stream unexpectedly while processing segment " + segmentIndex + " at frame " + (startFrame + i));
break; // Stop processing this segment if stream ends early
}
// Record the frame
recorder.record(frame);
}
// Recorder and grabber are closed automatically by try-with-resources
System.out.println("Finished processing segment " + segmentIndex);
} catch (Exception e) {
System.err.println("Error processing segment " + segmentIndex + ": " + e.getMessage());
// Attempt to clean up the potentially incomplete/corrupt segment file
File failedSegment = new File(segmentFile);
if (failedSegment.exists()) {
if (failedSegment.delete()) {
System.out.println("Cleaned up failed segment file: " + segmentFile);
} else {
System.err.println("Warning: Failed to delete incomplete segment file: " + segmentFile);
}
}
throw e; // Re-throw the exception to be caught by the executor handling
}
}
public static void main(String[] args) {
try {
// Example usage
splitVideoMultithreaded("input.mp4", "output/segment", 10);
} catch (Exception e) {
System.err.println("Multithreaded video splitting failed: " + e.getMessage());
e.printStackTrace();
}
}
}
Performance optimization tips
Optimizing video splitting involves several factors:
-
Segment Size Selection
- Choose segment duration based on your specific use case.
- Shorter segments (e.g., 5-10 seconds) are generally better for adaptive streaming (like HLS or DASH).
- Longer segments (e.g., 30 seconds or more) might be suitable for archival or batch processing tasks where startup overhead per segment is a concern.
-
Thread Pool Configuration
- Limit the number of threads to the number of available CPU cores
(
Runtime.getRuntime().availableProcessors()
) to avoid excessive context switching, unless I/O is the primary bottleneck. - Consider I/O limitations. If reading from or writing to slow storage (like a network drive or traditional HDD), increasing threads beyond CPU cores might not yield performance gains.
- Monitor memory usage. Each thread with its grabber and recorder consumes memory. Ensure your system has enough RAM.
- Limit the number of threads to the number of available CPU cores
(
-
FFmpeg Settings
- Explore hardware acceleration (e.g., NVENC, QSV, VideoToolbox) if supported by your hardware
and FFmpeg build. This requires specific configuration in
FFmpegFrameGrabber
andFFmpegFrameRecorder
(e.g., setting video codec options). - Copying codecs (
grabber.getVideoCodec()
,grabber.getAudioCodec()
) is significantly faster than re-encoding but requires the output container (MP4) to support the input codecs. Re-encoding offers more control over output format and quality but is CPU-intensive. - Balance quality versus speed if re-encoding. Higher quality settings or complex filters will increase processing time per segment.
- Explore hardware acceleration (e.g., NVENC, QSV, VideoToolbox) if supported by your hardware
and FFmpeg build. This requires specific configuration in
Error handling best practices
Robust error handling is crucial for reliable video processing:
-
Resource Management
- Always use try-with-resources for
FFmpegFrameGrabber
andFFmpegFrameRecorder
to ensure native resources are released correctly, even if errors occur. - Handle potential exceptions during
start()
,grab()
,record()
,stop()
, andclose()
calls within try-catch blocks where appropriate. - In multithreaded scenarios, ensure the
ExecutorService
is properly shut down usingshutdown()
andawaitTermination()
, handling potentialInterruptedException
.
- Always use try-with-resources for
-
Input Validation
- Check if the input video file exists and is readable before starting.
- Validate parameters like segment duration (must be positive).
- Verify that the output directory exists and has write permissions, or attempt to create it.
// Example Input Validation (can be integrated into the main methods) public static void validateInputs(String inputPath, String outputPrefix, int segmentDuration) throws IllegalArgumentException { File inputFile = new File(inputPath); if (!inputFile.exists() || !inputFile.canRead()) { throw new IllegalArgumentException("Input file does not exist or cannot be read: " + inputPath); } File outputDir = new File(outputPrefix).getParentFile(); if (outputDir != null) { // If outputPrefix includes a path if (!outputDir.exists()) { if (!outputDir.mkdirs()) { // Consider if this is a fatal error for your application System.err.println("Warning: Could not create output directory: " + outputDir.getAbsolutePath()); } } if (!outputDir.isDirectory()) { throw new IllegalArgumentException("Output path's parent is not a directory: " + outputDir); } if (!outputDir.canWrite()) { throw new IllegalArgumentException("Cannot write to output directory: " + outputDir); } } if (segmentDuration <= 0) { throw new IllegalArgumentException("Segment duration must be positive."); } }
-
Recovery Strategies
- Implement retry logic for transient failures (e.g., temporary I/O errors), possibly with exponential backoff, though less common for local file processing.
- Log detailed error information (stack traces, segment index, timestamps) using a logging framework (like Logback or Log4j) to aid debugging.
- In case of failure during segment creation, ensure any partially created output files are
deleted to avoid confusion or corruption, as shown in the
splitSegment
example's catch block. - For multithreaded processing, collect results (or exceptions) from all
Future
objects to get an overall status of the splitting job.
Practical tips for optimizing video processing
Beyond the code itself, consider these environmental and strategic factors:
- Use fast storage like SSDs or NVMe drives for input and output files to minimize I/O wait times, especially crucial for multithreaded performance.
- Adjust the thread pool size based on empirical testing with your specific hardware, workload, and I/O capabilities.
- Consider running heavy video processing tasks on dedicated machines or cloud instances optimized for compute workloads (high CPU cores, ample RAM).
- If applicable, pre-process videos (e.g., format normalization) before splitting to simplify the splitting logic.
- Use a Java profiler (like JProfiler, YourKit, or the built-in VisualVM) to identify performance bottlenecks in your code (CPU, memory allocation, I/O wait times, thread contention).
Conclusion and additional resources
Efficiently splitting videos in Java using FFmpeg and JavaCV can significantly improve your video processing workflows. By leveraging multithreading and implementing robust error handling and resource management, you can build fast, reliable applications capable of handling demanding video tasks.
For more complex video manipulation and encoding needs at scale, consider exploring cloud-based media processing services. Transloadit, for example, utilizes FFmpeg extensively in its video encoding service, powering versatile Robots like 🤖 /video/encode for transcoding and 🤖 /video/adaptive for creating adaptive bitrate streaming formats.
For further details on the tools used: