From 1ba21e937e348db1bad6e851f32d56ca1fc8da98 Mon Sep 17 00:00:00 2001 From: Chiko Shimizu Date: Fri, 7 Mar 2025 18:01:20 +0900 Subject: [PATCH 1/3] Enable playlist on the video player screen. --- .../jetstream/src/main/assets/movies.json | 24 +++---- .../jetstream/data/entities/MovieDetails.kt | 2 +- .../data/repositories/MovieRepositoryImpl.kt | 2 +- .../jetstream/data/util/StringConstants.kt | 6 ++ .../screens/videoPlayer/VideoPlayerScreen.kt | 70 ++++++++++++++----- .../videoPlayer/VideoPlayerScreenViewModel.kt | 4 +- .../videoPlayer/components/RepeatIcon.kt | 63 +++++++++++++++++ .../components/VideoPlayerControls.kt | 45 ++++++++++-- .../components/VideoPlayerState.kt | 40 ++++++++++- 9 files changed, 215 insertions(+), 41 deletions(-) create mode 100644 JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/RepeatIcon.kt diff --git a/JetStreamCompose/jetstream/src/main/assets/movies.json b/JetStreamCompose/jetstream/src/main/assets/movies.json index 8f2d6d4ee..f824f4877 100644 --- a/JetStreamCompose/jetstream/src/main/assets/movies.json +++ b/JetStreamCompose/jetstream/src/main/assets/movies.json @@ -24,7 +24,7 @@ }, { "id": "6f251d94cc5c2", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 3, "rankUpDown": "+23", @@ -70,7 +70,7 @@ }, { "id": "51df9ee9a5c29", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 5, "rankUpDown": "+27", @@ -116,7 +116,7 @@ }, { "id": "040fab3d5e08e", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 7, "rankUpDown": "+57", @@ -162,7 +162,7 @@ }, { "id": "c4278acc58c31", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 9, "rankUpDown": "+29", @@ -208,7 +208,7 @@ }, { "id": "feee7e2119c28", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 11, "rankUpDown": "+36", @@ -254,7 +254,7 @@ }, { "id": "66f35ba9d671d", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 13, "rankUpDown": "+16", @@ -300,7 +300,7 @@ }, { "id": "523d5cdae88f7", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 15, "rankUpDown": "+17", @@ -346,7 +346,7 @@ }, { "id": "c10133062b2aa", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 18, "rankUpDown": "+33", @@ -392,7 +392,7 @@ }, { "id": "af69b8b439cb9", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 20, "rankUpDown": "+41", @@ -438,7 +438,7 @@ }, { "id": "5d428a566a71c", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 22, "rankUpDown": "+28", @@ -461,7 +461,7 @@ }, { "id": "84e74ee74bfc", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 23, "rankUpDown": "+48", @@ -507,7 +507,7 @@ }, { "id": "aa723f7cb6d5d", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 25, "rankUpDown": "+35", diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieDetails.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieDetails.kt index 84a9d80af..96028ad5c 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieDetails.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/entities/MovieDetails.kt @@ -36,5 +36,5 @@ data class MovieDetails( val budget: String, val revenue: String, val similarMovies: MovieList, - val reviewsAndRatings: List + val reviewsAndRatings: List, ) diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt index a1f13361a..0bc3a7eb7 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt @@ -116,7 +116,7 @@ class MovieRepositoryImpl @Inject constructor( reviewCount = DefaultCount, reviewRating = DefaultRating ), - ) + ), ) } diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/util/StringConstants.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/util/StringConstants.kt index 70df94c0b..8b844d05d 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/util/StringConstants.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/util/StringConstants.kt @@ -125,5 +125,11 @@ object StringConstants { const val VideoPlayerControlSettingsButton = "Playlist Button" const val VideoPlayerControlPlayPauseButton = "Playlist Button" const val VideoPlayerControlForward = "Fast forward 10 seconds" + const val VideoPlayerControlSkipNextButton = "Skip to the next movie" + const val VideoPlayerControlSkipPreviousButton = "Skip to the previous movie" + const val VideoPlayerControlRepeatAll = "Repeat all movies" + const val VideoPlayerControlRepeatOne = "Repeat movie" + const val VideoPlayerControlRepeatNone = "No repeat" + const val VideoPlayerControlRepeatButton = "Repeat Button" } } diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt index dbf408e9b..91e6ab506 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt @@ -41,6 +41,7 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.compose.PlayerSurface import androidx.media3.ui.compose.SURFACE_TYPE_TEXTURE_VIEW import androidx.media3.ui.compose.modifiers.resizeWithContentScale +import com.google.jetstream.data.entities.Movie import com.google.jetstream.data.entities.MovieDetails import com.google.jetstream.presentation.common.Error import com.google.jetstream.presentation.common.Loading @@ -104,24 +105,10 @@ fun VideoPlayerScreenContent(movieDetails: MovieDetails, onBackPressed: () -> Un ) LaunchedEffect(exoPlayer, movieDetails) { - exoPlayer.setMediaItem( - MediaItem.Builder() - .setUri(movieDetails.videoUri) - .setSubtitleConfigurations( - if (movieDetails.subtitleUri == null) { - emptyList() - } else { - listOf( - MediaItem.SubtitleConfiguration - .Builder(Uri.parse(movieDetails.subtitleUri)) - .setMimeType("application/vtt") - .setLanguage("en") - .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) - .build() - ) - } - ).build() - ) + exoPlayer.addMediaItem(movieDetails.intoMediaItem()) + movieDetails.similarMovies.forEach { + exoPlayer.addMediaItem(it.intoMediaItem()) + } exoPlayer.prepare() } @@ -172,10 +159,16 @@ fun VideoPlayerScreenContent(movieDetails: MovieDetails, onBackPressed: () -> Un contentCurrentPosition = contentCurrentPosition, contentDuration = exoPlayer.duration, isPlaying = videoPlayerState.isPlaying, + hasNextMovie = videoPlayerState.hasNextMovie, + hasPreviousMovie = videoPlayerState.hasPreviousMovie, + repeatMode = videoPlayerState.repeatMode, focusRequester = focusRequester, onShowControls = videoPlayerState::showControls, onSeek = { exoPlayer.seekTo(exoPlayer.duration.times(it).toLong()) }, - onPlayPauseToggle = videoPlayerState::togglePlayPause + onPlayPauseToggle = videoPlayerState::togglePlayPause, + onNextMovie = videoPlayerState::nextMovie, + onPreviousMovie = videoPlayerState::previousMovie, + onRepeat = videoPlayerState::toggleRepeat, ) } ) @@ -206,3 +199,42 @@ private fun Modifier.dPadEvents( videoPlayerState.showControls() } ) + +private fun MovieDetails.intoMediaItem(): MediaItem { + return MediaItem.Builder() + .setUri(videoUri) + .setSubtitleConfigurations( + if (subtitleUri == null) { + emptyList() + } else { + listOf( + MediaItem.SubtitleConfiguration + .Builder(Uri.parse(subtitleUri)) + .setMimeType("application/vtt") + .setLanguage("en") + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build() + ) + } + ).build() +} + +private fun Movie.intoMediaItem(): MediaItem { + return MediaItem.Builder() + .setUri(videoUri) + .setSubtitleConfigurations( + if (subtitleUri == null) { + emptyList() + } else { + listOf( + MediaItem.SubtitleConfiguration + .Builder(Uri.parse(subtitleUri)) + .setMimeType("application/vtt") + .setLanguage("en") + .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build() + ) + } + ) + .build() +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreenViewModel.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreenViewModel.kt index 5a79da150..3eb778be6 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreenViewModel.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreenViewModel.kt @@ -51,7 +51,7 @@ class VideoPlayerScreenViewModel @Inject constructor( @Immutable sealed class VideoPlayerScreenUiState { - object Loading : VideoPlayerScreenUiState() - object Error : VideoPlayerScreenUiState() + data object Loading : VideoPlayerScreenUiState() + data object Error : VideoPlayerScreenUiState() data class Done(val movieDetails: MovieDetails) : VideoPlayerScreenUiState() } diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/RepeatIcon.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/RepeatIcon.kt new file mode 100644 index 000000000..d67aae4d2 --- /dev/null +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/RepeatIcon.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2025 Google LLC + * + * 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 com.google.jetstream.presentation.screens.videoPlayer.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Repeat +import androidx.compose.material.icons.filled.RepeatOne +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.dp +import androidx.media3.common.Player +import androidx.tv.material3.LocalContentColor +import com.google.jetstream.data.util.StringConstants + +@Composable +fun RepeatIcon( + isPlaying: Boolean, + onShowControls: () -> Unit, + modifier: Modifier = Modifier, + repeatMode: Int = Player.REPEAT_MODE_OFF, + contentDescription: String? = StringConstants.Composable.VideoPlayerControlRepeatButton, + onClick: () -> Unit = {} +) { + val isRepeating = repeatMode != Player.REPEAT_MODE_OFF + val color = LocalContentColor.current + + VideoPlayerControlsIcon( + icon = when (repeatMode) { + Player.REPEAT_MODE_ONE -> Icons.Default.RepeatOne + else -> Icons.Default.Repeat + }, + isPlaying = isPlaying, + contentDescription = contentDescription, + onShowControls = onShowControls, + onClick = onClick, + modifier = modifier.drawBehind { + if (isRepeating) { + val radius = 2.dp.toPx() + drawCircle( + color = color, + radius = radius, + center = Offset((size.width - radius) / 2, size.height - radius * 3) + ) + } + } + ) +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControls.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControls.kt index 1755a17cc..a24d957da 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControls.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControls.kt @@ -16,12 +16,15 @@ package com.google.jetstream.presentation.screens.videoPlayer.components +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AutoAwesomeMotion import androidx.compose.material.icons.filled.ClosedCaption import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.SkipNext +import androidx.compose.material.icons.filled.SkipPrevious import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -37,10 +40,16 @@ fun VideoPlayerControls( contentCurrentPosition: Long, contentDuration: Long, isPlaying: Boolean, + hasNextMovie: Boolean, + hasPreviousMovie: Boolean, + repeatMode: Int, focusRequester: FocusRequester, onPlayPauseToggle: () -> Unit = {}, onSeek: (Float) -> Unit = {}, - onShowControls: () -> Unit = {} + onShowControls: () -> Unit = {}, + onPreviousMovie: () -> Unit = {}, + onNextMovie: () -> Unit = {}, + onRepeat: () -> Unit = {} ) { VideoPlayerMainFrame( mediaTitle = { @@ -54,8 +63,38 @@ fun VideoPlayerControls( mediaActions = { Row( modifier = Modifier.padding(bottom = 16.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { + VideoPlayerControlsIcon( + icon = Icons.Default.SkipPrevious, + isPlaying = isPlaying, + contentDescription = + StringConstants.Composable.VideoPlayerControlSkipPreviousButton, + onShowControls = onShowControls, + onClick = onPreviousMovie + ) + VideoPlayerControlsIcon( + icon = Icons.Default.SkipNext, + isPlaying = isPlaying, + contentDescription = + StringConstants.Composable.VideoPlayerControlSkipNextButton, + onShowControls = onShowControls, + onClick = onNextMovie + ) + RepeatIcon( + isPlaying = isPlaying, + repeatMode = repeatMode, + onShowControls = onShowControls, + onClick = onRepeat, + ) + VideoPlayerControlsIcon( + icon = Icons.Default.SkipPrevious, + isPlaying = isPlaying, + contentDescription = + StringConstants.Composable.VideoPlayerControlClosedCaptionsButton, + onShowControls = onShowControls + ) VideoPlayerControlsIcon( icon = Icons.Default.AutoAwesomeMotion, isPlaying = isPlaying, @@ -64,7 +103,6 @@ fun VideoPlayerControls( onShowControls = onShowControls ) VideoPlayerControlsIcon( - modifier = Modifier.padding(start = 12.dp), icon = Icons.Default.ClosedCaption, isPlaying = isPlaying, contentDescription = @@ -72,7 +110,6 @@ fun VideoPlayerControls( onShowControls = onShowControls ) VideoPlayerControlsIcon( - modifier = Modifier.padding(start = 12.dp), icon = Icons.Default.Settings, isPlaying = isPlaying, contentDescription = diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt index 66787f7b5..b9123c0e0 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt @@ -25,8 +25,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.compose.state.NextButtonState import androidx.media3.ui.compose.state.PlayPauseButtonState +import androidx.media3.ui.compose.state.PreviousButtonState +import androidx.media3.ui.compose.state.RepeatButtonState +import androidx.media3.ui.compose.state.rememberNextButtonState import androidx.media3.ui.compose.state.rememberPlayPauseButtonState +import androidx.media3.ui.compose.state.rememberPreviousButtonState +import androidx.media3.ui.compose.state.rememberRepeatButtonState import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.CONFLATED @@ -37,7 +43,10 @@ import kotlinx.coroutines.flow.debounce class VideoPlayerState( @IntRange(from = 0) private val hideSeconds: Int, - val playPauseButtonState: PlayPauseButtonState, + private val playPauseButtonState: PlayPauseButtonState, + private val previousButtonState: PreviousButtonState, + private val nextButtonState: NextButtonState, + private val repeatButtonState: RepeatButtonState, ) { var isControlsVisible by mutableStateOf(true) private set @@ -45,10 +54,31 @@ class VideoPlayerState( val isPlaying get() = !playPauseButtonState.showPlay + val hasNextMovie + get() = nextButtonState.isEnabled + + val hasPreviousMovie + get() = previousButtonState.isEnabled + + val repeatMode + get() = repeatButtonState.repeatModeState + fun togglePlayPause() { playPauseButtonState.onClick() } + fun nextMovie() { + nextButtonState.onClick() + } + + fun previousMovie() { + previousButtonState.onClick() + } + + fun toggleRepeat() { + repeatButtonState.onClick() + } + fun showControls() { if (isPlaying) { updateControlVisibility() @@ -85,10 +115,16 @@ fun rememberVideoPlayerState( @IntRange(from = 0) hideSeconds: Int = 2 ): VideoPlayerState { val playPauseButtonState = rememberPlayPauseButtonState(exoPlayer) + val nextButtonState = rememberNextButtonState(exoPlayer) + val previousButtonState = rememberPreviousButtonState(exoPlayer) + val repeatButtonState = rememberRepeatButtonState(exoPlayer) return remember(playPauseButtonState) { VideoPlayerState( hideSeconds = hideSeconds, - playPauseButtonState = playPauseButtonState + playPauseButtonState = playPauseButtonState, + nextButtonState = nextButtonState, + previousButtonState = previousButtonState, + repeatButtonState = repeatButtonState ) } .also { LaunchedEffect(it) { it.observe() } } From 1450bf15f03c28263884905c9672bcb66d0d0951 Mon Sep 17 00:00:00 2001 From: Chiko Shimizu Date: Mon, 10 Mar 2025 15:55:48 +0900 Subject: [PATCH 2/3] Move state objects introduced from media3-ui-compose to buttons to control playback --- .../jetstream/src/main/assets/movies.json | 30 +++++----- .../data/repositories/MovieRepositoryImpl.kt | 6 +- .../screens/videoPlayer/VideoPlayerScreen.kt | 19 ++----- .../videoPlayer/components/NextButton.kt | 47 ++++++++++++++++ .../videoPlayer/components/PreviousButton.kt | 47 ++++++++++++++++ .../videoPlayer/components/RememberPlayer.kt | 2 +- .../{RepeatIcon.kt => RepeatButton.kt} | 19 ++++--- .../components/VideoPlayerControls.kt | 54 +++++------------- .../components/VideoPlayerSeeker.kt | 25 ++++++--- .../components/VideoPlayerState.kt | 55 +------------------ 10 files changed, 164 insertions(+), 140 deletions(-) create mode 100644 JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/NextButton.kt create mode 100644 JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/PreviousButton.kt rename JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/{RepeatIcon.kt => RepeatButton.kt} (82%) diff --git a/JetStreamCompose/jetstream/src/main/assets/movies.json b/JetStreamCompose/jetstream/src/main/assets/movies.json index f824f4877..c5e49740b 100644 --- a/JetStreamCompose/jetstream/src/main/assets/movies.json +++ b/JetStreamCompose/jetstream/src/main/assets/movies.json @@ -553,7 +553,7 @@ }, { "id": "f1b81e90f812", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 27, "rankUpDown": "+22", @@ -599,7 +599,7 @@ }, { "id": "504431d1aca8", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 29, "rankUpDown": "+39", @@ -645,7 +645,7 @@ }, { "id": "c08e5ae6ecc9f", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 31, "rankUpDown": "+42", @@ -691,7 +691,7 @@ }, { "id": "40353aa9623af", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 33, "rankUpDown": "+51", @@ -714,7 +714,7 @@ }, { "id": "64e067f836ca2", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 34, "rankUpDown": "+31", @@ -760,7 +760,7 @@ }, { "id": "61946ea9ede15", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 36, "rankUpDown": "+21", @@ -829,7 +829,7 @@ }, { "id": "08ef353fd4def", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 39, "rankUpDown": "+55", @@ -875,7 +875,7 @@ }, { "id": "43e5a062e2bfc", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 41, "rankUpDown": "+48", @@ -921,7 +921,7 @@ }, { "id": "73ce574852058", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 43, "rankUpDown": "+19", @@ -967,7 +967,7 @@ }, { "id": "defa276de73e5", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 45, "rankUpDown": "+54", @@ -990,7 +990,7 @@ }, { "id": "d84978bdf9622", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 46, "rankUpDown": "+33", @@ -1036,7 +1036,7 @@ }, { "id": "834ce43565946", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 48, "rankUpDown": "+57", @@ -1082,7 +1082,7 @@ }, { "id": "07c92f3a31737", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 50, "rankUpDown": "+30", @@ -1128,7 +1128,7 @@ }, { "id": "b995170bc926", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 52, "rankUpDown": "+46", @@ -1174,7 +1174,7 @@ }, { "id": "5be58f705ee35", - "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + "videoUri": "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", "subtitleUri": "https://thepaciellogroup.github.io/AT-browser-tests/video/subtitles-en.vtt", "rank": 54, "rankUpDown": "+54", diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt index 0bc3a7eb7..c6934b6ec 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt @@ -26,10 +26,10 @@ import com.google.jetstream.data.util.StringConstants.Movie.Reviewer.DefaultCoun import com.google.jetstream.data.util.StringConstants.Movie.Reviewer.DefaultRating import com.google.jetstream.data.util.StringConstants.Movie.Reviewer.FreshTomatoes import com.google.jetstream.data.util.StringConstants.Movie.Reviewer.ReviewerName -import javax.inject.Inject -import javax.inject.Singleton import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import javax.inject.Inject +import javax.inject.Singleton @Singleton class MovieRepositoryImpl @Inject constructor( @@ -80,7 +80,7 @@ class MovieRepositoryImpl @Inject constructor( override suspend fun getMovieDetails(movieId: String): MovieDetails { val movieList = movieDataSource.getMovieList() val movie = movieList.find { it.id == movieId } ?: movieList.first() - val similarMovieList = movieList.shuffled().subList(0, 2) + val similarMovieList = movieList.subList(1, 4) val castList = movieCastDataSource.getMovieCastList() return MovieDetails( diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt index 91e6ab506..2a6234774 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt @@ -97,10 +97,10 @@ fun VideoPlayerScreen( @androidx.annotation.OptIn(UnstableApi::class) @Composable fun VideoPlayerScreenContent(movieDetails: MovieDetails, onBackPressed: () -> Unit) { - val exoPlayer = rememberPlayer(LocalContext.current) + val context = LocalContext.current + val exoPlayer = rememberPlayer(context) val videoPlayerState = rememberVideoPlayerState( - exoPlayer = exoPlayer, hideSeconds = 4, ) @@ -148,27 +148,18 @@ fun VideoPlayerScreenContent(movieDetails: MovieDetails, onBackPressed: () -> Un VideoPlayerOverlay( modifier = Modifier.align(Alignment.BottomCenter), focusRequester = focusRequester, - isPlaying = videoPlayerState.isPlaying, + isPlaying = exoPlayer.isPlaying, isControlsVisible = videoPlayerState.isControlsVisible, centerButton = { VideoPlayerPulse(pulseState) }, subtitles = { /* TODO Implement subtitles */ }, showControls = videoPlayerState::showControls, controls = { VideoPlayerControls( + player = exoPlayer, movieDetails = movieDetails, contentCurrentPosition = contentCurrentPosition, - contentDuration = exoPlayer.duration, - isPlaying = videoPlayerState.isPlaying, - hasNextMovie = videoPlayerState.hasNextMovie, - hasPreviousMovie = videoPlayerState.hasPreviousMovie, - repeatMode = videoPlayerState.repeatMode, focusRequester = focusRequester, - onShowControls = videoPlayerState::showControls, - onSeek = { exoPlayer.seekTo(exoPlayer.duration.times(it).toLong()) }, - onPlayPauseToggle = videoPlayerState::togglePlayPause, - onNextMovie = videoPlayerState::nextMovie, - onPreviousMovie = videoPlayerState::previousMovie, - onRepeat = videoPlayerState::toggleRepeat, + onShowControls = { videoPlayerState.showControls(exoPlayer.isPlaying) }, ) } ) diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/NextButton.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/NextButton.kt new file mode 100644 index 000000000..5937a122c --- /dev/null +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/NextButton.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2025 Google LLC + * + * 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 com.google.jetstream.presentation.screens.videoPlayer.components + +import androidx.annotation.OptIn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.SkipNext +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.ui.compose.state.NextButtonState +import androidx.media3.ui.compose.state.rememberNextButtonState +import com.google.jetstream.data.util.StringConstants + +@OptIn(UnstableApi::class) +@Composable +fun NextButton( + player: Player, + modifier: Modifier = Modifier, + state: NextButtonState = rememberNextButtonState(player), + onShowControls: () -> Unit = {}, +) { + VideoPlayerControlsIcon( + icon = Icons.Default.SkipNext, + isPlaying = player.isPlaying, + contentDescription = + StringConstants.Composable.VideoPlayerControlSkipNextButton, + onShowControls = onShowControls, + onClick = state::onClick, + modifier = modifier + ) +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/PreviousButton.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/PreviousButton.kt new file mode 100644 index 000000000..546e40289 --- /dev/null +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/PreviousButton.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2025 Google LLC + * + * 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 com.google.jetstream.presentation.screens.videoPlayer.components + +import androidx.annotation.OptIn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.SkipPrevious +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.ui.compose.state.PreviousButtonState +import androidx.media3.ui.compose.state.rememberPreviousButtonState +import com.google.jetstream.data.util.StringConstants + +@OptIn(UnstableApi::class) +@Composable +fun PreviousButton( + player: Player, + modifier: Modifier = Modifier, + state: PreviousButtonState = rememberPreviousButtonState(player), + onShowControls: () -> Unit = {}, +) { + VideoPlayerControlsIcon( + icon = Icons.Default.SkipPrevious, + isPlaying = player.isPlaying, + contentDescription = + StringConstants.Composable.VideoPlayerControlSkipPreviousButton, + onShowControls = onShowControls, + onClick = state::onClick, + modifier = modifier, + ) +} diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/RememberPlayer.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/RememberPlayer.kt index 41baf289b..9c5dd0e4b 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/RememberPlayer.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/RememberPlayer.kt @@ -39,6 +39,6 @@ fun rememberPlayer(context: Context) = remember { .build() .apply { playWhenReady = true - repeatMode = Player.REPEAT_MODE_ONE + repeatMode = Player.REPEAT_MODE_OFF } } diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/RepeatIcon.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/RepeatButton.kt similarity index 82% rename from JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/RepeatIcon.kt rename to JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/RepeatButton.kt index d67aae4d2..334573c4d 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/RepeatIcon.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/RepeatButton.kt @@ -16,6 +16,7 @@ package com.google.jetstream.presentation.screens.videoPlayer.components +import androidx.annotation.OptIn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Repeat import androidx.compose.material.icons.filled.RepeatOne @@ -25,18 +26,22 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.dp import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.ui.compose.state.RepeatButtonState +import androidx.media3.ui.compose.state.rememberRepeatButtonState import androidx.tv.material3.LocalContentColor import com.google.jetstream.data.util.StringConstants +@OptIn(UnstableApi::class) @Composable -fun RepeatIcon( - isPlaying: Boolean, - onShowControls: () -> Unit, +fun RepeatButton( + player: Player, modifier: Modifier = Modifier, - repeatMode: Int = Player.REPEAT_MODE_OFF, + state: RepeatButtonState = rememberRepeatButtonState(player), contentDescription: String? = StringConstants.Composable.VideoPlayerControlRepeatButton, - onClick: () -> Unit = {} + onShowControls: () -> Unit, ) { + val repeatMode = state.repeatModeState val isRepeating = repeatMode != Player.REPEAT_MODE_OFF val color = LocalContentColor.current @@ -45,10 +50,10 @@ fun RepeatIcon( Player.REPEAT_MODE_ONE -> Icons.Default.RepeatOne else -> Icons.Default.Repeat }, - isPlaying = isPlaying, + isPlaying = player.isPlaying, contentDescription = contentDescription, onShowControls = onShowControls, - onClick = onClick, + onClick = state::onClick, modifier = modifier.drawBehind { if (isRepeating) { val radius = 2.dp.toPx() diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControls.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControls.kt index a24d957da..224cee759 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControls.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControls.kt @@ -23,34 +23,26 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AutoAwesomeMotion import androidx.compose.material.icons.filled.ClosedCaption import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.icons.filled.SkipNext -import androidx.compose.material.icons.filled.SkipPrevious import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.unit.dp +import androidx.media3.common.Player import com.google.jetstream.data.entities.MovieDetails import com.google.jetstream.data.util.StringConstants import kotlin.time.Duration.Companion.milliseconds @Composable fun VideoPlayerControls( + player: Player, movieDetails: MovieDetails, contentCurrentPosition: Long, - contentDuration: Long, - isPlaying: Boolean, - hasNextMovie: Boolean, - hasPreviousMovie: Boolean, - repeatMode: Int, focusRequester: FocusRequester, - onPlayPauseToggle: () -> Unit = {}, - onSeek: (Float) -> Unit = {}, onShowControls: () -> Unit = {}, - onPreviousMovie: () -> Unit = {}, - onNextMovie: () -> Unit = {}, - onRepeat: () -> Unit = {} ) { + val isPlaying = player.isPlaying + VideoPlayerMainFrame( mediaTitle = { VideoPlayerMediaTitle( @@ -66,34 +58,17 @@ fun VideoPlayerControls( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - VideoPlayerControlsIcon( - icon = Icons.Default.SkipPrevious, - isPlaying = isPlaying, - contentDescription = - StringConstants.Composable.VideoPlayerControlSkipPreviousButton, - onShowControls = onShowControls, - onClick = onPreviousMovie + PreviousButton( + player = player, + onShowControls = onShowControls ) - VideoPlayerControlsIcon( - icon = Icons.Default.SkipNext, - isPlaying = isPlaying, - contentDescription = - StringConstants.Composable.VideoPlayerControlSkipNextButton, - onShowControls = onShowControls, - onClick = onNextMovie + NextButton( + player = player, + onShowControls = onShowControls ) - RepeatIcon( - isPlaying = isPlaying, - repeatMode = repeatMode, + RepeatButton( + player = player, onShowControls = onShowControls, - onClick = onRepeat, - ) - VideoPlayerControlsIcon( - icon = Icons.Default.SkipPrevious, - isPlaying = isPlaying, - contentDescription = - StringConstants.Composable.VideoPlayerControlClosedCaptionsButton, - onShowControls = onShowControls ) VideoPlayerControlsIcon( icon = Icons.Default.AutoAwesomeMotion, @@ -120,13 +95,10 @@ fun VideoPlayerControls( }, seeker = { VideoPlayerSeeker( + player = player, focusRequester = focusRequester, - isPlaying = isPlaying, - onPlayPauseToggle = onPlayPauseToggle, - onSeek = onSeek, onShowControls = onShowControls, contentProgress = contentCurrentPosition.milliseconds, - contentDuration = contentDuration.milliseconds, ) }, more = null diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerSeeker.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerSeeker.kt index b6140ffb3..034d505fe 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerSeeker.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerSeeker.kt @@ -16,6 +16,7 @@ package com.google.jetstream.presentation.screens.videoPlayer.components +import androidx.annotation.OptIn import androidx.compose.foundation.layout.Row import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Pause @@ -25,19 +26,28 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.ui.compose.state.PlayPauseButtonState +import androidx.media3.ui.compose.state.rememberPlayPauseButtonState import com.google.jetstream.data.util.StringConstants import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +@OptIn(UnstableApi::class) @Composable fun VideoPlayerSeeker( + player: Player, focusRequester: FocusRequester, - isPlaying: Boolean, contentProgress: Duration, - contentDuration: Duration, - onPlayPauseToggle: () -> Unit, - onSeek: (Float) -> Unit, + modifier: Modifier = Modifier, + state: PlayPauseButtonState = rememberPlayPauseButtonState(player), + onSeek: (Float) -> Unit = { + player.seekTo(player.duration.times(it).toLong()) + }, onShowControls: () -> Unit = {}, ) { + val contentDuration = player.contentDuration.milliseconds val contentProgressString = contentProgress.toComponents { h, m, s, _ -> if (h > 0) { @@ -56,13 +66,14 @@ fun VideoPlayerSeeker( } Row( + modifier = modifier, verticalAlignment = Alignment.CenterVertically ) { VideoPlayerControlsIcon( modifier = Modifier.focusRequester(focusRequester), - icon = if (!isPlaying) Icons.Default.PlayArrow else Icons.Default.Pause, - onClick = onPlayPauseToggle, - isPlaying = isPlaying, + icon = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause, + onClick = state::onClick, + isPlaying = player.isPlaying, contentDescription = StringConstants .Composable .VideoPlayerControlPlayPauseButton diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt index b9123c0e0..5d82dc9c4 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt @@ -24,15 +24,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.ui.compose.state.NextButtonState -import androidx.media3.ui.compose.state.PlayPauseButtonState -import androidx.media3.ui.compose.state.PreviousButtonState -import androidx.media3.ui.compose.state.RepeatButtonState -import androidx.media3.ui.compose.state.rememberNextButtonState -import androidx.media3.ui.compose.state.rememberPlayPauseButtonState -import androidx.media3.ui.compose.state.rememberPreviousButtonState -import androidx.media3.ui.compose.state.rememberRepeatButtonState import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.CONFLATED @@ -43,43 +34,11 @@ import kotlinx.coroutines.flow.debounce class VideoPlayerState( @IntRange(from = 0) private val hideSeconds: Int, - private val playPauseButtonState: PlayPauseButtonState, - private val previousButtonState: PreviousButtonState, - private val nextButtonState: NextButtonState, - private val repeatButtonState: RepeatButtonState, ) { var isControlsVisible by mutableStateOf(true) private set - val isPlaying - get() = !playPauseButtonState.showPlay - - val hasNextMovie - get() = nextButtonState.isEnabled - - val hasPreviousMovie - get() = previousButtonState.isEnabled - - val repeatMode - get() = repeatButtonState.repeatModeState - - fun togglePlayPause() { - playPauseButtonState.onClick() - } - - fun nextMovie() { - nextButtonState.onClick() - } - - fun previousMovie() { - previousButtonState.onClick() - } - - fun toggleRepeat() { - repeatButtonState.onClick() - } - - fun showControls() { + fun showControls(isPlaying: Boolean = true) { if (isPlaying) { updateControlVisibility() } else { @@ -100,6 +59,7 @@ class VideoPlayerState( .debounce { it.toLong() * 1000 } .collect { isControlsVisible = false } } + } /** @@ -111,20 +71,11 @@ class VideoPlayerState( @androidx.annotation.OptIn(UnstableApi::class) @Composable fun rememberVideoPlayerState( - exoPlayer: ExoPlayer, @IntRange(from = 0) hideSeconds: Int = 2 ): VideoPlayerState { - val playPauseButtonState = rememberPlayPauseButtonState(exoPlayer) - val nextButtonState = rememberNextButtonState(exoPlayer) - val previousButtonState = rememberPreviousButtonState(exoPlayer) - val repeatButtonState = rememberRepeatButtonState(exoPlayer) - return remember(playPauseButtonState) { + return remember { VideoPlayerState( hideSeconds = hideSeconds, - playPauseButtonState = playPauseButtonState, - nextButtonState = nextButtonState, - previousButtonState = previousButtonState, - repeatButtonState = repeatButtonState ) } .also { LaunchedEffect(it) { it.observe() } } From 352dcadf22dc37c0882e871e5da7455e65702319 Mon Sep 17 00:00:00 2001 From: Chiko Shimizu Date: Mon, 10 Mar 2025 16:55:45 +0900 Subject: [PATCH 3/3] Remove playback position from video player composables for performance improvement --- .../data/repositories/MovieRepositoryImpl.kt | 4 ++-- .../screens/videoPlayer/VideoPlayerScreen.kt | 14 ----------- .../components/VideoPlayerControls.kt | 3 --- .../components/VideoPlayerSeeker.kt | 24 +++++++++++++++---- .../components/VideoPlayerState.kt | 1 - 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt index c6934b6ec..d817f33c4 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/data/repositories/MovieRepositoryImpl.kt @@ -26,10 +26,10 @@ import com.google.jetstream.data.util.StringConstants.Movie.Reviewer.DefaultCoun import com.google.jetstream.data.util.StringConstants.Movie.Reviewer.DefaultRating import com.google.jetstream.data.util.StringConstants.Movie.Reviewer.FreshTomatoes import com.google.jetstream.data.util.StringConstants.Movie.Reviewer.ReviewerName -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow @Singleton class MovieRepositoryImpl @Inject constructor( diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt index 2a6234774..b9bcc6955 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt @@ -24,9 +24,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -56,7 +54,6 @@ import com.google.jetstream.presentation.screens.videoPlayer.components.remember import com.google.jetstream.presentation.screens.videoPlayer.components.rememberVideoPlayerPulseState import com.google.jetstream.presentation.screens.videoPlayer.components.rememberVideoPlayerState import com.google.jetstream.presentation.utils.handleDPadKeyEvents -import kotlinx.coroutines.delay object VideoPlayerScreen { const val MovieIdBundleKey = "movieId" @@ -112,16 +109,6 @@ fun VideoPlayerScreenContent(movieDetails: MovieDetails, onBackPressed: () -> Un exoPlayer.prepare() } - var contentCurrentPosition by remember { mutableLongStateOf(0L) } - - // TODO: Update in a more thoughtful manner - LaunchedEffect(Unit) { - while (true) { - delay(300) - contentCurrentPosition = exoPlayer.currentPosition - } - } - BackHandler(onBack = onBackPressed) val pulseState = rememberVideoPlayerPulseState() @@ -157,7 +144,6 @@ fun VideoPlayerScreenContent(movieDetails: MovieDetails, onBackPressed: () -> Un VideoPlayerControls( player = exoPlayer, movieDetails = movieDetails, - contentCurrentPosition = contentCurrentPosition, focusRequester = focusRequester, onShowControls = { videoPlayerState.showControls(exoPlayer.isPlaying) }, ) diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControls.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControls.kt index 224cee759..bf3a70370 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControls.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerControls.kt @@ -31,13 +31,11 @@ import androidx.compose.ui.unit.dp import androidx.media3.common.Player import com.google.jetstream.data.entities.MovieDetails import com.google.jetstream.data.util.StringConstants -import kotlin.time.Duration.Companion.milliseconds @Composable fun VideoPlayerControls( player: Player, movieDetails: MovieDetails, - contentCurrentPosition: Long, focusRequester: FocusRequester, onShowControls: () -> Unit = {}, ) { @@ -98,7 +96,6 @@ fun VideoPlayerControls( player = player, focusRequester = focusRequester, onShowControls = onShowControls, - contentProgress = contentCurrentPosition.milliseconds, ) }, more = null diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerSeeker.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerSeeker.kt index 034d505fe..6ab168d80 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerSeeker.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerSeeker.kt @@ -22,6 +22,11 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -31,15 +36,14 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.ui.compose.state.PlayPauseButtonState import androidx.media3.ui.compose.state.rememberPlayPauseButtonState import com.google.jetstream.data.util.StringConstants -import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.delay @OptIn(UnstableApi::class) @Composable fun VideoPlayerSeeker( player: Player, focusRequester: FocusRequester, - contentProgress: Duration, modifier: Modifier = Modifier, state: PlayPauseButtonState = rememberPlayPauseButtonState(player), onSeek: (Float) -> Unit = { @@ -48,8 +52,20 @@ fun VideoPlayerSeeker( onShowControls: () -> Unit = {}, ) { val contentDuration = player.contentDuration.milliseconds + + var currentPositionMs by remember(player) { mutableLongStateOf(0L) } + val currentPosition = currentPositionMs.milliseconds + + // TODO: Update in a more thoughtful manner + LaunchedEffect(Unit) { + while (true) { + delay(300) + currentPositionMs = player.currentPosition + } + } + val contentProgressString = - contentProgress.toComponents { h, m, s, _ -> + currentPosition.toComponents { h, m, s, _ -> if (h > 0) { "$h:${m.padStartWith0()}:${s.padStartWith0()}" } else { @@ -80,7 +96,7 @@ fun VideoPlayerSeeker( ) VideoPlayerControllerText(text = contentProgressString) VideoPlayerControllerIndicator( - progress = (contentProgress / contentDuration).toFloat(), + progress = (currentPosition / contentDuration).toFloat(), onSeek = onSeek, onShowControls = onShowControls ) diff --git a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt index 5d82dc9c4..d1d773f8c 100644 --- a/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt +++ b/JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/components/VideoPlayerState.kt @@ -59,7 +59,6 @@ class VideoPlayerState( .debounce { it.toLong() * 1000 } .collect { isControlsVisible = false } } - } /**