Usage - a-schild/jave2 GitHub Wiki

Usage

Getting it

Check the latest release for the current version. The examples below use 4.2.0.

Every platform at once

jave-all-deps pulls in the library and the binary for every supported platform. It is the simplest choice and the largest download.

<dependency>
    <groupId>ws.schild</groupId>
    <artifactId>jave-all-deps</artifactId>
    <version>4.2.0</version>
</dependency>

One platform

Take jave-core plus the package for the platform you deploy to. This is much smaller, and is what you want in a container image.

<dependency>
    <groupId>ws.schild</groupId>
    <artifactId>jave-core</artifactId>
    <version>4.2.0</version>
</dependency>
<dependency>
    <groupId>ws.schild</groupId>
    <artifactId>jave-nativebin-linux64</artifactId>
    <version>4.2.0</version>
</dependency>
Operating system Architecture Package
Windows x64 jave-nativebin-win64
Windows ARM 64 bit jave-nativebin-win-arm64
macOS Apple silicon jave-nativebin-osxm1
macOS Intel x64 jave-nativebin-osx64deprecated, Apple ends intel support with macOS 27
Linux x64 jave-nativebin-linux64
Linux ARM 64 bit jave-nativebin-linux-arm64
Linux ARM 32 bit jave-nativebin-linux-arm32

The 32 bit x86 packages, jave-nativebin-win32 and jave-nativebin-linux32, were removed in 4.0.0. Stay on 3.6.0 if you need them.

Keeping the versions together

If you pick packages yourself, import the bill of materials and leave the versions off. A core and a native binary from different releases is the usual way this goes wrong.

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>ws.schild</groupId>
            <artifactId>jave-bom</artifactId>
            <version>4.2.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>ws.schild</groupId>
        <artifactId>jave-core</artifactId>
    </dependency>
    <dependency>
        <groupId>ws.schild</groupId>
        <artifactId>jave-nativebin-linux64</artifactId>
    </dependency>
</dependencies>

The javadoc is at javadoc.io.

Coming from 4.0.0

Two things in 4.1.0 change behaviour rather than only adding to it.

Encoder.abortEncoding() now actually stops the encoding. It used to close the streams before killing the process, and closing a pipe does not wake a thread already blocked reading it, so the call waited for ffmpeg to finish of its own accord and then returned as though it had aborted. The same applied to the deadline in MultimediaObject.getInfo(long). Both take effect immediately now. If you built waiting or timeout logic around the old behaviour, it is no longer needed.

slf4j-api moved from 1.7.36 to 2.0.18. slf4j 2 finds its binding through the ServiceLoader rather than through StaticLoggerBinder, so if your project still uses a 1.7 binding, logback-classic 1.2.x or slf4j-simple 1.7.x, and picks 2.0.18 up from here, you will see "No SLF4J providers were found" and lose your logging until the binding is moved to a 2.x one. Only the api is used, so pinning slf4j-api to 1.7.36 in your own build is also a valid answer.

GraalVM native image

Works from 4.2.0 onwards with nothing to configure. The jave-nativebin-* packages carry the reachability metadata that registers the bundled ffmpeg, and native-image picks that up by itself.

Before 4.2.0 the executable was silently dropped from the image, because native-image discards resources unless something registers them, and the first encoding failed with Could not find ffmpeg platform executable in resources. If you see that, the version is the thing to check first.

Three things are worth knowing when building an image:

  • Depend on one platform package, not jave-all-deps. An image is built for a single platform, so pulling in every binary embeds every binary, several hundred megabytes of ffmpeg that the image can never use.
  • The temporary directory has to be writable, and it has to allow execution. The binary is extracted at run time and then run, so a container with a read only /tmp, or one mounted noexec, fails at the point of extraction or immediately after. That is equally true on the jvm, but it catches people more often in a minimal native image container.
  • A custom ProcessLocator sidesteps all of it. If you point the encoder at an ffmpeg you install into the image yourself, nothing is extracted and none of the above applies.

This is checked in CI on every change that could affect it: a native image is built and run, and it extracts the binary, encodes a file and reads the result back.

Encoding

The class you will use most is ws.schild.jave.Encoder.

File source = new File("source.mov");
File target = new File("target.webm");

AudioAttributes audio = new AudioAttributes();
audio.setCodec("libvorbis");
audio.setBitRate(192000);
audio.setSamplingRate(44100);
audio.setChannels(2);

VideoAttributes video = new VideoAttributes();
video.setCodec("libvpx");
video.setBitRate(250000);
video.setFrameRate(25);

EncodingAttributes attrs = new EncodingAttributes();
attrs.setOutputFormat("webm");
attrs.setAudioAttributes(audio);
attrs.setVideoAttributes(video);

new Encoder().encode(new MultimediaObject(source), target, attrs);

encode() blocks until the transcoding has finished or failed. To follow its progress, see monitoring below.

Encoding attributes

EncodingAttributes carries everything the encoder needs.

Method What it does
setOutputFormat(String) The container to write. Must be one the ffmpeg in use can write, see Encoder.getSupportedEncodingFormats()
setAudioAttributes(AudioAttributes) Audio settings. Leave it unset and the result has no audio
setVideoAttributes(VideoAttributes) Video settings. Leave it unset and the result has no video, including album art, which is carried as a video stream
setOffset(Float) Start this many seconds into the source
setDuration(Float) Encode only this many seconds
setStreamLoop(Integer) Repeat the input this many extra times, -1 for endlessly

The method for the container was called setFormat in very old versions. It has been setOutputFormat for a long time, and examples using the old name will not compile.

See Encoding Attributes for the audio and video settings, and Supported formats for what the bundled ffmpeg can read and write.

Monitoring the transcoding operation

Pass an EncoderProgressListener and the encoder will call it as it goes.

new Encoder().encode(new MultimediaObject(source), target, attrs,
    new EncoderProgressListener() {

        @Override
        public void sourceInfo(MultimediaInfo info) {
            // Called once, before the work starts, when the source could be read.
            // Not called when several sources are concatenated, since there is no
            // single source to describe.
        }

        @Override
        public void progress(int permil) {
            // 0 to 1000, or PROGRESS_UNKNOWN when the source declares no duration
            // and there is nothing to be a proportion of.
            if (permil == EncoderProgressListener.PROGRESS_UNKNOWN) {
                System.out.println("working");   // an indeterminate progress bar
            } else {
                System.out.println(permil / 10.0 + "%");
            }
        }

        @Override
        public void message(String message) {
            // Usually a warning from ffmpeg
        }

        @Override
        public void done() {
            // Called once the run has finished successfully. This is a default
            // method, so listeners written before 3.6.0 need not implement it.
        }
    });

When the progress is not a proportion

A percentage needs a total, and some sources do not declare one: live streams, and webm files from browser recorders, which commonly carry no duration in the header. Several sources concatenated have no single duration either. In all those cases progress() is called with EncoderProgressListener.PROGRESS_UNKNOWN, which is -1.

Before 4.1.0 those cases divided by the missing duration anyway and handed the listener a large negative number that looked like a permil, which is what made progress bars jump about on webm input (#269).

When it fails

encode() throws EncoderException, or InputFormatException when the source could not be understood. The message carries what ffmpeg reported, including its exit code.

try {
    new Encoder().encode(new MultimediaObject(source), target, attrs);
} catch (InputFormatException e) {
    // the source is not something ffmpeg could read
} catch (EncoderException e) {
    // the run itself failed, e.getMessage() has ffmpeg's complaint
}

Encoder.getUnhandledMessages() returns the ffmpeg output lines the library did not recognise, which is often where the real reason is.

Reading information about a file

MultimediaInfo info = new MultimediaObject(new File("source.mp4")).getInfo();

info.getFormat();            // container
info.getDuration();          // milliseconds, -1 when unknown
info.getVideo().getSize();   // VideoSize, null when there is no video
info.getAudio().getBitRate();
info.getRotate();            // degrees clockwise, from the stream metadata

A source that never answers can hold the call open, so there is a form that gives up:

// stop waiting after five seconds
MultimediaInfo info = new MultimediaObject(url).getInfo(5000L);

Going further

⚠️ **GitHub.com Fallback** ⚠️