2

I'm trying to use Jetpack Compose with ExoPlayer

Problems

  • When orientation is changed, ExoPlayer restarts video, but previous playback still exists and plays in the background. For example, if I rotate the phone twice, there are three sound tracks playing, with the latest video in the foreground
  • exoPlayer.duration is always TIME_UNSET (Long.MIN_VALUE + 1) even when PlaybackState changes to Player.STATE_READY -> This was fixed by using exoPlayerState.exoPlayer.duration.

Versions

Jetpack Compose version: 1.0.4 (latest as of now) ExoPlayer version: 2.15.1 (latest as of now)

Code

Here's my Player Composable and its implementation

class ExoPlayerState(context: Context) {
    val exoPlayer = SimpleExoPlayer.Builder(context).build()
    val duration by mutableStateOf(exoPlayer.duration)
    val bufferedPosition by mutableStateOf(exoPlayer.bufferedPosition)
    var position by mutableStateOf(exoPlayer.currentPosition)
}

@Composable
fun Player(
    modifier: Modifier = Modifier,
    sourceUrl: String
) {
    val context = LocalContext.current
    val exoPlayerState by remember(context) { mutableStateOf(ExoPlayerState(context)) } // <---- Problem 1?
    LaunchedEffect(sourceUrl) {
        exoPlayerState.exoPlayer.addListener(object : Player.Listener {
            override fun onPlaybackStateChanged(playbackState: Int) {
                when (playbackState) {
                    Player.STATE_READY -> {
                        Log.d("Player", "STATE_READY- duration: ${exoPlayerState.duration}") // <----- Problem 2
                    }

                    Player.STATE_ENDED -> {}

                    Player.STATE_BUFFERING, Player.STATE_IDLE -> {}
                }
            }
        })

        val mediaSource = generateMediaSource(context, sourceUrl)
        exoPlayerState.exoPlayer.setMediaSource(mediaSource)
        exoPlayerState.exoPlayer.prepare()
    }

    AndroidView(factory = {
        PlayerView(it).apply {
            player = exoPlayerState.exoPlayer
            useController = false
            (player as SimpleExoPlayer).playWhenReady = true
        }
    })

    // my custom player controller composable

    PlayerOverlay(
        exoPlayerState = exoPlayerState,
        onValueChangeFinished = {
            exoPlayerState.exoPlayer.seekTo(exoPlayerState.position)
        },
        modifier = modifier
    )
}

private fun generateMediaSource(context: Context, videoUrl: String): MediaSource {
    val mediaItem = MediaItem.Builder()
        .setUri(Uri.parse(videoUrl))
        .setDrmSessionForClearPeriods(true)
        .build()
    return DefaultMediaSourceFactory(buildDataSourceFactory(context)).createMediaSource(mediaItem)
}

private fun buildDataSourceFactory(context: Context): DataSource.Factory {
    return DefaultDataSourceFactory(
        context,
        getDefaultHttpDataSourceFactory(context)
    )
}

private fun getDefaultHttpDataSourceFactory(context: Context): HttpDataSource.Factory {
    return DefaultHttpDataSource.Factory()
        .setUserAgent(Util.getUserAgent(context, context.packageName))
}

My Guess

My guess is that I'm now properly saving states of exoplayer and updating it? I thought creating ExoPlayerState and remembering it in the composable would properly handle this, but apparently that was not enough.

btw, my custom seekbar logic works fine. It properly seeks the video to the saved position.

onValueChangeFinished = {
    exoPlayerState.exoPlayer.seekTo(exoPlayerState.position)
}
Saehun Sean Oh
  • 2,103
  • 1
  • 21
  • 41

2 Answers2

1

It can be fixed by adding.

val context = LocalCotext.current

context.findActivity()?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT

fun Context.findActivity(): Activity = when(this){

is Activity -> this

is ContextWrapper -> baseContext.findActivity()

else -> null
danday74
  • 52,471
  • 49
  • 232
  • 283
Isaac
  • 11
  • 3
0

First, you are using remember, which itself gets destroyed upon a configuration change. rememberSaveable is what you want to use instead.

Secondly, you should never store such important state info in the context of a Composable. It is always recommended to store this in a ViewModel instead, so it stays preserved through the lifetime of the app.

Your player seeks to origin since upon the destruction of the remembered value, a new state is created and then remembered.

Just store the state in a ViewModel and use state hoisting to read and update the state. Learn more here

Richard Onslow Roper
  • 5,477
  • 2
  • 11
  • 42