CLI tool
bramp/ffmpeg-cli-wrapper avatar
bramp/ffmpeg-cli-wrapper

ffmpeg-cli-wrapper builds FFmpeg argument lists in Java instead of concatenating strings

Java wrapper around the FFmpeg command line tool

1,911 stars425 forksJavaBSD-2-Clause

At a glance

What is it?
bramp/ffmpeg-cli-wrapper gives Java code a fluent builder for FFmpeg and FFprobe, so an encode is described with typed setters and executed through FFmpegExecutor. Compatibility depends on the FFmpeg build on the host, not on the jar version.
Who is it for?
Adopt this library when you need more than one FFmpeg invocation in Java and the argument strings have started to rot, since the builder keeps the options typed and reviewable. Do not adopt it for a single fixed command, where ProcessBuilder is enough.
Can I use it commercially?
Yes. BSD-2-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 5 days ago.
What is it written in?
Mainly Java, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The builder is the whole point, and it is a state machine

FFmpeg's command line has many mutually related options, and the usual Java approach of concatenating strings produces code that compiles regardless of whether the options make sense together. This library replaces that with FFmpegBuilder, a fluent chain that terminates in done() and produces a job description rather than a String.

The chain in the README's video encoding example alternates between input and output groups, each closed by done(). setInput accepts either a filename or an FFmpegProbeResult, which means the common case of sizing an output from the probed duration needs no manual arithmetic.

java
FFmpegBuilder builder =
    new FFmpegBuilder()
        .setInput(in)
        .done()
        .overrideOutputFiles(true)
        .addOutput("output.mp4")
        .setFormat("mp4")
        .setTargetSize(250_000)
        .disableSubtitle()
        .done();

Beyond the structural grouping, the setters carry the semantics FFmpeg itself has. setVideoFrameRate takes a rational, written as setVideoFrameRate(24, 1), so you get 24/1 rather than a float. setTargetSize(250_000) expresses an intent in bytes instead of the bitrate arithmetic that appears in the underlying arguments. disableSubtitle() is a flag rather than a codec selection.

One setting is easy to miss and matters in practice: setStrict(FFmpegBuilder.Strict.EXPERIMENTAL) is what allows codecs such as x264 and aac, which FFmpeg otherwise gates behind its experimental flag.

Probing comes first, and it returns parsed streams and format

FFprobe wraps the ffprobe binary and returns an FFmpegProbeResult rather than raw JSON text. The object exposes getFormat() and getStreams(), and the fields on them are public: format.filename, format.format_long_name, format.duration, and on a stream, codec_long_name, width and height.

java
FFprobe ffprobe = new FFprobe("/path/to/ffprobe");
FFmpegProbeResult probeResult = ffprobe.probe("input.mp4");

FFmpegFormat format = probeResult.getFormat();
System.out.format(
    "%nFile: '%s' ; Format: '%s' ; Duration: %.3fs",
    format.filename, format.format_long_name, format.duration);

This is the step that separates the library from a ProcessBuilder wrapper. You get a typed model of the media before you build anything, so duration, stream count and codec names are available to Java logic rather than to a regex. The video example depends on exactly that: it probes the input and then feeds the result into setInput.

The constructors take explicit paths to the binaries, as in new FFmpeg("/path/to/ffmpeg") and new FFprobe("/path/to/ffprobe"). The library does not search PATH for you, which is deliberate in a container: you point it at the exact binary your image ships.

Executor, jobs, and the one-pass versus two-pass choice

FFmpegExecutor is constructed from the two wrappers and turns a builder into something runnable. Two methods matter. createJob(builder).run() performs a single pass. createTwoPassJob(builder).run() performs the two-pass encode, and the README describes it as better quality at the cost of being slower.

java
FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
executor.createJob(builder).run();
executor.createTwoPassJob(builder).run();

The two-pass path is the reason to use this library rather than calling the binary directly, because the log-file handling and the first-pass invocation are fiddly to get right by hand.

Audio and video options are set with the same paired-setter style: setAudioChannels(1) for mono, setAudioCodec("aac"), setAudioSampleRate(48_000), setAudioBitRate(32768), setVideoCodec("libx264"), setVideoResolution(640, 480). The codec names are FFmpeg's own identifiers, which means the wrapper validates nothing about them; a typo surfaces as an FFmpeg error at run time rather than as a compile error.

Progress arrives as a listener callback parsed from stderr

FFmpegExecutor can be given a ProgressListener when the job is created, and the callback receives a Progress object with out_time_ns, status, frame, fps and speed. FFmpegUtils.toTimecode converts the nanosecond count into a readable timecode.

java
FFmpegJob job =
    executor.createJob(
        builder,
        new ProgressListener() {
          final double duration_ns = in.getFormat().duration * TimeUnit.SECONDS.toNanos(1);

          @Override
          public void progress(Progress progress) {
            double percentage = progress.out_time_ns / duration_ns;
          }
        });

job.run();

The percentage in the example is computed by the caller, dividing out_time_ns by the probed duration, which tells you something about the API: duration is not handed to the listener, so you keep the FFmpegProbeResult around and do the division yourself. That also means a job with an unknown duration gives you a meaningless percentage, and the raw fields are what you should log instead.

The job object is returned rather than executed inline, so you can hold a reference to the FFmpegJob before calling run().

The jar is independent of the FFmpeg on the host, and that cuts both ways

This is the limitation worth understanding before you adopt it. The library generates command-line arguments; it does not ship, bundle or verify FFmpeg. The README states that any reasonably modern FFmpeg should work, listing 5.x, 6.x and 7.x, and that compatibility depends on which features you actually use.

The consequence is that version skew lives outside your dependency management. A jar upgrade cannot break you by changing codec names, but a base image rebuild that bumps FFmpeg can. Conversely, a codec identifier that the library happily passes through may not exist in the FFmpeg build you deploy against, and you find out at run time.

The Ubuntu section adds a concrete version of that problem. The README states the original FFmpeg is supported rather than the libav variant, notes that Ubuntu shipped the original before 12.04 and in 15.04 and later, and points to a PPA or a static build for the releases in between. If your build environment is one of those, the wrapper is not the failing component, but it will look like it is.

When plain ProcessBuilder is the better answer

The alternative approaches differ in what they cost you. ProcessBuilder plus a hand-written argument list has no dependency, no probe model, and no progress parsing, and for a fixed command that is called once it is the smaller solution.

The fluent builder earns its place when the argument list is long and varies with input. The video example sets many options across input, output, audio and video groups; the same list as a raw String array would be unreviewable and would drift out of sync with the FFmpeg documentation.

What this library does not offer is a native FFmpeg binding such as an in-process encoder. It runs the same binary you would run by hand, in a separate process, with the arguments assembled in Java. If you need to avoid a subprocess, or you need FFmpeg features that no setter covers, you will end up reaching for a raw execution path anyway. The repository has an EXAMPLES.md file, generated from the test source code by a make target, which is where the README's code samples come from; checking it is the fastest way to see whether a setter exists for what you need.

Versioning, JDK support and the simplified BSD licence

Java 11 and above is the stated floor, and the README says the library is tested on JDK 11, 17 and 21. The Makefile lists those three as JDK_VERSIONS and fans make test out to a target per JDK, so the matrix is real rather than aspirational. Building is mvn compile, testing is mvn test, and formatting follows Google Java Style with separate make targets for format, check-format and check-style.

The current release is ffmpeg-0.9.2, tagged on 2026-04-06, following ffmpeg-0.9.1 on 2026-04-04. The last push to the repository was on 2026-10-11, so post-release work is landing on main without a new tag yet. Note the README's install snippet still shows version 0.9.1 in the Maven coordinates while the release list carries 0.9.2, which is the kind of detail worth checking against Maven Central before you pin.

Releases are automated. A maintainer bumps the version in pom.xml, removes -SNAPSHOT, pushes a tag such as ffmpeg-0.9.1, and a GitHub Action runs the test matrix, signs and publishes to the Sonatype Central Portal, and creates a GitHub Release with generated notes. Consuming the artifact from Maven Central means you inherit that signing chain rather than running it yourself.

Licensing is a simplified two-clause BSD licence, Copyright (c) 2013-2024, Andrew Brampton, with the text in the LICENCE file at the repository root. That is a permissive licence subject to its notice and redistribution conditions, separate from the FFmpeg binary your deployment has to provide under its own terms.

Editorial conclusion

Adopt this library when you need more than one FFmpeg invocation in Java and the argument strings have started to rot, since the builder keeps the options typed and reviewable. Do not adopt it for a single fixed command, where ProcessBuilder is enough. Before deploying, verify the FFmpeg build on your host, because the library only generates arguments and the README notes some distributions ship libav instead.

Frequently asked questions

How do I add ffmpeg-cli-wrapper to a Maven project?

Add the net.bramp.ffmpeg group with the ffmpeg artifact under a version, for example 0.9.1 in the README's snippet. The library targets Java 11 and above and is published to Maven Central.

Which JDK versions does ffmpeg-cli-wrapper support?

Java 11 and above. The README says the library is tested on JDK 11, 17 and 21, and the Makefile fans make test out across those three versions.

How do I get progress while FFmpeg is encoding?

Pass a ProgressListener to executor.createJob(builder, listener). The callback receives a Progress object with out_time_ns, status, frame, fps and speed, and FFmpegUtils.toTimecode converts the nanosecond value into a timecode string.

Does the library include FFmpeg itself?

No. It generates command-line arguments and runs the binary at the path you pass to new FFmpeg and new FFprobe. The README states only the original FFmpeg is supported, not the libav variant, so some Ubuntu releases need a PPA or a static build.

Official sources

  1. bramp/ffmpeg-cli-wrapper on GitHub
  2. Issues
  3. License: BSD-2-Clause
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/bramp-ffmpeg-cli-wrapper.svg)](https://hysenlabs.com/projects/bramp-ffmpeg-cli-wrapper)

Was this useful?