GIFCompressor

Compresses GIF files into the MP4 format, using hardware accelerated Android codecs available on the device. Works on API 18+.

implementation 'com.otaliastudios.gif:compressor:1.0.0'
  • Fast compression to lightweight MP4 (AVC)
  • Hardware accelerated
  • Multithreaded
  • Convenient, fluent API
  • Concatenate multiple GIF files [docs]
  • Choose output size, with automatic cropping [docs]
  • Choose output rotation [docs]
  • Choose output speed [docs]
  • Choose output frame rate [docs]
  • Override frames timestamp, e.g. to slow down the middle part of the video [docs]
  • Error handling [docs]
  • Configurable strategies [docs]

GIFCompressor

Using GIFCompressor in the most basic form is pretty simple:

GIFCompressor.into(filePath)
        .addDataSource(context, uri) // or...
        .addDataSource(context, filePath) // or...
        .addDataSource(context, fileDescriptor) // or...
        .addDataSource(dataSource)
        .setListener(new GIFListener() {
             public void onGIFCompressionProgress(double progress) {}
             public void onGIFCompressionCompleted() {}
             public void onGIFCompressionCanceled() {}
             public void onGIFCompressionFailed(@NonNull Throwable exception) {}
        }).compress()

Take a look at the demo app for a real example or keep reading below for documentation.

Setup

This library requires API level 18 (Android 4.3) or later.
If your app targets older versions, you can override the minSdkVersion by
adding this line to your manifest file:

<uses-sdk tools:overrideLibrary="com.otaliastudios.gif" />

In this case you should check at runtime that API level is at least 18, before
calling any method here.

Data Sources

Starting a compression operation will require a source for our data, which is not necessarily
a File. The DataSource objects will automatically take care about releasing streams / resources,
which is convenient but it means that they can not be used twice.

UriDataSource

The Android friendly source can be created with new UriDataSource(context, uri) or simply
using addDataSource(context, uri) in the compression builder.

FileDescriptorDataSource

A data source backed by a file descriptor. Use new FileDescriptorDataSource(context, descriptor) or
simply addDataSource(context, descriptor) in the compression builder.

FilePathDataSource

A data source backed by a file absolute path. Use new FilePathDataSource(context, path) or
simply addDataSource(context, path) in the compression builder.

GIF Concatenation

As you might have guessed, you can use addDataSource(source) multiple times. All the source
GIFs will be stitched together:

Transcoder.into(filePath)
        .addDataSource(source1)
        .addDataSource(source2)
        .addDataSource(source3)
        // ...

In the above example, the three GIF files will be stitched together in the order they are added
to the builder. Once source1 ends, we'll append source2 and so on. The library will take care
of applying consistent parameters (frame rate, bit rate) during the conversion.

For Example:

GIFCompressor.into(filePath)
        .addDataSource(source1) // 20 seconds
        .addDataSource(source2) // 5 seconds
        .addDataSource(source3) // 5 seconds
        // ...

In the above example, the output file will be 30 seconds long:

Video: | •••••••••••••••••• source1 •••••••••••••••••• | •••• source2 •••• | •••• source3 •••• |  

Listening for events

GIF compression will happen on a background thread, but we will send updates through the GIFListener
interface, which can be applied when building the request:

GIFCompressor.into(filePath)
        .setListenerHandler(handler)
        .setListener(new GIFListener() {
             public void onGIFCompressionProgress(double progress) {}
             public void onGIFCompressionCompleted() {}
             public void onGIFCompressionCanceled() {}
             public void onGIFCompressionFailed(@NonNull Throwable exception) {}
        })
        // ...

All of the listener callbacks are called:

  • If present, on the handler specified by setListenerHandler()
  • If it has a handler, on the thread that started the compress() call
  • As a last resort, on the UI thread

onGIFCompressionProgress

This simply sends a double indicating the current progress. The value is typically between 0 and 1,
but can be a negative value to indicate that we are not able to compute progress (yet?).

This is the right place to update a ProgressBar, for example.

onGIFCompressionCanceled

The compression operation was canceled. This can happen when the Future returned by compress()
is cancelled by the user.

onGIFCompressionFailed

This can happen in a number of cases and is typically out of our control. Input options might be
wrong, write permissions might be missing, codec might be absent, input file might be not supported
or simply corrupted.

You can take a look at the Throwable being passed to know more about the exception.

onGIFCompressionCompleted

Compression operation succeeded. The output file now contains the desired video.

Strategies

Track strategies return options for the engine to understand how the GIF file (or files)
should be compressed.

Transcoder.into(filePath)
        .setStrategy(strategy)
        // ...

The point of Strategy is to inspect the input android.media.MediaFormat and return
the output android.media.MediaFormat, filled with required options.

This library offers a default strategy that covers most use cases, called DefaultStrategy.
This strategy converts the GIF stream to AVC format and is very configurable.

Frame Size

The DefaultStrategy helps in defining a consistent output size. If the chosen size does not
match the aspect ratio of the input GIF(s) size, GIFCompressor will automatically crop part
of the input so it matches the final ratio.

We provide helpers for common tasks:

DefaultStrategy strategy;

// Sets an exact size. If aspect ratio does not match, cropping will take place.
strategy = DefaultStrategy.exact(1080, 720).build();

// Keeps the aspect ratio, but scales down the input size with the given fraction.
strategy = DefaultStrategy.fraction(0.5F).build();

// Ensures that each video size is at most the given value - scales down otherwise.
strategy = DefaultStrategy.atMost(1000).build();

// Ensures that minor and major dimension are at most the given values - scales down otherwise.
strategy = DefaultStrategy.atMost(500, 1000).build();

In fact, all of these will simply call new DefaultStrategy.Builder(resizer) with a special
resizer. We offer handy resizers:

Name Description
ExactResizer Returns the exact dimensions passed to the constructor.
AspectRatioResizer Crops the input size to match the given aspect ratio.
FractionResizer Reduces the input size by the given fraction (0..1).
AtMostResizer If needed, reduces the input size so that the "at most" constraints are matched. Aspect ratio is kept.
PassThroughResizer Returns the input size unchanged.

You can also group resizers through MultiResizer, which applies resizers in chain:

// First scales down, then ensures size is at most 1000. Order matters!
Resizer resizer = new MultiResizer();
resizer.addResizer(new FractionResizer(0.5F));
resizer.addResizer(new AtMostResizer(1000));

// First makes it 16:9, then ensures size is at most 1000. Order matters!
Resizer resizer = new MultiResizer();
resizer.addResizer(new AspectRatioResizer(16F / 9F));
resizer.addResizer(new AtMostResizer(1000));

This option is already available through the DefaultStrategy builder, so you can do:

DefaultStrategy strategy = new DefaultStrategy.Builder()
        .addResizer(new AspectRatioResizer(16F / 9F))
        .addResizer(new FractionResizer(0.5F))
        .addResizer(new AtMostResizer(1000))
        .build();

Other options

You can configure the DefaultStrategy with other options unrelated to the video size:

DefaultStrategy strategy = new DefaultStrategy.Builder()
        .bitRate(bitRate)
        .bitRate(DefaultVideoStrategy.BITRATE_UNKNOWN) // tries to estimate
        .frameRate(frameRate) // will be capped to the input frameRate
        .keyFrameInterval(interval) // interval between key-frames in seconds
        .build();

Advanced Options

Frame rotation

You can set the output rotation with the setRotation(int) method. This will apply a clockwise
rotation to the input GIF frames. Accepted values are 0, 90, 180, 270:

GIFCompressor.into(filePath)
        .setRotation(rotation) // 0, 90, 180, 270
        // ...

Time interpolation

We offer APIs to change the timestamp of each GIF frame. You can pass a TimeInterpolator
to the compressor builder to be able to receive the frame timestamp as input, and return a new one
as output.

GIFCompressor.into(filePath)
        .setTimeInterpolator(timeInterpolator)
        // ...

As an example, this is the implementation of the default interpolator, called DefaultTimeInterpolator,
that will just return the input time unchanged:

@Override
public long interpolate(long time) {
    // Receive input time in microseconds and return a possibly different one.
    return time;
}

It should be obvious that returning invalid times can make the process crash at any point, or at least
the compression operation fail.

Video speed

We also offer a special time interpolator called SpeedTimeInterpolator that accepts a float parameter
and will modify the video speed.

  • A speed factor equal to 1 will leave speed unchanged
  • A speed factor < 1 will slow the GIF down
  • A speed factor > 1 will accelerate the GIF

This interpolator can be set using setTimeInterpolator(TimeInterpolator), or, as a shorthand,
using setSpeed(float):

GIFCompressor.into(filePath)
        .setSpeed(0.5F) // 0.5x
        .setSpeed(1F) // Unchanged
        .setSpeed(2F) // Twice as fast
        // ...

Compatibility

As stated pretty much everywhere, not all codecs/devices/manufacturers support all sizes/options.
This is a complex issue which has no easy solution - a wrong size can lead to a compression error
or corrupted file.

Android platform specifies requirements for manufacturers through the CTS (Compatibility test suite).
Only a few codecs and sizes are strictly required to work.

We collect common presets in the DefaultStrategies class:

GIFCompressor.into(filePath)
        .setStrategy(DefaultStrategies.for720x1280()) // 16:9
        .setStrategy(DefaultStrategies.for360x480()) // 4:3
        // ...

GitHub