Create a basic video editing app using Media3 Transformer

The Transformer APIs in Jetpack Media3 are designed to make media editing performant and reliable. Transformer supports a number of operations, including:

  • Modifying a video with trimming, scaling, and rotating
  • Adding effects like overlays and filters
  • Processing special formats like HDR and slow-motion video
  • Exporting a media item after applying edits

This page walks you through some of the key use cases covered by Transformer. For more details you can head to our full guides on Media3 Transformer.

Get started

To get started, add a dependency on the Transformer, Effect, and Common modules of Jetpack Media3:

implementation "androidx.media3:media3-transformer:1.2.1"
implementation "androidx.media3:media3-effect:1.2.1"
implementation "androidx.media3:media3-common:1.2.1"

Make sure to replace 1.2.1 with your preferred version of the library. You can refer to the release notes to see the latest version.

Important classes

Class Purpose
Transformer Start and stop transformations and check for progress updates on a running transformation.
EditedMediaItem Represents a media item to process and the edits to apply to it.
Effects A collection of audio and video effects.

Configure the output

With Transformer.Builder, you can now specify videoMimeType and audioMimetype directory by setting the function without needing to create a TransformationRequest object.

Transcode between formats

The following code shows how to configure a Transformer object to output H.265/AVC video and AAC audio:

Kotlin

val transformer = Transformer.Builder(context)
    .setVideoMimeType(MimeTypes.VIDEO_H265)
    .setAudioMimeType(MimeTypes.AUDIO_AAC)
    .build()

Java

Transformer transformer = new Transformer.Builder(context)
    .setVideoMimeType(MimeTypes.VIDEO_H265)
    .setAudioMimeType(MimeTypes.AUDIO_AAC)
    .build();

If the input media format already matches the transformation request for audio or video, Transformer automatically switches to transmuxing, that is, copying the compressed samples from the input container to the output container without modification. This avoids the computational cost and potential quality loss of decoding and re-encoding in the same format.

Set HDR mode

If the input media file is in an HDR format, you can choose between a few different modes for how Transformer processes the HDR information. You probably want to use either HDR_MODE_KEEP_HDR or HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL.

HDR_MODE_KEEP_HDR HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL
Description Preserve the HDR data, meaning that the HDR output format is the same as the HDR input format. Tonemap HDR input to SDR using an OpenGL tone-mapper, meaning that the output format will be in SDR.
Support Supported on API levels 31+ for devices that include an encoder with the FEATURE_HdrEditing capability. Supported on API levels 29+.
Errors If not supported, attempts to use HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL instead. If not supported, throws an ExportException.

On devices that support the required encoding capabilities and run Android 13 (API level 33) or higher, Transformer objects let you edit HDR videos. HDR_MODE_KEEP_HDR is the default mode when building the Composition object, as shown in the following code:

Kotlin

val composition = Composition.Builder(
    ImmutableList.of(videoSequence))
    .setHdrMode(HDR_MODE_KEEP_HDR)
    .build()

Java

Composition composition = new Composition.Builder(
    ImmutableList.of(videoSequence))
    .setHdrMode(Composition.HDR_MODE_KEEP_HDR)
    .build();

Prepare a media item

A MediaItem represents an audio or video item in your app. An EditedMediaItem collects a MediaItem along with the transformations to apply to it.

Trim a video

To remove unwanted portions of a video, you can set custom start and end positions by adding a ClippingConfiguration to the MediaItem.

Kotlin

val clippingConfiguration = MediaItem.ClippingConfiguration.Builder()
    .setStartPositionMs(10_000) // start at 10 seconds
    .setEndPositionMs(20_000) // end at 20 seconds
    .build()
val mediaItem = MediaItem.Builder()
    .setUri(videoUri)
    .setClippingConfiguration(clippingConfiguration)
    .build()

Java

ClippingConfiguration clippingConfiguration = new MediaItem.ClippingConfiguration.Builder()
    .setStartPositionMs(10_000) // start at 10 seconds
    .setEndPositionMs(20_000) // end at 20 seconds
    .build();
MediaItem mediaItem = new MediaItem.Builder()
    .setUri(videoUri)
    .setClippingConfiguration(clippingConfiguration)
    .build();

Use built-in effects

Media3 includes a number of built-in video effects for common transformations, for example:

Class Effect
Presentation Scale the media item by resolution or aspect ratio
ScaleAndRotateTransformation Scale the media item by a multiplier and/or rotate the media item
Crop Crop the media item to a smaller or larger frame
OverlayEffect Add a text or image overlay on top of the media item

For audio effects, you can add a sequence of AudioProcessor instances that will transform the raw (PCM) audio data. For example, you can use a ChannelMixingAudioProcessor to mix and scale audio channels.

To use these effects, create an instance of the effect or audio processor, build an instance of Effects with the audio and video effects you want to apply to the media item, then add the Effects object to an EditedMediaItem.

Kotlin

val channelMixingProcessor = ChannelMixingAudioProcessor()
val rotateEffect = ScaleAndRotateTransformation.Builder().setRotationDegrees(60f).build()
val cropEffect = Crop(-0.5f, 0.5f, -0.5f, 0.5f)

val effects = Effects(listOf(channelMixingProcessor), listOf(rotateEffect, cropEffect))

val editedMediaItem = EditedMediaItem.Builder(mediaItem)
    .setEffects(effects)
    .build()

Java

ChannelMixingAudioProcessor channelMixingProcessor = new ChannelMixingAudioProcessor();
ScaleAndRotateTransformation rotateEffect = new ScaleAndRotateTransformation.Builder()
    .setRotationDegrees(60f)
    .build();
Crop cropEffect = new Crop(-0.5f, 0.5f, -0.5f, 0.5f);

Effects effects = new Effects(
    ImmutableList.of(channelMixingProcessor),
    ImmutableList.of(rotateEffect, cropEffect)
);

EditedMediaItem editedMediaItem = new EditedMediaItem.Builder(mediaItem)
    .setEffects(effects)
    .build();

Create custom effects

By extending the effects included in Media3, you can create custom effects specific to your use cases. In the following example, use subclass MatrixTransformation to zoom the video into filling the frame over the first second of playback:

Kotlin

val zoomEffect = MatrixTransformation { presentationTimeUs ->
    val transformationMatrix = Matrix()
    // Set the scaling factor based on the playback position
    val scale = min(1f, presentationTimeUs / 1_000f)
    transformationMatrix.postScale(/* x */ scale, /* y */ scale)
    transformationMatrix
}

val editedMediaItem = EditedMediaItem.Builder(inputMediaItem)
    .setEffects(Effects(listOf(), listOf(zoomEffect))
    .build()

Java

MatrixTransformation zoomEffect = presentationTimeUs -> {
    Matrix transformationMatrix = new Matrix();
    // Set the scaling factor based on the playback position
    float scale = min(1f, presentationTimeUs / 1_000f);
    transformationMatrix.postScale(/* x */ scale, /* y */ scale);
    return transformationMatrix;
};

EditedMediaItem editedMediaItem = new EditedMediaItem.Builder(inputMediaItem)
    .setEffects(new Effects(ImmutableList.of(), ImmutableList.of(zoomEffect)))
    .build();

To further customize the behavior of an effect, implement a GlShaderProgram. The queueInputFrame() method is used to process input frames. For example, to leverage the machine learning capabilities of MediaPipe, you can use a MediaPipe FrameProcessor to send each frame through a MediaPipe graph. See an example of this in the Transformer demo app.

Preview effects

With ExoPlayer, you can preview the effects added to a media item before starting the export process. Using the same Effects object as for the EditedMediaItem, call setVideoEffects() on your ExoPlayer instance.

Kotlin

val player = ExoPlayer.builder(context)
    .build()
    .also { exoPlayer ->
        exoPlayer.setMediaItem(inputMediaItem)
        exoPlayer.setVideoEffects(effects)
        exoPlayer.prepare()
    }

Java

ExoPlayer player = new ExoPlayer.builder(context).build();
player.setMediaItem(inputMediaItem);
player.setVideoEffects(effects);
exoPlayer.prepare();

You can also preview audio effects with ExoPlayer. When building your ExoPlayer instance, pass in a custom RenderersFactory that configures the player’s audio renderers to output audio to an AudioSink that uses your AudioProcessor sequence. In the example below, we do this by overriding the buildAudioSink() method of a DefaultRenderersFactory.

Kotlin

val player = ExoPlayer.Builder(context, object : DefaultRenderersFactory(context) {
    override fun buildAudioSink(
        context: Context,
        enableFloatOutput: Boolean,
        enableAudioTrackPlaybackParams: Boolean,
        enableOffload: Boolean
    ): AudioSink? {
        return DefaultAudioSink.Builder(context)
            .setEnableFloatOutput(enableFloatOutput)
            .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
            .setOffloadMode(if (enableOffload) {
                     DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED
                } else {
                    DefaultAudioSink.OFFLOAD_MODE_DISABLED
                })
            .setAudioProcessors(arrayOf(channelMixingProcessor))
            .build()
        }
    }).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context, new DefaultRenderersFactory(context) {
        @Nullable
        @Override
        protected AudioSink buildAudioSink(
            Context context,
            boolean enableFloatOutput,
            boolean enableAudioTrackPlaybackParams,
            boolean enableOffload
        ) {
            return new DefaultAudioSink.Builder(context)
                .setEnableFloatOutput(enableFloatOutput)
                .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
                .setOffloadMode(
                    enableOffload
                        ? DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED
                        : DefaultAudioSink.OFFLOAD_MODE_DISABLED)
                .setAudioProcessors(new AudioProcessor[]{channelMixingProcessor})
                .build();
        }
    }).build();

Start a transformation

Lastly, create a Transformer to apply your edits and start exporting the resulting media item.

Kotlin

val transformer = Transformer.Builder(context)
    .addListener(listener)
    .build()
transformer.start(editedMediaItem, outputPath)

Java

Transformer transformer = new Transformer.Builder(context)
    .addListener(listener)
    .build();
transformer.start(editedMediaItem, outputPath);

You can similarly cancel the export process if needed with Transformer.cancel().

Check for progress updates

Transformer.start returns immediately and runs asynchronously. To query the current progress of a transformation, call Transformer.getProgress(). This method takes a ProgressHolder, and if the progress state is available, that is, if the method returns PROGRESS_STATE_AVAILABLE, then the provided ProgressHolder will be updated with the current progress percentage.

You can also attach a listener to your Transformer to be notified about completion or error events.