Skip to content

Commit 4f57da5

Browse files
authored
Merge pull request #199 from chikoski/main
media3-ui-compose integration
2 parents 668eddf + 632aa8c commit 4f57da5

File tree

13 files changed

+258
-183
lines changed

13 files changed

+258
-183
lines changed

JetStreamCompose/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
.cxx
88
/.idea/
99
local.properties
10+
.kotlin

JetStreamCompose/benchmark/build.gradle.kts

+3
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ dependencies {
7070
// Use 1.2.0-alpha03 or above versions for benchmarking TV apps
7171
implementation(libs.androidx.benchmark.macro.junit4)
7272
implementation(libs.androidx.rules)
73+
74+
implementation(platform(libs.androidx.compose.bom))
75+
implementation(libs.androidx.compose.runtime)
7376
}
7477

7578
androidComponents {

JetStreamCompose/gradle/libs.versions.toml

+12-12
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
[versions]
2-
activity-compose = "1.10.0"
3-
android-gradle-plugin = "8.7.3"
4-
android-test-plugin = "8.7.3"
2+
activity-compose = "1.10.1"
3+
android-gradle-plugin = "8.8.2"
4+
android-test-plugin = "8.8.2"
55
androidx-baselineprofile = "1.3.3"
66
benchmark-macro-junit4 = "1.3.3"
77
coil-compose = "2.7.0"
8-
compose-bom = "2025.01.00"
8+
compose-bom = "2025.02.00"
99
tv-material = "1.0.0"
1010
core-ktx = "1.15.0"
1111
core-splashscreen = "1.0.1"
1212
hilt-navigation-compose = "1.2.0"
13-
hilt-android = "2.52"
13+
hilt-android = "2.54"
1414
junit = "1.2.1"
1515
kotlin-android = "2.1.0"
16-
kotlinx-serialization = "1.6.3"
17-
ksp = "2.0.20-1.0.24"
16+
kotlinx-serialization = "1.8.0"
17+
ksp = "2.1.0-1.0.29"
1818
lifecycle-runtime-ktx = "2.8.7"
19-
media3-ui = "1.5.1"
20-
media3-exoplayer = "1.5.1"
21-
navigation-compose = "2.8.5"
19+
media3 = "1.6.0-beta01"
20+
navigation-compose = "2.8.8"
2221
profileinstaller = "1.4.1"
2322
uiautomator = "2.3.0"
2423
rules = "1.6.1"
@@ -29,6 +28,7 @@ androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro
2928
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
3029
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
3130
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
31+
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
3232
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" }
3333
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "core-splashscreen" }
3434
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
@@ -37,8 +37,8 @@ androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-ru
3737
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
3838
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle-runtime-ktx" }
3939
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
40-
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3-ui" }
41-
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3-exoplayer" }
40+
androidx-media3-ui = { module = "androidx.media3:media3-ui-compose", version.ref = "media3" }
41+
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
4242
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
4343
androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "profileinstaller" }
4444
androidx-tv-material = { module = "androidx.tv:tv-material", version.ref = "tv-material" }
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
44
networkTimeout=10000
55
zipStoreBase=GRADLE_USER_HOME
66
zipStorePath=wrapper/dists

JetStreamCompose/jetstream/build.gradle.kts

+4
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ android {
6565
excludes += "/META-INF/{AL2.0,LGPL2.1}"
6666
}
6767
}
68+
69+
kotlinOptions {
70+
jvmTarget = "17"
71+
}
6872
}
6973

7074
dependencies {

JetStreamCompose/jetstream/src/main/java/com/google/jetstream/presentation/screens/videoPlayer/VideoPlayerScreen.kt

+35-134
Original file line numberDiff line numberDiff line change
@@ -16,60 +16,45 @@
1616

1717
package com.google.jetstream.presentation.screens.videoPlayer
1818

19-
import android.content.Context
2019
import android.net.Uri
2120
import androidx.activity.compose.BackHandler
2221
import androidx.compose.foundation.focusable
2322
import androidx.compose.foundation.layout.Box
24-
import androidx.compose.foundation.layout.Row
2523
import androidx.compose.foundation.layout.fillMaxSize
26-
import androidx.compose.foundation.layout.padding
27-
import androidx.compose.material.icons.Icons
28-
import androidx.compose.material.icons.filled.AutoAwesomeMotion
29-
import androidx.compose.material.icons.filled.ClosedCaption
30-
import androidx.compose.material.icons.filled.Settings
3124
import androidx.compose.runtime.Composable
3225
import androidx.compose.runtime.LaunchedEffect
3326
import androidx.compose.runtime.getValue
3427
import androidx.compose.runtime.mutableLongStateOf
35-
import androidx.compose.runtime.mutableStateOf
3628
import androidx.compose.runtime.remember
3729
import androidx.compose.runtime.setValue
3830
import androidx.compose.ui.Alignment
3931
import androidx.compose.ui.Modifier
4032
import androidx.compose.ui.focus.FocusRequester
33+
import androidx.compose.ui.layout.ContentScale
4134
import androidx.compose.ui.platform.LocalContext
42-
import androidx.compose.ui.unit.dp
43-
import androidx.compose.ui.viewinterop.AndroidView
4435
import androidx.hilt.navigation.compose.hiltViewModel
4536
import androidx.lifecycle.compose.collectAsStateWithLifecycle
4637
import androidx.media3.common.C
4738
import androidx.media3.common.MediaItem
48-
import androidx.media3.common.Player
4939
import androidx.media3.common.util.UnstableApi
50-
import androidx.media3.datasource.DefaultDataSource
5140
import androidx.media3.exoplayer.ExoPlayer
52-
import androidx.media3.exoplayer.source.ProgressiveMediaSource
53-
import androidx.media3.ui.PlayerView
41+
import androidx.media3.ui.compose.PlayerSurface
42+
import androidx.media3.ui.compose.SURFACE_TYPE_TEXTURE_VIEW
43+
import androidx.media3.ui.compose.modifiers.resizeWithContentScale
5444
import com.google.jetstream.data.entities.MovieDetails
55-
import com.google.jetstream.data.util.StringConstants
5645
import com.google.jetstream.presentation.common.Error
5746
import com.google.jetstream.presentation.common.Loading
58-
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerControlsIcon
59-
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerMainFrame
60-
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerMediaTitle
61-
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerMediaTitleType
47+
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerControls
6248
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerOverlay
6349
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerPulse
6450
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerPulse.Type.BACK
6551
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerPulse.Type.FORWARD
6652
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerPulseState
67-
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerSeeker
6853
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerState
54+
import com.google.jetstream.presentation.screens.videoPlayer.components.rememberPlayer
6955
import com.google.jetstream.presentation.screens.videoPlayer.components.rememberVideoPlayerPulseState
7056
import com.google.jetstream.presentation.screens.videoPlayer.components.rememberVideoPlayerState
7157
import com.google.jetstream.presentation.utils.handleDPadKeyEvents
72-
import kotlin.time.Duration.Companion.milliseconds
7358
import kotlinx.coroutines.delay
7459

7560
object VideoPlayerScreen {
@@ -94,9 +79,11 @@ fun VideoPlayerScreen(
9479
is VideoPlayerScreenUiState.Loading -> {
9580
Loading(modifier = Modifier.fillMaxSize())
9681
}
82+
9783
is VideoPlayerScreenUiState.Error -> {
9884
Error(modifier = Modifier.fillMaxSize())
9985
}
86+
10087
is VideoPlayerScreenUiState.Done -> {
10188
VideoPlayerScreenContent(
10289
movieDetails = s.movieDetails,
@@ -109,11 +96,13 @@ fun VideoPlayerScreen(
10996
@androidx.annotation.OptIn(UnstableApi::class)
11097
@Composable
11198
fun VideoPlayerScreenContent(movieDetails: MovieDetails, onBackPressed: () -> Unit) {
112-
val context = LocalContext.current
113-
val videoPlayerState = rememberVideoPlayerState(hideSeconds = 4)
99+
val exoPlayer = rememberPlayer(LocalContext.current)
100+
101+
val videoPlayerState = rememberVideoPlayerState(
102+
exoPlayer = exoPlayer,
103+
hideSeconds = 4,
104+
)
114105

115-
// TODO: Move to ViewModel for better reuse
116-
val exoPlayer = rememberExoPlayer(context)
117106
LaunchedEffect(exoPlayer, movieDetails) {
118107
exoPlayer.setMediaItem(
119108
MediaItem.Builder()
@@ -137,13 +126,12 @@ fun VideoPlayerScreenContent(movieDetails: MovieDetails, onBackPressed: () -> Un
137126
}
138127

139128
var contentCurrentPosition by remember { mutableLongStateOf(0L) }
140-
var isPlaying: Boolean by remember { mutableStateOf(exoPlayer.isPlaying) }
129+
141130
// TODO: Update in a more thoughtful manner
142131
LaunchedEffect(Unit) {
143132
while (true) {
144133
delay(300)
145134
contentCurrentPosition = exoPlayer.currentPosition
146-
isPlaying = exoPlayer.isPlaying
147135
}
148136
}
149137

@@ -160,140 +148,53 @@ fun VideoPlayerScreenContent(movieDetails: MovieDetails, onBackPressed: () -> Un
160148
)
161149
.focusable()
162150
) {
163-
AndroidView(
164-
factory = {
165-
PlayerView(context).apply { useController = false }
166-
},
167-
update = { it.player = exoPlayer },
168-
onRelease = { exoPlayer.release() }
151+
PlayerSurface(
152+
player = exoPlayer,
153+
surfaceType = SURFACE_TYPE_TEXTURE_VIEW,
154+
modifier = Modifier.resizeWithContentScale(
155+
contentScale = ContentScale.Fit,
156+
sourceSizeDp = null
157+
)
169158
)
170159

171160
val focusRequester = remember { FocusRequester() }
172161
VideoPlayerOverlay(
173162
modifier = Modifier.align(Alignment.BottomCenter),
174163
focusRequester = focusRequester,
175-
state = videoPlayerState,
176-
isPlaying = isPlaying,
164+
isPlaying = videoPlayerState.isPlaying,
165+
isControlsVisible = videoPlayerState.isControlsVisible,
177166
centerButton = { VideoPlayerPulse(pulseState) },
178167
subtitles = { /* TODO Implement subtitles */ },
168+
showControls = videoPlayerState::showControls,
179169
controls = {
180170
VideoPlayerControls(
181-
movieDetails,
182-
isPlaying,
183-
contentCurrentPosition,
184-
exoPlayer,
185-
videoPlayerState,
186-
focusRequester
171+
movieDetails = movieDetails,
172+
contentCurrentPosition = contentCurrentPosition,
173+
contentDuration = exoPlayer.duration,
174+
isPlaying = videoPlayerState.isPlaying,
175+
focusRequester = focusRequester,
176+
onShowControls = videoPlayerState::showControls,
177+
onSeek = { exoPlayer.seekTo(exoPlayer.duration.times(it).toLong()) },
178+
onPlayPauseToggle = videoPlayerState::togglePlayPause
187179
)
188180
}
189181
)
190182
}
191183
}
192184

193-
@Composable
194-
fun VideoPlayerControls(
195-
movieDetails: MovieDetails,
196-
isPlaying: Boolean,
197-
contentCurrentPosition: Long,
198-
exoPlayer: ExoPlayer,
199-
state: VideoPlayerState,
200-
focusRequester: FocusRequester
201-
) {
202-
val onPlayPauseToggle = { shouldPlay: Boolean ->
203-
if (shouldPlay) {
204-
exoPlayer.play()
205-
} else {
206-
exoPlayer.pause()
207-
}
208-
}
209-
210-
VideoPlayerMainFrame(
211-
mediaTitle = {
212-
VideoPlayerMediaTitle(
213-
title = movieDetails.name,
214-
secondaryText = movieDetails.releaseDate,
215-
tertiaryText = movieDetails.director,
216-
type = VideoPlayerMediaTitleType.DEFAULT
217-
)
218-
},
219-
mediaActions = {
220-
Row(
221-
modifier = Modifier.padding(bottom = 16.dp),
222-
verticalAlignment = Alignment.CenterVertically
223-
) {
224-
VideoPlayerControlsIcon(
225-
icon = Icons.Default.AutoAwesomeMotion,
226-
state = state,
227-
isPlaying = isPlaying,
228-
contentDescription = StringConstants
229-
.Composable
230-
.VideoPlayerControlPlaylistButton
231-
)
232-
VideoPlayerControlsIcon(
233-
modifier = Modifier.padding(start = 12.dp),
234-
icon = Icons.Default.ClosedCaption,
235-
state = state,
236-
isPlaying = isPlaying,
237-
contentDescription = StringConstants
238-
.Composable
239-
.VideoPlayerControlClosedCaptionsButton
240-
)
241-
VideoPlayerControlsIcon(
242-
modifier = Modifier.padding(start = 12.dp),
243-
icon = Icons.Default.Settings,
244-
state = state,
245-
isPlaying = isPlaying,
246-
contentDescription = StringConstants
247-
.Composable
248-
.VideoPlayerControlSettingsButton
249-
)
250-
}
251-
},
252-
seeker = {
253-
VideoPlayerSeeker(
254-
focusRequester,
255-
state,
256-
isPlaying,
257-
onPlayPauseToggle,
258-
onSeek = { exoPlayer.seekTo(exoPlayer.duration.times(it).toLong()) },
259-
contentProgress = contentCurrentPosition.milliseconds,
260-
contentDuration = exoPlayer.duration.milliseconds
261-
)
262-
},
263-
more = null
264-
)
265-
}
266-
267-
@androidx.annotation.OptIn(UnstableApi::class)
268-
@Composable
269-
private fun rememberExoPlayer(context: Context) = remember {
270-
ExoPlayer.Builder(context)
271-
.setSeekForwardIncrementMs(10)
272-
.setSeekBackIncrementMs(10)
273-
.setMediaSourceFactory(
274-
ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
275-
)
276-
.setVideoScalingMode(C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING)
277-
.build()
278-
.apply {
279-
playWhenReady = true
280-
repeatMode = Player.REPEAT_MODE_ONE
281-
}
282-
}
283-
284185
private fun Modifier.dPadEvents(
285186
exoPlayer: ExoPlayer,
286187
videoPlayerState: VideoPlayerState,
287188
pulseState: VideoPlayerPulseState
288189
): Modifier = this.handleDPadKeyEvents(
289190
onLeft = {
290-
if (!videoPlayerState.controlsVisible) {
191+
if (!videoPlayerState.isControlsVisible) {
291192
exoPlayer.seekBack()
292193
pulseState.setType(BACK)
293194
}
294195
},
295196
onRight = {
296-
if (!videoPlayerState.controlsVisible) {
197+
if (!videoPlayerState.isControlsVisible) {
297198
exoPlayer.seekForward()
298199
pulseState.setType(FORWARD)
299200
}

0 commit comments

Comments
 (0)