16
16
17
17
package com.google.jetstream.presentation.screens.videoPlayer
18
18
19
- import android.content.Context
20
19
import android.net.Uri
21
20
import androidx.activity.compose.BackHandler
22
21
import androidx.compose.foundation.focusable
23
22
import androidx.compose.foundation.layout.Box
24
- import androidx.compose.foundation.layout.Row
25
23
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
31
24
import androidx.compose.runtime.Composable
32
25
import androidx.compose.runtime.LaunchedEffect
33
26
import androidx.compose.runtime.getValue
34
27
import androidx.compose.runtime.mutableLongStateOf
35
- import androidx.compose.runtime.mutableStateOf
36
28
import androidx.compose.runtime.remember
37
29
import androidx.compose.runtime.setValue
38
30
import androidx.compose.ui.Alignment
39
31
import androidx.compose.ui.Modifier
40
32
import androidx.compose.ui.focus.FocusRequester
33
+ import androidx.compose.ui.layout.ContentScale
41
34
import androidx.compose.ui.platform.LocalContext
42
- import androidx.compose.ui.unit.dp
43
- import androidx.compose.ui.viewinterop.AndroidView
44
35
import androidx.hilt.navigation.compose.hiltViewModel
45
36
import androidx.lifecycle.compose.collectAsStateWithLifecycle
46
37
import androidx.media3.common.C
47
38
import androidx.media3.common.MediaItem
48
- import androidx.media3.common.Player
49
39
import androidx.media3.common.util.UnstableApi
50
- import androidx.media3.datasource.DefaultDataSource
51
40
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
54
44
import com.google.jetstream.data.entities.MovieDetails
55
- import com.google.jetstream.data.util.StringConstants
56
45
import com.google.jetstream.presentation.common.Error
57
46
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
62
48
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerOverlay
63
49
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerPulse
64
50
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerPulse.Type.BACK
65
51
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerPulse.Type.FORWARD
66
52
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerPulseState
67
- import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerSeeker
68
53
import com.google.jetstream.presentation.screens.videoPlayer.components.VideoPlayerState
54
+ import com.google.jetstream.presentation.screens.videoPlayer.components.rememberPlayer
69
55
import com.google.jetstream.presentation.screens.videoPlayer.components.rememberVideoPlayerPulseState
70
56
import com.google.jetstream.presentation.screens.videoPlayer.components.rememberVideoPlayerState
71
57
import com.google.jetstream.presentation.utils.handleDPadKeyEvents
72
- import kotlin.time.Duration.Companion.milliseconds
73
58
import kotlinx.coroutines.delay
74
59
75
60
object VideoPlayerScreen {
@@ -94,9 +79,11 @@ fun VideoPlayerScreen(
94
79
is VideoPlayerScreenUiState .Loading -> {
95
80
Loading (modifier = Modifier .fillMaxSize())
96
81
}
82
+
97
83
is VideoPlayerScreenUiState .Error -> {
98
84
Error (modifier = Modifier .fillMaxSize())
99
85
}
86
+
100
87
is VideoPlayerScreenUiState .Done -> {
101
88
VideoPlayerScreenContent (
102
89
movieDetails = s.movieDetails,
@@ -109,11 +96,13 @@ fun VideoPlayerScreen(
109
96
@androidx.annotation.OptIn (UnstableApi ::class )
110
97
@Composable
111
98
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
+ )
114
105
115
- // TODO: Move to ViewModel for better reuse
116
- val exoPlayer = rememberExoPlayer(context)
117
106
LaunchedEffect (exoPlayer, movieDetails) {
118
107
exoPlayer.setMediaItem(
119
108
MediaItem .Builder ()
@@ -137,13 +126,12 @@ fun VideoPlayerScreenContent(movieDetails: MovieDetails, onBackPressed: () -> Un
137
126
}
138
127
139
128
var contentCurrentPosition by remember { mutableLongStateOf(0L ) }
140
- var isPlaying : Boolean by remember { mutableStateOf(exoPlayer.isPlaying) }
129
+
141
130
// TODO: Update in a more thoughtful manner
142
131
LaunchedEffect (Unit ) {
143
132
while (true ) {
144
133
delay(300 )
145
134
contentCurrentPosition = exoPlayer.currentPosition
146
- isPlaying = exoPlayer.isPlaying
147
135
}
148
136
}
149
137
@@ -160,140 +148,53 @@ fun VideoPlayerScreenContent(movieDetails: MovieDetails, onBackPressed: () -> Un
160
148
)
161
149
.focusable()
162
150
) {
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
+ )
169
158
)
170
159
171
160
val focusRequester = remember { FocusRequester () }
172
161
VideoPlayerOverlay (
173
162
modifier = Modifier .align(Alignment .BottomCenter ),
174
163
focusRequester = focusRequester,
175
- state = videoPlayerState,
176
- isPlaying = isPlaying ,
164
+ isPlaying = videoPlayerState.isPlaying ,
165
+ isControlsVisible = videoPlayerState.isControlsVisible ,
177
166
centerButton = { VideoPlayerPulse (pulseState) },
178
167
subtitles = { /* TODO Implement subtitles */ },
168
+ showControls = videoPlayerState::showControls,
179
169
controls = {
180
170
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
187
179
)
188
180
}
189
181
)
190
182
}
191
183
}
192
184
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
-
284
185
private fun Modifier.dPadEvents (
285
186
exoPlayer : ExoPlayer ,
286
187
videoPlayerState : VideoPlayerState ,
287
188
pulseState : VideoPlayerPulseState
288
189
): Modifier = this .handleDPadKeyEvents(
289
190
onLeft = {
290
- if (! videoPlayerState.controlsVisible ) {
191
+ if (! videoPlayerState.isControlsVisible ) {
291
192
exoPlayer.seekBack()
292
193
pulseState.setType(BACK )
293
194
}
294
195
},
295
196
onRight = {
296
- if (! videoPlayerState.controlsVisible ) {
197
+ if (! videoPlayerState.isControlsVisible ) {
297
198
exoPlayer.seekForward()
298
199
pulseState.setType(FORWARD )
299
200
}
0 commit comments