Best Website for Jetpack Compose App Development

Android Jetpack Compose

Stay ahead with the latest tools, trends, and best practices in Android Jetpack Compose App development

Modern Video Player in Jetpack Compose

Modern Video Player in Jetpack Compose - Coding Bihar
How to build a Modern Video Player in Jetpack Compose

Building a Modern Video Player in Jetpack Compose with Media3 ExoPlayer

Video content is an essential part of modern Android apps, from social media platforms to educational applications. With the evolution of Android development, Jetpack Compose has revolutionized UI design, providing a declarative and concise way to build layouts. At the same time, Media3 ExoPlayer offers a robust solution for playing audio and video content efficiently. Combining these two powerful tools allows developers to create immersive, full-featured video players with minimal code. In this article, we will explore how to build a modern video player in Jetpack Compose using Media3 ExoPlayer, covering everything from initialization to full-screen playback.

Why Use Media3 ExoPlayer with Jetpack Compose?

ExoPlayer has long been the preferred solution for media playback on Android due to its flexibility, performance, and wide range of features. Media3 is the latest iteration of ExoPlayer, designed to integrate seamlessly with modern Android architecture components. When paired with Jetpack Compose, developers can:
  • Create dynamic UIs that react to state changes naturally.
  • Implement full-screen playback with ease.
  • Manage multiple video streams or playlists efficiently.
  • Customize controls like play, pause, seek, and volume.
Using Media3 ensures the video player is future-proof, optimized for performance, and compatible with the latest Android API levels.

Setting Up the Project

1. Add Dependencies

In your build.gradle (app) file:
To get started, include the following dependencies in your build.gradle file:
              // ExoPlayer
    implementation("androidx.media3:media3-exoplayer:1.8.0")
    implementation("androidx.media3:media3-ui:1.8.0")
             // Permission handling
    implementation("com.google.accompanist:accompanist-permissions:0.37.3")
    implementation ("androidx.navigation:navigation-compose:2.9.2")

    implementation("io.coil-kt:coil-compose:2.4.0")
In AndroidManifest.xml add permissions:
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="32" />
This includes the core ExoPlayer library, UI components, and Jetpack Compose essentials.

2. Data Model

data class VideoItem(val uri: Uri, val name: String, val thumbnail: Bitmap?)

3. Load Device Videos

We use MediaStore to fetch video files and thumbnails:
fun loadVideos(context: Context): List {
    val videos = mutableListOf()
    val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
    } else {
        MediaStore.Video.Media.EXTERNAL_CONTENT_URI
    }

    val projection = arrayOf(
        MediaStore.Video.Media._ID,
        MediaStore.Video.Media.DISPLAY_NAME
    )

    context.contentResolver.query(
        collection,
        projection,
        null,
        null,
        "${MediaStore.Video.Media.DATE_ADDED} DESC"
    )?.use { cursor ->
        val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
        val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)

        while (cursor.moveToNext()) {
            val id = cursor.getLong(idColumn)
            val name = cursor.getString(nameColumn)
            val contentUri = ContentUris.withAppendedId(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
                id
            )

            val thumbnail: Bitmap? = try {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    context.contentResolver.loadThumbnail(
                        contentUri,
                        Size(200, 200), // thumbnail size
                        null
                    )
                } else {
                    @Suppress("DEPRECATION")
                    MediaStore.Video.Thumbnails.getThumbnail(
                        context.contentResolver,
                        id,
                        MediaStore.Video.Thumbnails.MINI_KIND,
                        null
                    )
                }
            } catch (_: Exception) {
                null
            }

            videos.add(VideoItem(contentUri, name, thumbnail))
        }
    }
    return videos
}

4. Main App with Permission Handling

package com.codingbihar.videoplayer

import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController


@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VideoApp() {
    val context = LocalContext.current
    val navController = rememberNavController()

    // Pick correct permission for the Android version
    val requiredPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        Manifest.permission.READ_MEDIA_VIDEO
    } else {
        Manifest.permission.READ_EXTERNAL_STORAGE
    }

    // Initial permission check
    val initialPermission = ContextCompat.checkSelfPermission(
        context,
        requiredPermission
    ) == PackageManager.PERMISSION_GRANTED

    var hasPermission by remember { mutableStateOf(initialPermission) }
    var permissionChecked by remember { mutableStateOf(false) } // track if check completed
    var videos by remember { mutableStateOf>(emptyList()) }

    // Launcher to request permission
    val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
        hasPermission = granted
        if (granted) {
            videos = loadVideos(context)
        }
    }

    // Ensure videos load after initial permission check
    LaunchedEffect(hasPermission) {
        if (hasPermission) {
            videos = loadVideos(context)
        }
        permissionChecked = true
    }

    // If permission check not done yet, show loading
    if (!permissionChecked) {
        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            CircularProgressIndicator()
        }
        return
    }

    if (!hasPermission) {
        // Show permission UI
        Scaffold(topBar = { TopAppBar(title = { Text("Videos") }) }) { padding ->
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(padding),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                Text("Permission required to access device videos")
                Spacer(modifier = Modifier.height(8.dp))
                Button(onClick = { launcher.launch(requiredPermission) }) {
                    Text("Grant permission")
                }
            }
        }
    } else {
        // Permission granted: show videos
        NavHost(navController = navController, startDestination = "video_list") {
            composable("video_list") {
                VideoListScreen(navController, videos)
            }
            composable("player/{index}") { backStackEntry ->
                val index = backStackEntry.arguments?.getString("index")?.toIntOrNull() ?: 0
                if (videos.isNotEmpty()) {
                    VideoPlayerScreen(uris = videos.map { it.uri }, startIndex = index)
                } else {
                    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                        Text("No videos")
                    }
                }
            }
        }
    }
}

5. Video List Screen

package com.codingbihar.videoplayer

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController


@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VideoListScreen(navController: NavHostController, videos: List) {
    Scaffold(topBar = { TopAppBar(title = { Text("Device videos") }) }) { padding ->
        if (videos.isEmpty()) {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(padding),
                contentAlignment = Alignment.Center
            ) {
                Text("No videos found")
            }
        } else {
            LazyColumn(modifier = Modifier.fillMaxSize().padding(padding)) {
                itemsIndexed(videos) { index, item ->
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .clickable { navController.navigate("player/$index") }
                            .padding(8.dp),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        if (item.thumbnail != null) {
                            Image(
                                bitmap = item.thumbnail.asImageBitmap(),
                                contentDescription = null,
                                modifier = Modifier
                                    .size(96.dp)
                                    .clip(RoundedCornerShape(8.dp)),
                                contentScale = ContentScale.Crop
                            )
                        } else {
                            Box(
                                modifier = Modifier
                                    .size(96.dp)
                                    .clip(RoundedCornerShape(8.dp))
                                    .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)),
                                contentAlignment = Alignment.Center
                            ) {
                                Text("No\nthumb", fontSize = 12.sp)
                            }
                        }

                        Spacer(modifier = Modifier.width(12.dp))

                        Column(modifier = Modifier.weight(1f)) {
                            Text(item.name, maxLines = 1, overflow = TextOverflow.Ellipsis)
                            Text(item.uri.lastPathSegment ?: item.uri.toString(), maxLines = 1,
                                overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.headlineMedium)
                        }
                    }
                }
            }
        }
    }
}

6. Video Player Screen 

Here you can embed Media3 PlayerView inside Compose:
package com.codingbihar.videoplayer

import android.content.res.Configuration
import android.net.Uri
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView


@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VideoPlayerScreen(uris: List, startIndex: Int = 0) {
    val context = LocalContext.current
    var player by remember { mutableStateOf(null) }
    val configuration = LocalConfiguration.current
    val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE

    // create player
    DisposableEffect(uris, startIndex) {
        val exo = ExoPlayer.Builder(context).build().apply {
            val items = uris.map { MediaItem.fromUri(it) }
            setMediaItems(items, startIndex, C.TIME_UNSET)
            prepare()
            playWhenReady = true
        }
        player = exo

        onDispose {
            player?.release()
            player = null
        }
    }

    if (isLandscape) {
        // Fullscreen mode
        Box(modifier = Modifier.fillMaxSize()) {
            AndroidView(
                factory = { ctx ->
                    PlayerView(ctx).apply {
                        useController = true
                    }
                },
                update = { it.player = player },
                modifier = Modifier.fillMaxSize()
            )
        }
    } else {
        // Normal portrait mode with toolbar + controls
        Scaffold(topBar = { TopAppBar(title = { Text("Player") }) }) { padding ->
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(padding)
            ) {
                AndroidView(
                    factory = { ctx ->
                        PlayerView(ctx).apply {
                            useController = true
                            keepScreenOn = true
                        }
                    },
                    update = { it.player = player },
                    modifier = Modifier.fillMaxSize()
                )
            }
        }
    }
}

🎉 Final Result:

On launch, the app asks for storage permission. If granted, it lists all device videos with thumbnails. Clicking a video opens the Media3 ExoPlayer with playback controls. Player automatically pauses on background and releases when destroyed.
Media3 Exo Player Screenshot

Media3 Exo Player Screenshot

Media3 Exo Player Screenshot
👉 This is a minimal, production-ready Compose video player app. Next steps you can add: Seekbar with progress updates Playback speed control Shuffle / repeat buttons Double-tap to seek gestures Fullscreen toggle
 Sandeep Kumar

Posted by Sandeep Kumar

Please share your feedback us at:sandeep@codingbihar.com. Thank you for being a part of our community!

Special Message

Welcome to Coding Bihar