Search code examples
android-mediaplayerexoplayerandroid-mediasessionandroid-media3exoplayer-media-item

Android Jetpack Compose Media3 HLS Stream with playback service


I would like to get HLS streaming working using Media3 with a background playback service. I see references online on how to use MediaController setMediaItem in the activity/composable, however, not sure how to use media source which also uses custom HTTP header. Here is my composable function.

@Composable
fun PlayerTest(
    token: String,
    url: String,
) {
    val context = LocalContext.current

    val dataSourceFactory = DataSource.Factory {
        val dataSource = DefaultHttpDataSource.Factory().createDataSource()
        dataSource.setRequestProperty("Authorization", token)
        dataSource
    }

    val hlsMediaSource = HlsMediaSource.Factory(dataSourceFactory)
        .createMediaSource(MediaItem.fromUri(url))

    val sessionToken = SessionToken(context, ComponentName(context, PlaybackService::class.java))
    val controllerFuture = MediaController.Builder(context, sessionToken).buildAsync()

    DisposableEffect(
        Box {
            AndroidView(
                factory = {
                    PlayerView(context).apply {
                        resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL
                        useController = false
                        layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
                    }
                },
                update = { playerView ->
                    controllerFuture.addListener({
                        val controller = controllerFuture.get()
                        controller.setMediaSource(hlsMediaSource) // <--- NOT FOUND
                        playerView.setPlayer(controller)
                    }, MoreExecutors.directExecutor())
                }
            )

            AudioPlayer(...)
        },
    ) {
        onDispose {
            MediaController.releaseFuture(controllerFuture)
        }
    }
}

Also, I don't see options in MediaItem.Builder() that takes media source. I am using Media3 version 1.3.0. Any suggestions?

The method setMediaSource is available in ExoPlayer implementation which is created in the service. Or should I let service build the URL and pass setMediaSource instead of doing it in the composable?


Solution

  • It doesn't need to think how media are operated on UI layer because MediaSessionService can cope with it. UI layer just connects MediaSessionService through SessionToken which builds MediaController, and you can set it to PlayerView.

    SampleVideoScreen.kt

    @Composable
    fun SampleVideoScreen(viewModel: SampleVideoViewModel) {
        val context = LocalContext.current
        DisposableEffect(viewModel) {
            viewModel.prepare(context)
            onDispose { viewModel.dispose() }
        }
    
        val controller by viewModel.mediaController.observeAsState()
        AndroidView(
            factory = {
                PlayerView(context)
            },
            update = { playerView ->
                playerView.player = controller
            }
        )
    
    }
    

    SampleVideoViewModel.kt

    class SampleVideoViewModel: ViewModel() {
        private var sessionToken: SessionToken? = null
        val mediaController = MutableLiveData<MediaController>()
    
        fun prepare(context: Context) {
            viewModelScope.launch(Dispatchers.IO) {
                val playIntent = Intent(context, PlaybackService::class.java)
                    .setAction("playSample")
                    .putExtra("url", "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8")
                context.startService(playIntent)
                val component = ComponentName(context, PlaybackService::class.java)
                sessionToken = SessionToken(context, component).also { session ->
                    MediaController.Builder(context, session)
                        .buildAsync().get().also { controller ->
                            mediaController.postValue(controller)
                        }
                }
            }
        }
    
        fun dispose() {
            mediaController.value?.clearMediaItems()
            mediaController.postValue(null)
        }
    }