Exporting a Lottie Animation to Video Using Media3
Summary
Exporting a Lottie animation to MP4 — with audio — unlocks a ton for creative and media-heavy apps. From story editors to motion-graphics tools, turning Lottie scenes into shareable video clips lets users post anywhere. It’s a common pattern in successful template-driven apps—think Unfold, InStories, and Storybeat—where users start from animated templates and customize them with photos, text, or video.
This post walks you through a clean and performant pipeline that renders Lottie to video using the Media3 Transformer API and OpenGL — and adds audio support along the way.
What We’ll Build
We’ll implement a function:
suspend fun recordLottieToVideo(
context: Context,
lottieFrameFactory: LottieFrameFactory,
audioUri: String,
outputFilePath: String,
onProgress: (Float) -> Unit = {},
onSuccess: (fileSize: Long) -> Unit = {},
onError: (Throwable) -> Unit = {},
durationMs: Long,
frameRate: Int = 30,
width: Int = 1080,
height: Int = 1920
)
This function:
- Renders Lottie frames to GPU textures
- Encodes them to video using Media3
- Adds an audio track via raw PCM decoding
- Outputs a complete
.mp4
file
The Problem: Bridging Two Worlds
The solution to exporting Lottie animations with audio using Media3 comes down to connecting two seemingly unrelated pieces:
-
On one side, we have
LottieDrawable
, a regular Android Drawable generated by Lottie that can draw animations onto any Canvas, including hardware-accelerated ones. -
On the other side, we have Media3’s
Transformer
, which can be configured with aRawAssetLoader
to receive custom video frames (as OpenGL texture IDs) and raw audio chunks.
But how do you go from a LottieDrawable
— something that draws on a Canvas — to an OpenGL texture ID that Media3 can encode?
The missing link is ImageReader
. It gives us a Surface that:
-
Can be drawn to using lockHardwareCanvas() (GPU-accelerated)
-
Provides Image frames that can be uploaded to the GPU as textures
This makes it the perfect bridge between the CPU-based Lottie drawing world and the GPU-based Media3 encoding pipeline.
Control Everything with RawAssetLoader
The RawAssetLoader
gives you full manual control over how video and audio data is fed into the Media3 encoder:
.setAssetLoaderFactory(
RawAssetLoaderFactory(
audioFormat = audioFormat,
videoFormat = videoFormat,
onRawAssetLoaderCreated = { loader ->
rawAssetLoaderDeferred.complete(loader)
}
)
)
From here on, you’re responsible for queuing:
- OpenGL texture IDs with timestamps
- Raw PCM audio chunks with timestamps
Render Lottie to a Hardware Canvas
Each frame of the Lottie animation is generated as a LottieDrawable
. The most performant way to get that drawable into a video frame is to draw it into a hardware-accelerated canvas — not into a regular software Bitmap.
val hCanvas = imageReader.surface.lockHardwareCanvas()
try {
lottieFrame.setBounds(0, 0, videoWidth, videoHeight)
lottieFrame.draw(hCanvas)
} finally {
imageReader.surface.unlockCanvasAndPost(hCanvas)
}
This approach is not just efficient — it’s the key to real-time performance. By using lockHardwareCanvas() from an ImageReader.Surface, you automatically get a hardware-backed Canvas that is connected to GPU memory.
✅ Does Lottie use the GPU here?
Yes! Internally, LottieDrawable.draw(Canvas)
chooses between a software (renderAndDrawAsBitmap
) and hardware (drawDirectlyToCanvas
) rendering path.
If the Canvas
is hardware-accelerated — as it is when created from lockHardwareCanvas()
— Lottie will automatically use GPU rendering via drawDirectlyToCanvas()
.
Upload Frame as Texture via ImageReader
The unsung hero here is ImageReader
. It provides the surface we draw to, and then lets us acquire the drawn image for use with OpenGL:
val imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, maxImages)
Once a frame is drawn, we acquire the image, upload it to an OpenGL texture and and enqueue the texture id returned into the RawAssetLoader
:
val image = awaitForLastImage.await()
val textureId = uploadImageToGLTexture(image)
rawAssetLoader.queueInputTexture(textureId, presentationTimeUs)
This texture now represents a video frame and will be encoded by Media3 Transformer.
Decode and Enqueue Audio
In parallel, we decode audio from a URI into raw PCM chunks using a custom AudioDecoder
. Each chunk is queued manually into the asset loader:
val (audioChunk, presentationTimeUs) = awaitForAudioChunk.await()
rawAssetLoader.queueAudioData(audioChunk, presentationTimeUs, isLast)
The audio chunks can be read from a local audio file in the traditional, using a MediaExtractor
, a MediaCodec
and creating a pipeline of small bufferes that are extracted, decoded and then passd to the RawAssetLoader
’s callback. Synchronization between video frames and audio chunks is manual — but simple, since you’re in full control.
Bringing It All Together
launch {
repeat(totalFrames) { frameIndex ->
// 1. Draw Lottie frame into hardware canvas
// 2. Acquire image from ImageReader
// 3. Upload image to GL texture
// 4. Enqueue texture into Media3
}
rawAssetLoader.signalEndOfVideoInput()
}
launch {
while (!endOfStream) {
// 1. Decode next PCM chunk
// 2. Enqueue it to Media3
}
rawAssetLoader.signalEndOfAudioInput()
}
In the example code, I have used the coroutine CompletableDeferred
calls as semaphores to suspend the thread and wait for the corresponding results in a typesafe way. This design also makes it easy to wrap the process inside a use case that emits a Flow of results.
The full solution can be found in the Github repository: LottieRecorder
Why This Approach Works
RawAssetLoader
gives full audio+video controlImageReader
is optimized for canvas-to-texture uselockHardwareCanvas()
ensures GPU memory drawingLottieDrawable
already handles GPU rendering internally when used on a hardware canvas
Future Improvements
- Use a
SurfaceTexture
bound to an OES external texture, draw to itsSurface
with lockHardwareCanvas(), then sample that external texture directly in GL for near zero-copy (no CPU readbacks)—but expect trickier setup/synchronization and test broadly. - Investigate using Skottie (Skia GPU Lottie renderer) for even faster native rendering