Add PreviousButtonState and NextButtonState to ui-compose

* Provide a helper Composable for remembering the state instance and launching the listening coroutine that observes the changes in the Player
* Add an example to demo-compose of using Previous- and Next- ButtonStates inside a Previous- and NextButton Composable.
* Reformat the MainActivity usage of `Previous`, `PlayPause`, `Next` buttons to form Minimal Player Controls

PiperOrigin-RevId: 691943147
This commit is contained in:
jbibik 2024-10-31 14:49:43 -07:00 committed by Copybara-Service
parent 676a3872a5
commit 1b302e879a
14 changed files with 595 additions and 93 deletions

View File

@ -57,8 +57,10 @@
* IMA extension:
* UI:
* Add `PlayerSurface` Composable to `media3-ui-compose` module.
* Add `PlayPauseButtonState` class and `rememberPlayPauseButtonState`
Composable to `media3-ui-compose` module.
* Add `PlayPauseButtonState`, `NextButtonState`, `PreviousButtonState`
classes and the corresponding `rememberPlayPauseButtonState`,
`rememberNextButtonState`, `rememberPreviousButtonState` Composables to
`media3-ui-compose` module.
* Downloads:
* OkHttp Extension:
* Cronet Extension:
@ -76,8 +78,9 @@
* Cast Extension:
* Test Utilities:
* Demo app:
* Add `PlayPauseButton` Composable UI element to `demo-compose` utilizing
`PlayPauseButtonState`.
* Add `PlayPauseButton`, `NextButton`, `PreviousButton` and
`MinimalControls` Composable UI elements to `demo-compose` utilizing
`PlayPauseButtonState`, `NextButtonState`, and `PreviousButtonState`.
* Remove deprecated symbols:
* Remove deprecated `AudioMixer.create()` method. Use
`DefaultAudioMixer.Factory().create()` instead.

View File

@ -19,18 +19,22 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.runtime.Composable
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.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.demo.compose.buttons.PlayPauseButton
import androidx.media3.demo.compose.buttons.MinimalControls
import androidx.media3.demo.compose.data.videos
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.compose.PlayerSurface
@ -45,21 +49,35 @@ class MainActivity : ComponentActivity() {
val context = LocalContext.current
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri(videos[0]))
setMediaItems(videos.map { MediaItem.fromUri(it) })
prepare()
playWhenReady = true
repeatMode = Player.REPEAT_MODE_ONE
}
}
MediaPlayerScreen(player = exoPlayer, modifier = Modifier.fillMaxSize())
MediaPlayerScreen(
player = exoPlayer,
modifier = Modifier.fillMaxSize().navigationBarsPadding(),
)
}
}
@Composable
private fun MediaPlayerScreen(player: Player, modifier: Modifier = Modifier) {
var showControls by remember { mutableStateOf(true) }
Box(modifier) {
PlayerSurface(player = player, surfaceType = SURFACE_TYPE_SURFACE_VIEW)
PlayPauseButton(player, Modifier.align(Alignment.Center).size(100.dp))
PlayerSurface(
player = player,
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
modifier =
modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null, // to prevent the ripple from the tap
) {
showControls = !showControls
},
)
if (showControls) {
MinimalControls(player, Modifier.align(Alignment.Center))
}
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.demo.compose.buttons
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.media3.common.Player
/**
* Minimal playback controls for a [Player].
*
* Includes buttons for seeking to a previous/next items or playing/pausing the playback.
*/
@Composable
internal fun MinimalControls(player: Player, modifier: Modifier = Modifier) {
val graySemiTransparentBackground = Color.Gray.copy(alpha = 0.4f)
val modifierForIconButton =
modifier.size(80.dp).background(graySemiTransparentBackground, CircleShape)
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
PreviousButton(player, modifierForIconButton)
PlayPauseButton(player, modifierForIconButton)
NextButton(player, modifierForIconButton)
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.demo.compose.buttons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.media3.common.Player
import androidx.media3.demo.compose.R
import androidx.media3.ui.compose.state.rememberNextButtonState
@Composable
internal fun NextButton(player: Player, modifier: Modifier = Modifier) {
val state = rememberNextButtonState(player)
IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
Icon(
Icons.Default.SkipNext,
contentDescription = stringResource(R.string.next_button),
modifier = modifier,
)
}
}

View File

@ -17,8 +17,8 @@
package androidx.media3.demo.compose.buttons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PauseCircle
import androidx.compose.material.icons.filled.PlayCircle
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
@ -31,7 +31,7 @@ import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
@Composable
internal fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
val state = rememberPlayPauseButtonState(player)
val icon = if (state.showPlay) Icons.Default.PlayCircle else Icons.Default.PauseCircle
val icon = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause
val contentDescription =
if (state.showPlay) stringResource(R.string.playpause_button_play)
else stringResource(R.string.playpause_button_pause)

View File

@ -0,0 +1,40 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.demo.compose.buttons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SkipPrevious
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.media3.common.Player
import androidx.media3.demo.compose.R
import androidx.media3.ui.compose.state.rememberPreviousButtonState
@Composable
internal fun PreviousButton(player: Player, modifier: Modifier = Modifier) {
val state = rememberPreviousButtonState(player)
IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
Icon(
Icons.Default.SkipPrevious,
contentDescription = stringResource(R.string.previous_button),
modifier = modifier,
)
}
}

View File

@ -18,6 +18,5 @@ package androidx.media3.demo.compose.data
val videos =
listOf(
"https://html5demos.com/assets/dizzy.mp4",
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
"https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm",
)

View File

@ -17,4 +17,6 @@
<string name="app_name">Media3 Compose Demo</string>
<string name="playpause_button_play">Play</string>
<string name="playpause_button_pause">Pause</string>
<string name="next_button">Next</string>
<string name="previous_button">Previous</string>
</resources>

View File

@ -0,0 +1,65 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.ui.compose.state
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.media3.common.Player
import androidx.media3.common.listen
import androidx.media3.common.util.UnstableApi
/**
* Remembers the value of [NextButtonState] created based on the passed [Player] and launch a
* coroutine to listen to [Player]'s changes. If the [Player] instance changes between compositions,
* produce and remember a new value.
*/
@UnstableApi
@Composable
fun rememberNextButtonState(player: Player): NextButtonState {
val nextButtonState = remember(player) { NextButtonState(player) }
LaunchedEffect(player) { nextButtonState.observe() }
return nextButtonState
}
/**
* State that holds all interactions to correctly deal with a UI component representing a
* seek-to-next button.
*
* This button has no internal state to maintain, it can only be enabled or disabled.
*
* @property[isEnabled] determined by `isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT)`
*/
@UnstableApi
class NextButtonState(private val player: Player) {
var isEnabled by mutableStateOf(player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT))
private set
fun onClick() {
player.seekToNext()
}
suspend fun observe(): Nothing =
player.listen { events ->
if (events.contains(Player.EVENT_AVAILABLE_COMMANDS_CHANGED)) {
isEnabled = isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT)
}
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.ui.compose.state
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.media3.common.Player
import androidx.media3.common.listen
import androidx.media3.common.util.UnstableApi
/**
* Remembers the value of [PreviousButtonState] created based on the passed [Player] and launch a
* coroutine to listen to [Player]'s changes. If the [Player] instance changes between compositions,
* produce and remember a new value.
*/
@UnstableApi
@Composable
fun rememberPreviousButtonState(player: Player): PreviousButtonState {
val previousButtonState = remember(player) { PreviousButtonState(player) }
LaunchedEffect(player) { previousButtonState.observe() }
return previousButtonState
}
/**
* State that holds all interactions to correctly deal with a UI component representing a
* seek-to-previous button.
*
* This button has no internal state to maintain, it can only be enabled or disabled.
*
* @property[isEnabled] determined by `isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS)`
*/
@UnstableApi
class PreviousButtonState(private val player: Player) {
var isEnabled by mutableStateOf(player.isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS))
private set
fun onClick() {
player.seekToPrevious()
}
suspend fun observe(): Nothing =
player.listen { events ->
if (events.contains(Player.EVENT_AVAILABLE_COMMANDS_CHANGED)) {
isEnabled = isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS)
}
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.ui.compose.state
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.media3.common.Player
import androidx.media3.ui.compose.utils.TestPlayer
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/** Unit test for [NextButtonState]. */
@RunWith(AndroidJUnit4::class)
class NextButtonStateTest {
@get:Rule val composeTestRule = createComposeRule()
@Test
fun addSeekNextCommandToPlayer_buttonStateTogglesFromDisabledToEnabled() {
val player = TestPlayer()
player.playbackState = Player.STATE_READY
player.playWhenReady = true
player.removeCommands(Player.COMMAND_SEEK_TO_NEXT)
lateinit var state: NextButtonState
composeTestRule.setContent { state = rememberNextButtonState(player = player) }
assertThat(state.isEnabled).isFalse()
player.addCommands(Player.COMMAND_SEEK_TO_NEXT)
composeTestRule.waitForIdle()
assertThat(state.isEnabled).isTrue()
}
@Test
fun removeSeekNextCommandToPlayer_buttonStateTogglesFromEnabledToDisabled() {
val player = TestPlayer()
player.playbackState = Player.STATE_READY
player.playWhenReady = true
lateinit var state: NextButtonState
composeTestRule.setContent { state = rememberNextButtonState(player = player) }
assertThat(state.isEnabled).isTrue()
player.removeCommands(Player.COMMAND_SEEK_TO_NEXT)
composeTestRule.waitForIdle()
assertThat(state.isEnabled).isFalse()
}
@Test
fun clickNextOnPenultimateMediaItem_buttonStateTogglesFromEnabledToDisabled() {
val player = TestPlayer()
player.playbackState = Player.STATE_READY
player.playWhenReady = true
lateinit var state: NextButtonState
composeTestRule.setContent { state = rememberNextButtonState(player = player) }
assertThat(state.isEnabled).isTrue()
player.seekToNext()
composeTestRule.waitForIdle()
assertThat(state.isEnabled).isFalse()
}
@Test
fun playerInReadyState_buttonClicked_nextItemPlaying() {
val player = TestPlayer()
player.playbackState = Player.STATE_READY
player.playWhenReady = true
val state = NextButtonState(player)
assertThat(player.currentMediaItemIndex).isEqualTo(0)
state.onClick()
assertThat(player.currentMediaItemIndex).isEqualTo(1)
}
}

View File

@ -16,15 +16,11 @@
package androidx.media3.ui.compose.state
import android.os.Looper
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.media3.common.Player
import androidx.media3.common.SimpleBasePlayer
import androidx.media3.ui.compose.utils.TestPlayer
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.collect.ImmutableList
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ -133,74 +129,3 @@ class PlayPauseButtonStateTest {
assertThat(player.playbackState).isEqualTo(Player.STATE_BUFFERING)
}
}
private class TestPlayer : SimpleBasePlayer(Looper.myLooper()!!) {
private var state =
State.Builder()
.setAvailableCommands(Player.Commands.Builder().addAllCommands().build())
.setPlaylist(ImmutableList.of(MediaItemData.Builder(/* uid= */ Any()).build()))
.build()
override fun getState(): State {
return state
}
override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> {
state =
state
.buildUpon()
.setPlayWhenReady(playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
.build()
return Futures.immediateVoidFuture()
}
override fun handlePrepare(): ListenableFuture<*> {
state =
state
.buildUpon()
.setPlayerError(null)
.setPlaybackState(if (state.timeline.isEmpty) STATE_ENDED else STATE_BUFFERING)
.build()
return Futures.immediateVoidFuture()
}
override fun handleSeek(
mediaItemIndex: Int,
positionMs: Long,
seekCommand: @Player.Command Int,
): ListenableFuture<*> {
state =
state.buildUpon().setPlaybackState(STATE_BUFFERING).setContentPositionMs(positionMs).build()
return Futures.immediateVoidFuture()
}
fun setPlaybackState(playbackState: @Player.State Int) {
state = state.buildUpon().setPlaybackState(playbackState).build()
invalidateState()
}
fun setPosition(positionMs: Long) {
state = state.buildUpon().setContentPositionMs(positionMs).build()
invalidateState()
}
fun removeCommands(vararg commands: @Player.Command Int) {
state =
state
.buildUpon()
.setAvailableCommands(
Player.Commands.Builder().addAllCommands().removeAll(*commands).build()
)
.build()
invalidateState()
}
fun addCommands(vararg commands: @Player.Command Int) {
state =
state
.buildUpon()
.setAvailableCommands(Player.Commands.Builder().addAll(*commands).build())
.build()
invalidateState()
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.ui.compose.state
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.media3.common.Player
import androidx.media3.ui.compose.utils.TestPlayer
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/** Unit test for [PreviousButtonState]. */
@RunWith(AndroidJUnit4::class)
class PreviousButtonStateTest {
@get:Rule val composeTestRule = createComposeRule()
@Test
fun addSeekPrevCommandToPlayer_buttonStateTogglesFromDisabledToEnabled() {
val player = TestPlayer()
player.playbackState = Player.STATE_READY
player.playWhenReady = true
player.removeCommands(Player.COMMAND_SEEK_TO_PREVIOUS)
lateinit var state: PreviousButtonState
composeTestRule.setContent { state = rememberPreviousButtonState(player = player) }
assertThat(state.isEnabled).isFalse()
composeTestRule.runOnUiThread { player.addCommands(Player.COMMAND_SEEK_TO_PREVIOUS) }
composeTestRule.waitForIdle()
assertThat(state.isEnabled).isTrue()
}
@Test
fun removeSeekPrevCommandToPlayer_buttonStateTogglesFromEnabledToDisabled() {
val player = TestPlayer()
player.playbackState = Player.STATE_READY
player.playWhenReady = true
lateinit var state: PreviousButtonState
composeTestRule.setContent { state = rememberPreviousButtonState(player = player) }
assertThat(state.isEnabled).isTrue()
composeTestRule.runOnUiThread { player.removeCommands(Player.COMMAND_SEEK_TO_PREVIOUS) }
composeTestRule.waitForIdle()
assertThat(state.isEnabled).isFalse()
}
@Test
fun playerInReadyState_prevButtonClicked_sameItemPlayingFromBeginning() {
val player = TestPlayer()
player.playbackState = Player.STATE_READY
player.playWhenReady = true
val state = PreviousButtonState(player)
assertThat(player.currentMediaItemIndex).isEqualTo(0)
state.onClick()
assertThat(player.currentMediaItemIndex).isEqualTo(0)
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.ui.compose.utils
import android.os.Looper
import androidx.media3.common.Player
import androidx.media3.common.SimpleBasePlayer
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
/**
* A fake [Player] that uses [SimpleBasePlayer]'s minimal number of default methods implementations
* to build upon to simulate realistic playback scenarios for testing.
*/
internal class TestPlayer : SimpleBasePlayer(Looper.myLooper()!!) {
private var state =
State.Builder()
.setAvailableCommands(Player.Commands.Builder().addAllCommands().build())
.setPlaylist(
ImmutableList.of(
MediaItemData.Builder(/* uid= */ "First").build(),
MediaItemData.Builder(/* uid= */ "Second").build(),
)
)
.setPlayWhenReady(true, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
.build()
override fun getState(): State {
return state
}
override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> {
state =
state
.buildUpon()
.setPlayWhenReady(playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
.build()
return Futures.immediateVoidFuture()
}
override fun handlePrepare(): ListenableFuture<*> {
state =
state
.buildUpon()
.setPlayerError(null)
.setPlaybackState(if (state.timeline.isEmpty) STATE_ENDED else STATE_BUFFERING)
.build()
return Futures.immediateVoidFuture()
}
override fun handleSeek(
mediaItemIndex: Int,
positionMs: Long,
seekCommand: @Player.Command Int,
): ListenableFuture<*> {
state =
state
.buildUpon()
.setPlaybackState(STATE_BUFFERING)
.setCurrentMediaItemIndex(mediaItemIndex)
.setContentPositionMs(positionMs)
.build()
if (mediaItemIndex == state.playlist.size - 1) {
removeCommands(Player.COMMAND_SEEK_TO_NEXT)
}
return Futures.immediateVoidFuture()
}
fun setPlaybackState(playbackState: @Player.State Int) {
state = state.buildUpon().setPlaybackState(playbackState).build()
invalidateState()
}
fun setPosition(positionMs: Long) {
state = state.buildUpon().setContentPositionMs(positionMs).build()
invalidateState()
}
fun removeCommands(vararg commands: @Player.Command Int) {
state =
state
.buildUpon()
.setAvailableCommands(
Player.Commands.Builder().addAllCommands().removeAll(*commands).build()
)
.build()
invalidateState()
}
fun addCommands(vararg commands: @Player.Command Int) {
state =
state
.buildUpon()
.setAvailableCommands(Player.Commands.Builder().addAll(*commands).build())
.build()
invalidateState()
}
}