Android Video with Media3, Caching and Transformer
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. See the section bibliography for the reference material consulted while preparing these pages. |
Baseline date: 2026-10-07. Jetpack Media3 is the supported way to play video on
Android. The standalone ExoPlayer repository (google/ExoPlayer) is archived, and its code lives on in the
androidx.media3 artifacts. Media3 artifacts are published on Google’s Maven repository, so check the current version
on the Media3 release page. The packaging that produces
what the player consumes is in video streaming: HLS, DASH and CMAF.
Dependencies
val media3 = "<version>" // use the current stable version from the release page
dependencies {
implementation("androidx.media3:media3-exoplayer:$media3")
implementation("androidx.media3:media3-exoplayer-hls:$media3")
implementation("androidx.media3:media3-exoplayer-dash:$media3")
implementation("androidx.media3:media3-ui:$media3")
implementation("androidx.media3:media3-datasource:$media3") // CacheDataSource and SimpleCache
implementation("androidx.media3:media3-database:$media3")
implementation("androidx.media3:media3-transformer:$media3") // on-device transcoding
implementation("androidx.media3:media3-effect:$media3")
}
Playing HLS and DASH
ExoPlayer chooses the media source from the URL’s type, so adding the exoplayer-hls and exoplayer-dash modules is
enough to play .m3u8 and .mpd. The player implements adaptive bitrate selection and switches rendition by segment.
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
class PlayerHolder(context: Context) {
val player: ExoPlayer = ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri("https://cdn.example.com/videos/42/master.m3u8"))
prepare()
playWhenReady = false
}
fun release() = player.release()
}
Release the player in onStop or when the composable leaves the composition, and create one player per screen, not per
list item. In a list, show the poster image (see
Coil and Glide) and create the player when the user taps play.
For private video, add the signed-cookie or token header with a custom DataSource.Factory, see
video streaming. Reference:
ExoPlayer hello world and
HLS support.
Caching segments with SimpleCache
SimpleCache stores downloaded media on disk, so replays and short seeks do not hit the network again.
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
object MediaCache {
private var cache: SimpleCache? = null
fun get(context: Context): SimpleCache = cache ?: SimpleCache(
File(context.cacheDir, "media"),
LeastRecentlyUsedCacheEvictor(200L * 1024 * 1024), // 200 MB
StandaloneDatabaseProvider(context),
).also { cache = it }
}
fun buildPlayer(context: Context): ExoPlayer {
val dataSourceFactory = CacheDataSource.Factory()
.setCache(MediaCache.get(context))
.setUpstreamDataSourceFactory(DefaultHttpDataSource.Factory())
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
return ExoPlayer.Builder(context)
.setMediaSourceFactory(DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory))
.build()
}
Use a single SimpleCache instance per cache directory for the whole process: a second instance on the same folder throws.
Reference: downloading and caching media.
Compressing before upload with Transformer
Media3 Transformer re-encodes a video on the device with the hardware codecs, so the upload is smaller and faster, see device-side compression and uploads for when this is worth doing.
import androidx.media3.common.MimeTypes
import androidx.media3.effect.Presentation
import androidx.media3.transformer.*
fun compress(context: Context, input: Uri, outputPath: String, onDone: (Boolean) -> Unit) {
val edited = EditedMediaItem.Builder(MediaItem.fromUri(input))
.setEffects(Effects(emptyList(), listOf(Presentation.createForHeight(720)))) // scale to 720p
.build()
val transformer = Transformer.Builder(context)
.setVideoMimeType(MimeTypes.VIDEO_H264)
.setAudioMimeType(MimeTypes.AUDIO_AAC)
.addListener(object : Transformer.Listener {
override fun onCompleted(composition: Composition, result: ExportResult) = onDone(true)
override fun onError(composition: Composition, result: ExportResult, exception: ExportException) = onDone(false)
})
.build()
transformer.start(edited, outputPath) // call from the main thread, runs in the background
}
Transformer can trim, rotate and change frame rate and mime type. It uses the device’s encoders, so output size and quality vary between phones. Keep the server-side transcode as the authority, see video encoding fundamentals. Reference: Media3 Transformer.
Cover frames with MediaMetadataRetriever
fun coverFrame(context: Context, uri: Uri, timeMs: Long): Bitmap? {
// MediaMetadataRetriever is AutoCloseable (so .use {} works) only from API 29; release() works on every level.
val retriever = MediaMetadataRetriever()
try {
retriever.setDataSource(context, uri)
return retriever.getFrameAtTime(timeMs * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
} finally {
retriever.release()
}
}
OPTION_CLOSEST_SYNC returns the nearest keyframe, which is fast. Scale the bitmap, compress it to JPEG or WebP, and upload
it as the poster, or leave the cover to the backend, see
video covers and thumbnails. Reference:
MediaMetadataRetriever.