Building a custom video player in Android often feels needlessly complex. Legacy libraries cause memory leaks, create lifecycle headaches, and refuse to cooperate with modern UI toolkits. You end up wasting hours debugging hardware decoders instead of shipping features.
The solution is using the Media3 ExoPlayer Android library alongside Jetpack Compose. This guide walks you through building a modern, seamless video playback experience for both network streams and local files.
Why Use Media3 ExoPlayer for Android Video Streaming?
Media3 is the latest media library from Google, succeeding the original ExoPlayer and MediaCompat libraries. It simplifies the integration between the underlying playback engine and your app’s UI.
Before diving into the code, you need to understand three core concepts:
- ExoPlayer: The engine that handles media loading, buffering, and decoding.
- PlayerView: The UI component that renders the video and provides standard playback controls.
- MediaItem: A representation of your media source (URL or local file) and its metadata.
Project Setup: Adding Media3 Dependencies
Setting up your project correctly ensures your video player runs smoothly across all Android versions.
Gradle Dependencies for ExoPlayer
Add the following to your build.gradle.kts (Module: app) file. For this tutorial, we are using Media3 version 1.5.1.
dependencies {
// Core ExoPlayer functionality
implementation("androidx.media3:media3-exoplayer:1.5.1")
// UI components like PlayerView
implementation("androidx.media3:media3-ui:1.5.1")
// Shared media constants and classes
implementation("androidx.media3:media3-common:1.5.1")
}
AndroidManifest Permissions for Streaming
To stream video Android apps require the INTERNET permission. If you plan to play local files, you must also request storage permissions.
<!-- AndroidManifest.xml -->
<manifest ...>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<application
...
android:usesCleartextTraffic="true"> <!-- Needed for http:// (non-https) streams -->
...
</application>
</manifest>
Building the Video Player in Jetpack Compose
Integrating an ExoPlayer Jetpack Compose architecture requires bridging traditional Android Views with composables.
Initializing the ExoPlayer Instance
The player must be tied to your Activity’s lifecycle. Initialize it in onCreate and always release it in onDestroy to free up hardware decoders and prevent memory leaks.
class MainActivity : ComponentActivity() {
lateinit var player: ExoPlayer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. Initialize the player
player = ExoPlayer.Builder(this).build()
setContent {
// UI Code here
}
}
override fun onDestroy() {
super.onDestroy()
// 2. Always release the player to prevent memory leaks
player.release()
}
}
Bridging PlayerView with AndroidView
Because PlayerView is a traditional Android View, we use AndroidView to host it inside our Compose UI. This creates a reliable Compose bridge for your video surface.
@Composable
fun VideoPlayerSurface(
player: Player?,
modifier: Modifier = Modifier
) {
AndroidView(
factory = { context ->
PlayerView(context).apply {
this.player = player
// Configure UI settings here
this.useController = true
}
},
update = { view ->
// Keep the view in sync with the player state
view.player = player
},
modifier = modifier
)
}
How to Stream Video from a URL in Android
Playing a network stream is straightforward. Create a MediaItem from your URI, set it to the player, and call prepare().
fun playFromUrl(url: String) {
val mediaItem = MediaItem.fromUri(url)
player.setMediaItem(mediaItem)
player.prepare() // Player starts buffering the video stream
player.play() // Player starts visuals
}
How to Play Local Video Files
If you want users to pick a video from their gallery, use the modern ActivityResultContracts . This handles the local file playback securely without requiring broad storage access.
val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri: Uri? ->
uri?.let {
player.setMediaItem(MediaItem.fromUri(it))
player.prepare()
player.play()
}
}
// Trigger in UI
Button(onClick = { filePickerLauncher.launch("video/*") }) {
Text("Select Local Video")
}
Best Practices for Android Video Playback
To ensure your media3 library setup performs optimally, keep these key takeaways in mind:
- Lifecycle Management: Always call
player.release()inonDestroy. - Cleartext Traffic: Enable
android:usesCleartextTraffic="true"in the manifest if your video URLs are nothttps. - Aspect Ratio: Use
Modifier.aspectRatio(16f / 9f)on yourVideoPlayerSurfaceto ensure a consistent video box. - Prepare then Play: Always call
player.prepare()beforeplayer.play()to ensure the engine starts loading the stream early.
FAQ: Media3 ExoPlayer in Jetpack Compose
What is the difference between ExoPlayer and Media3? Media3 is the newest media library suite from Google. ExoPlayer is the playback engine within Media3. Moving to Media3 standardizes the API and integrates better with modern Android components.
How do I release an ExoPlayer in Jetpack Compose?
You should release the ExoPlayer by calling player.release() in the onDestroy() lifecycle method of your hosting Activity or Fragment. This frees up hardware decoders and prevents memory leaks.
Does ExoPlayer support Jetpack Compose natively?
Media3 does not have native Compose components yet. You bridge this gap by wrapping the traditional PlayerView inside an AndroidView composable function.
📌 Full Course Playlist https://www.youtube.com/playlist?list=PLO1OrQEU0vHNmD9Xqzs-qXwzzwrDvdhVu
~ ~ THANK YOU FOR READING ~ ~