Implementation of Advanced Video Playback through ExoPlayer Using Kotlin
This article describes an approach to implementing a reliable media player solution in contemporary Android applications. In particular, we…
Implementation of Advanced Video Playback through ExoPlayer Using Kotlin

This article describes an approach to implementing a reliable media player solution in contemporary Android applications. In particular, we will implement a video playback feature by leveraging Jetpack Compose and ExoPlayer (nowadays available in AndroidX Media3 library).
Prior to starting, it is essential to note that managing video streams involves more than simply passing URL to some UI element. The application needs to optimize network usage, hardware decoders usage, and UI state management.
Initially, we will consider the main technical prerequisites. Then, we will have a look at the implementation code.
What is the Approach to Network Optimization?
The media stream takes up a lot of bandwidth. When a user uses the video scrubbing bar, the application should provide the bytes stored locally, not re-request them from the network.
For these purposes, the SimpleCache feature of ExoPlayer can be used. However, in a Jetpack Compose-based application, UI elements undergo frequent recreation (recomposition). Thus, creating the cache object directly in a composable function will result in orphaned objects. Therefore, the cache should be implemented as a singleton available globally in the application.
import android.content.Context
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import java.io.File
object VideoCache {
@Volatile
private var cacheInstance: SimpleCache? = null
fun getInstance(context: Context): SimpleCache {
return cacheInstance ?: synchronized(this) {
cacheInstance ?: SimpleCache(
File(context.cacheDir, "media_cache"),
LeastRecentlyUsedCacheEvictor(100L * 1024 * 1024),
StandaloneDatabaseProvider(context)
).also { cacheInstance = it }
}
}
}
Managing Composable Lifecycles and Hardware Resources
ExoPlayer relies on PlayerView, a traditional Android View class. Jetpack Compose provides the AndroidView wrapper to facilitate interoperability.
The primary technical challenge is lifecycle management. Video playback utilizes hardware decoders allocated by the Android operating system. If an application moves to the background and retains these system resources, the OS will likely terminate the process.
Instead of overriding Activity lifecycle methods, we implement DisposableEffect within our composable. This ties the player’s state and resource allocation directly to the composable’s presence in the UI tree.
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.media3.common.MediaItem
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.ui.PlayerView
@Composable
fun StreamingVideoPlayer(videoUrl: String, modifier: Modifier = Modifier) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
// Initialize ExoPlayer and preserve it across recompositions
val exoPlayer = remember {
val httpDataSourceFactory = DefaultHttpDataSource.Factory()
.setAllowCrossProtocolRedirects(true)
val cacheDataSourceFactory = CacheDataSource.Factory()
.setCache(VideoCache.getInstance(context))
.setUpstreamDataSourceFactory(httpDataSourceFactory)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
ExoPlayer.Builder(context)
.setMediaSourceFactory(DefaultMediaSourceFactory(cacheDataSourceFactory))
.build()
.apply {
setMediaItem(MediaItem.fromUri(videoUrl))
prepare()
playWhenReady = true
}
}
// Bind playback state to the Android lifecycle
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_PAUSE) {
exoPlayer.pause()
} else if (event == Lifecycle.Event.ON_RESUME) {
exoPlayer.play()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
exoPlayer.release() // Free hardware decoders
}
}
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { ctx ->
PlayerView(ctx).apply {
player = exoPlayer
useController = true
}
},
update = { playerView ->
playerView.player = exoPlayer
}
)
}
Handling Unsupported Media Formats
ExoPlayer comes equipped with the device’s native MediaCodec. This means that the media player supports media formats used on websites such as MP4, HLS and DASH which encode videos in H.264 or H.265 and audio in AAC.
In case one tries to play media that contains legacy formats such as avi or mkv or proprietary audio formats such as AC3, DTS or TrueHD then the native Media Codec will fail. This will cause either an error or in case the video plays, it will be the audio only.
In order to overcome this issue there are two possible solutions:
1. Transcoding On The Server: This is the way to go about it for mobile applications. In case the user requests a media file or upload a file to the server then the server will convert the file into HLS stream format or MP4 format. This way all the media will play across all Android devices without draining the battery very much. This requires powerful servers.
2. Software Decoding On The Client Side: With the help of FFmpeg Extension.In case transcoding from the server side is not an option, then you can make your application decode media through CPU or hardware decoding of the device.
AndroidX Media3 offers this possibility using the ExoPlayer FFmpeg Extension.
This FFmpeg extension is not available as -compiled Gradle dependency due to complicated open source licensing. Therefore, the developers need to clone Media3 repositories and build the extension locally using the Android NDK.
After you have added this compiled library to your application, you can configure the DefaultRenderersFactory to use software decoding:
import androidx.media3.exoplayer.DefaultRenderersFactory
// Configure the player to utilize FFmpeg software decoding if the hardware fails
val renderersFactory = DefaultRenderersFactory(context)
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
val exoPlayer = ExoPlayer.Builder(context, renderersFactory)
.setMediaSourceFactory(mediaSourceFactory) // Defined in earlier steps
.build()
Software decoding requires much computer power. This aids with compatibility between various file formats. Using FFmpeg for processing large files would consume much battery power and cause issues with the video on slower computers. Thus, server-side transcoding is preferred when possible.
메타데이터
- post_id
- f3a169ebebdb
- slug
- implementation-of-advanced-video-playback-through-exoplayer-using-kotlin-f3a169ebebdb
- url
- https://medium.com/@esracangungor/implementation-of-advanced-video-playback-through-exoplayer-using-kotlin-f3a169ebebdb
- canonical_url
- https://medium.com/@esracangungor/implementation-of-advanced-video-playback-through-exoplayer-using-kotlin-f3a169ebebdb
- author_url
- https://medium.com/@esracangungor
- status
- ok
- fetched_at
- 2026-09-06 05:12:33