1

I have the MediaPlayer configured inside my ViewModel. All I want is a way to observe the mediaPlayer.currentPosition in a Composable. However, I cannot find a way to store it in a MutableState<T> value for Compose to observe. I just have a simple slider:

Slider(value = someMutableStateValue, onValueChange = { someMutableStateValue = it }

In my ViewModel, I declare var someMutableStateValue by mutableStateOf(0f)

Now, this app is being built for TV so I cannot provide touch input whatsoever. I don't think it would be required anyway, but I'm informing just in case.

All I want, is a method that will update the value of someMutableStateValue every time the MediaPlayer.currentPosition changes. Maybe there is some listener, but I can't find it as of now. I'm comfortable with using LiveData too, but I'd like to avoid that if possible.

PS: I actually do not even need to pass anything to the onValueChange listener, since it would never be called on a TV.

PS MArk II: Also, I cannot find out why the Slider take up all the width of my Screen? Is it the default implementation? Even hardcoding the width or for that matter, using the fillMaxWidth(...) Modifier with a restricted fraction doesn't seem to work.

vighnesh153
  • 4,354
  • 2
  • 13
  • 27
Richard Onslow Roper
  • 5,477
  • 2
  • 11
  • 42
  • Please add a code sample of how you try to render your slider – Adrian K Nov 19 '21 at 09:04
  • It has `Modifier.fillMaxWidth()` inside, which is what you should expect from a non strict content size control.`Modifier.fillMaxWidth(0.5f)` works fine to me with `Slider` (not testing it on TV, may be that platform bug?) – Phil Dukhov Nov 19 '21 at 09:13
  • This issue doesn't seems related to Compose. Have you checked [this answer](https://stackoverflow.com/a/10001044/3585796)? It basically does same what @Adrian suggests. – Phil Dukhov Nov 19 '21 at 09:17
  • I thought we would have something to do with recompositions so I added the tag. Anyway, the answer suggests using Handlers and everything. Idk but isn't that kinda old school? I thought instead there'd be a way to leverage the benefits of declarative-ness of Compose. Adrian's answer demonstrates that. I guess it'll be alright, but it feels more of a workaround. I'll wait for a couple hours to see if anyone else gets some 'official' method. Adrian's would work flawlessly so there's no doubt I'll accept it otherwise. – Richard Onslow Roper Nov 19 '21 at 11:29
  • And yes, it renders fine on mobile. Maybe a platform bug. – Richard Onslow Roper Nov 19 '21 at 11:31
  • @PhilipDukhov I didn't find any `fillMaxWidth(...)` in `Slider` source? Where were you referring to? – Richard Onslow Roper Nov 19 '21 at 12:03
  • @MARSK `Slider` -> `SliderImpl` -> `Track` – Phil Dukhov Nov 19 '21 at 12:08

2 Answers2

3

It appears MediaPlayer doesn't offer a callback mechanism for the playback progress. I would guess this is because technically the progress would change with every frame but running a callback on every frame would impose a lot of CPU overhead, so they just omitted it alltogether. Instead, you need to resort to polling the progress. If you want to do this on the composable side, you could use LaunchedEffect:

var progress by remember { mutableStateOf(0f) }
LaunchedEffect(Unit){
    while(isActive){
        progress = mediaPlayer.currentPosition / mediaPlayer.duration
        delay(200) // change this to what feels smooth without impacting performance too much
    }
}
Adrian K
  • 3,942
  • 12
  • 15
  • Actually it kinda partially answers the question. Idk if it was the lack of info in the question or what, but is there a way that I can do all this calculation inside my `ViewModel`? You see, the way you are using a while loop and everything, this is completely achievable in a Composable (I already thought of this, but forgot to mention in the question.), but I can't figure out where I should place this while loop in a `ViewModel`. – Richard Onslow Roper Nov 19 '21 at 11:57
  • Also, instead of `Unit`, `mediaplayer.isPlaying()` should be passed to the `LaunchedEffect`. Other than that, instead of an infinite loop, you should pass the same boolean to the loop as well, so that it does not keep modifying the variable even when the player is paused. Shorter fix would be to just keep the code the same way, and add a conditional in the infinite loop, breaking it upon player pause. Got it? – Richard Onslow Roper Nov 19 '21 at 12:00
  • Seems like there is literally no other way. Is it ok on the processor? I mean the performance must not be impacted you know. I'll accept it since it works just the way I wanted it to. – Richard Onslow Roper Nov 19 '21 at 13:11
  • Of course you are right, there are some optimizations, like stopping to poll when the player is paused, or increasing the polling interval. But generally it seems this is the way to do it, I recently wondered how to achieve the same thing using exoplayer and there it seems to be done in the same manner – Adrian K Nov 19 '21 at 13:46
1

This is based on @Adriak K's answer above with a small improvement for ExoPlayer(v2.16.1) in case anyone else is looking into this:

var isPlaying by remember {
        mutableStateOf(false)
    }

isPlaying can be updated as below to keep track of player pause/resume state:

exoplayer.addListener(object : Player.Listener {
                    override fun onPlaybackStateChanged(playbackState: Int) {
                        super.onPlaybackStateChanged(playbackState)
                        isPlaying = playbackState == Player.STATE_READY && isPlaying()
                    }

                    override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
                        super.onPlayWhenReadyChanged(playWhenReady, reason)
                        isPlaying = playWhenReady
                    }
                })


LaunchedEffect(key1 = player, key2 = isPlaying) {
            while (isActive && isPlaying) {
                sliderPosition = (player.currentPosition + 0.0f) / player.duration               
                delay(200)
        }
}
Rez
  • 4,501
  • 1
  • 30
  • 27