Efficient video splitting in Java with FFmpeg and JavaCV
Splitting a video must preserve its audio and presentation timing. Counting calls to
FFmpegFrameGrabber.grab() does not measure video duration: a call can return an audio block or a
video frame. Variable frame rates make frame-count arithmetic unreliable too.
This example uses JavaCV to inspect the input and its bundled FFmpeg executable to split compressed packets. FFmpeg owns the timestamps and audio interleaving. No shell interprets the input filename.
Why split videos?
Segments are useful for editing, distributing downloads, and independent processing. Standalone MP4 segments do not constitute an adaptive streaming presentation; HLS or DASH also needs a manifest and appropriately encoded renditions.
Setting up your Java environment
For the tested Linux setup, use JDK 21.0.12.1 and Maven 3.9.16. Save this as pom.xml, then put
VideoSplitter.java from below in src/main/java/. These pins use JavaCV 1.5.14 and
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>
Set javacpp.platform to your actual platform, for example linux-x86_64 instead of
macosx-arm64. This downloads only the needed native artifacts. Check the
JavaCV installation instructions for supported platforms.
Understanding ffmpegframegrabber and ffmpegframerecorder
FFmpegFrameGrabber can decode images and samples. FFmpegFrameRecorder.record(frame) encodes
those decoded frames again, even if you select the input codec. Selecting a codec is not stream copying.
For splitting without visual edits, the subprocess below uses FFmpeg’s -c copy packet path. It
preserves the first video stream and, when present, the first audio stream. Additional audio tracks,
subtitles, chapters, rotation metadata, and arbitrary container metadata are outside this example’s
preservation contract. Verify those separately before extending the stream mapping.
Step-by-step guide to splitting videos
Save this complete class as VideoSplitter.java. The output directory must not already exist, so a
failed run cannot overwrite an earlier result. Partial files remain there for diagnosis if FFmpeg
fails. Use intact inputs, and validate the segments below before using the output directory.
The grabber checks metadata, and packet copying does not fully decode the media to validate it.
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]));
}
}
On macOS or Linux, compile and run it with:
mvn compile dependency:copy-dependencies &&
java -cp 'target/classes:target/dependency/*' VideoSplitter input.mp4 segments 10
FFmpeg starts a segment at a video keyframe at or after the requested boundary. A 10-second
request is a target, not an exact cut. The final segment may be shorter, and widely spaced keyframes
may produce much longer segments. Audio packets retain their relative timing; their boundaries need
not coincide exactly with video frames. Re-encoding with deliberately placed keyframes is required
for exact editorial boundaries. See the
segment muxer documentation.
Implementing multithreading for improved performance
Keep each input’s packet stream sequential. Starting several decoders and counting mixed audio/video
frames can drop audio or duplicate content at seek boundaries. For independent input files, use a
bounded executor, give each job its own output directory, and call Future.get() for every submitted
job so failures reach the caller. Measure throughput before increasing concurrency: packet copying
is often limited by storage.
Performance optimization tips
Stream copying avoids encoding work and generation loss. It requires codecs compatible with the output container; unsupported codecs should fail visibly instead of silently losing a stream. For resizing, watermarking, or codec changes, use a separate transcoding workflow and test its audio and timing behavior too.
Error handling best practices
The example rejects nonpositive or nonfinite durations, missing inputs, and existing output
directories. Durations use locale-independent plain decimals, including small values such as
0.0001 seconds. FFmpeg’s duration precision and range still apply; a positive finite Java double
alone does not guarantee a representable FFmpeg duration. Errors and timeouts propagate as failures.
FFmpeg’s -xerror option stops on detected errors,
including corrupt packets reported by the demuxer. A successful return still requires output checks:
stream copying can pass through damage that only a decoder detects.
Keep ffmpeg.log private because it can contain file paths.
A process boundary is not a sandbox: run untrusted media with filesystem, network, CPU, and memory
restrictions enforced by your worker environment.
Practical tips for optimizing video processing
Inspect actual outputs, including inputs with audio, variable frame rates, and no audio. With
separately installed ffmpeg and ffprobe, run this from the project directory to inspect and
decode every segment. The subshell stops at the first failed check:
(
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
)
Review diagnostics even when a command succeeds. Compare total video/audio packet counts and payloads with the input, and check the expected ending against a known complete source. Allow for keyframe-aligned segment lengths and container timestamp offsets; checking only that files exist cannot establish preservation.
Conclusion and additional resources
Use JavaCV for inspection and FFmpeg’s packet segmenter for efficient splitting without re-encoding. For adaptive streaming, see Transloadit’s video encoding service and 🤖 /video/adaptive Robot.
