Skip to content

Commit 72896e7

Browse files
MGaetan89StaehliJ
andauthored
Introduce PillarboxPreloadManager (#726)
Co-authored-by: Joaquim Stähli <[email protected]>
1 parent 5dd18b0 commit 72896e7

File tree

11 files changed

+732
-192
lines changed

11 files changed

+732
-192
lines changed

pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,41 @@ data class Playlist(val title: String, val items: List<DemoItem>, val descriptio
139139
DemoItem.OverlapinglockedSegments
140140
)
141141
)
142+
val StoryUrns = Playlist(
143+
title = "Story urns",
144+
items = listOf(
145+
DemoItem.URN(
146+
title = "Mario vs Sonic",
147+
description = "Tataki 1",
148+
urn = "urn:rts:video:13950405"
149+
),
150+
DemoItem.URN(
151+
title = "Pourquoi Beyoncé fait de la country",
152+
description = "Tataki 2",
153+
urn = "urn:rts:video:14815579"
154+
),
155+
DemoItem.URN(
156+
title = "L'île North Sentinel",
157+
description = "Tataki 3",
158+
urn = "urn:rts:video:13795051"
159+
),
160+
DemoItem.URN(
161+
title = "Mourir pour ressembler à une idole",
162+
description = "Tataki 4",
163+
urn = "urn:rts:video:14020134"
164+
),
165+
DemoItem.URN(
166+
title = "Pourquoi les gens mangent des insectes ?",
167+
description = "Tataki 5",
168+
urn = "urn:rts:video:12631996"
169+
),
170+
DemoItem.URN(
171+
title = "Le concert de Beyoncé à Dubai",
172+
description = "Tataki 6",
173+
urn = "urn:rts:video:13752646"
174+
)
175+
)
176+
)
142177
private val googleStreams = Playlist(
143178
title = "Google streams",
144179
items = listOf(

pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import ch.srgssr.pillarbox.player.asset.timeRange.Chapter
2222
import ch.srgssr.pillarbox.player.asset.timeRange.Credit
2323
import ch.srgssr.pillarbox.player.extension.setHandleAudioFocus
2424
import ch.srgssr.pillarbox.player.extension.toRational
25+
import ch.srgssr.pillarbox.player.utils.StringUtil
2526
import kotlinx.coroutines.flow.MutableStateFlow
2627

2728
/**
@@ -95,11 +96,7 @@ class SimplePlayerViewModel(application: Application) : AndroidViewModel(applica
9596
}
9697

9798
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
98-
val reasonString = when (reason) {
99-
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED -> "TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED"
100-
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE -> "TIMELINE_CHANGE_REASON_SOURCE_UPDATE"
101-
else -> "?"
102-
}
99+
val reasonString = StringUtil.timelineChangeReasonString(reason)
103100
Log.d(
104101
TAG,
105102
"onTimelineChanged $reasonString ${player.currentMediaItem?.mediaId}" +
@@ -122,13 +119,7 @@ class SimplePlayerViewModel(application: Application) : AndroidViewModel(applica
122119
}
123120

124121
override fun onPlaybackStateChanged(@Player.State playbackState: Int) {
125-
val stateString = when (playbackState) {
126-
Player.STATE_IDLE -> "STATE_IDLE"
127-
Player.STATE_READY -> "STATE_READY"
128-
Player.STATE_BUFFERING -> "STATE_BUFFERING"
129-
Player.STATE_ENDED -> "STATE_ENDED"
130-
else -> "?"
131-
}
122+
val stateString = StringUtil.playerStateString(playbackState)
132123
Log.d(TAG, "onPlaybackStateChanged $stateString ${player.currentMediaItem?.mediaMetadata?.title}")
133124
}
134125

@@ -137,7 +128,7 @@ class SimplePlayerViewModel(application: Application) : AndroidViewModel(applica
137128
}
138129

139130
override fun onPlayerErrorChanged(error: PlaybackException?) {
140-
Log.d(TAG, "onPlayerErrorChanged $error")
131+
Log.d(TAG, "onPlayerErrorChanged", error)
141132
}
142133

143134
override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) {

pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/OptimizedStory.kt

Lines changed: 157 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,112 +4,205 @@
44
*/
55
package ch.srgssr.pillarbox.demo.ui.showcases.layouts
66

7-
import androidx.compose.animation.core.Spring
8-
import androidx.compose.animation.core.spring
9-
import androidx.compose.foundation.layout.Arrangement
7+
import android.os.Build
8+
import androidx.compose.animation.animateColorAsState
9+
import androidx.compose.foundation.background
1010
import androidx.compose.foundation.layout.Box
11-
import androidx.compose.foundation.layout.Row
11+
import androidx.compose.foundation.layout.Column
1212
import androidx.compose.foundation.layout.fillMaxHeight
1313
import androidx.compose.foundation.layout.fillMaxSize
1414
import androidx.compose.foundation.layout.fillMaxWidth
1515
import androidx.compose.foundation.layout.padding
1616
import androidx.compose.foundation.layout.size
17-
import androidx.compose.foundation.pager.HorizontalPager
1817
import androidx.compose.foundation.pager.PagerDefaults
1918
import androidx.compose.foundation.pager.PagerSnapDistance
19+
import androidx.compose.foundation.pager.VerticalPager
2020
import androidx.compose.foundation.pager.rememberPagerState
21+
import androidx.compose.foundation.shape.CircleShape
22+
import androidx.compose.material3.CircularProgressIndicator
23+
import androidx.compose.material3.LinearProgressIndicator
2124
import androidx.compose.material3.MaterialTheme
22-
import androidx.compose.material3.Text
2325
import androidx.compose.runtime.Composable
26+
import androidx.compose.runtime.LaunchedEffect
27+
import androidx.compose.runtime.collectAsState
28+
import androidx.compose.runtime.derivedStateOf
29+
import androidx.compose.runtime.getValue
30+
import androidx.compose.runtime.movableContentOf
31+
import androidx.compose.runtime.mutableIntStateOf
32+
import androidx.compose.runtime.remember
33+
import androidx.compose.runtime.setValue
2434
import androidx.compose.ui.Alignment
2535
import androidx.compose.ui.Modifier
2636
import androidx.compose.ui.draw.drawBehind
2737
import androidx.compose.ui.graphics.Color
38+
import androidx.compose.ui.tooling.preview.Preview
2839
import androidx.compose.ui.unit.dp
29-
import androidx.lifecycle.compose.LifecycleStartEffect
3040
import androidx.lifecycle.viewmodel.compose.viewModel
41+
import androidx.media3.common.Player
42+
import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme
3143
import ch.srgssr.pillarbox.demo.ui.theme.paddings
44+
import ch.srgssr.pillarbox.player.currentPositionAsFlow
45+
import ch.srgssr.pillarbox.player.playbackStateAsFlow
3246
import ch.srgssr.pillarbox.ui.ScaleMode
3347
import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface
3448
import ch.srgssr.pillarbox.ui.widget.player.SurfaceType
49+
import kotlinx.coroutines.delay
50+
import kotlinx.coroutines.flow.map
51+
import kotlin.time.Duration.Companion.milliseconds
52+
import kotlin.time.Duration.Companion.seconds
3553

3654
/**
37-
* Optimized story trying to reproduce story-like TikTok or Instagram.
38-
*
39-
* Surface view may sometimes keep on screen. Maybe if we use TextView with PlayerView this strange behavior will disappear.
55+
* Optimized story-like layout.
4056
*/
4157
@Composable
4258
fun OptimizedStory(storyViewModel: StoryViewModel = viewModel()) {
43-
val pagerState = rememberPagerState {
44-
storyViewModel.playlist.items.size
59+
val mediaItems = storyViewModel.mediaItems
60+
val pagerState = rememberPagerState { mediaItems.size }
61+
val settledPage by remember { derivedStateOf { pagerState.settledPage } }
62+
val currentPage by remember { derivedStateOf { pagerState.currentPage } }
63+
LaunchedEffect(settledPage) {
64+
storyViewModel.play(storyViewModel.getPlayer(settledPage))
65+
}
66+
LaunchedEffect(currentPage) {
67+
storyViewModel.setCurrentPage(currentPage)
4568
}
46-
LifecycleStartEffect(pagerState) {
47-
storyViewModel.getPlayerForPageNumber(pagerState.currentPage).play()
4869

49-
onStopOrDispose {
50-
storyViewModel.pauseAllPlayer()
70+
val movablePlayerView = remember {
71+
(0 until storyViewModel.playerCount).map { index ->
72+
movableContentOf {
73+
val player = remember { storyViewModel.getPlayer(index) }
74+
PlayerView(player, modifier = Modifier.fillMaxSize())
75+
}
5176
}
5277
}
5378

54-
val playlist = storyViewModel.playlist.items
5579
Box(modifier = Modifier.fillMaxSize()) {
56-
HorizontalPager(
57-
beyondViewportPageCount = 0,
58-
key = { page -> playlist[page].uri },
80+
VerticalPager(
5981
flingBehavior = PagerDefaults.flingBehavior(
6082
state = pagerState,
6183
pagerSnapDistance = PagerSnapDistance.atMost(0),
62-
snapAnimationSpec = spring(stiffness = Spring.StiffnessHigh)
6384
),
64-
pageSpacing = 1.dp,
65-
state = pagerState
85+
beyondViewportPageCount = 0,
86+
state = pagerState,
6687
) { page ->
67-
// When flinging -> may "load" more that 3 pages
68-
val currentPage = pagerState.currentPage
69-
val player = if (page == currentPage - 1 || page == currentPage + 1 || page == currentPage) {
70-
val playerConfig = storyViewModel.getPlayerAndMediaItemIndexForPage(page)
71-
val playerPage = storyViewModel.getPlayerFromIndex(playerConfig.first)
72-
playerPage.playWhenReady = currentPage == page
73-
playerPage.seekToDefaultPosition(playerConfig.second)
74-
playerPage
75-
} else {
76-
null
88+
LaunchedEffect(page) {
89+
storyViewModel.setupPlayerForPage(page)
7790
}
78-
player?.let {
79-
PlayerSurface(
80-
modifier = Modifier.fillMaxHeight(),
81-
scaleMode = ScaleMode.Crop,
82-
// Using Texture instead of Surface because on Android API 34 animations are not working well due to the hack
83-
// See PlayerSurfaceView in AndroidPlayerSurfaceView
84-
surfaceType = SurfaceType.Texture,
85-
player = player,
86-
)
87-
}
88-
Text(text = "Page $page")
91+
movablePlayerView[page % movablePlayerView.size]()
8992
}
90-
Row(
91-
Modifier
93+
94+
PagerIndicator(
95+
currentPage = settledPage,
96+
pageCount = mediaItems.size,
97+
modifier = Modifier
98+
.align(Alignment.CenterEnd)
99+
.padding(end = MaterialTheme.paddings.small),
100+
)
101+
}
102+
}
103+
104+
@Composable
105+
private fun PlayerView(player: Player, modifier: Modifier = Modifier) {
106+
val progress by remember {
107+
player.currentPositionAsFlow(100.milliseconds)
108+
.map { it / player.duration.coerceAtLeast(1L).toFloat() }
109+
}.collectAsState(0f)
110+
111+
val isBuffering by remember {
112+
player.playbackStateAsFlow().map { it == Player.STATE_BUFFERING }
113+
}.collectAsState(false)
114+
115+
Box(
116+
modifier = modifier,
117+
) {
118+
PlayerSurface(
119+
modifier = Modifier.fillMaxHeight(),
120+
scaleMode = ScaleMode.Crop,
121+
surfaceType = if (Build.VERSION.SDK_INT == Build.VERSION_CODES.UPSIDE_DOWN_CAKE) SurfaceType.Texture else SurfaceType.Surface,
122+
player = player,
123+
defaultAspectRatio = 9 / 16f,
124+
)
125+
126+
if (isBuffering) {
127+
CircularProgressIndicator(
128+
color = Color.White,
129+
modifier = Modifier.align(Alignment.Center),
130+
)
131+
}
132+
133+
LinearProgressIndicator(
134+
progress = { progress },
135+
modifier = Modifier
92136
.fillMaxWidth()
93-
.align(Alignment.BottomCenter)
94-
.padding(bottom = MaterialTheme.paddings.baseline),
95-
horizontalArrangement = Arrangement.Center
96-
) {
97-
repeat(playlist.size) { iteration ->
98-
val color = if (pagerState.currentPage == iteration) ColorIndicatorCurrent else ColorIndicator
99-
Box(
100-
modifier = Modifier
101-
.padding(MaterialTheme.paddings.micro)
102-
.size(IndicatorSize)
103-
.drawBehind {
104-
drawCircle(color)
105-
}
106-
107-
)
108-
}
137+
.align(Alignment.BottomCenter),
138+
color = PrimaryComponentColor,
139+
trackColor = SecondaryComponentColor,
140+
gapSize = 0.dp,
141+
drawStopIndicator = {},
142+
)
143+
}
144+
}
145+
146+
@Composable
147+
private fun PagerIndicator(
148+
currentPage: Int,
149+
pageCount: Int,
150+
modifier: Modifier = Modifier,
151+
) {
152+
Column(
153+
modifier = modifier
154+
.background(
155+
color = SurfaceComponentColor,
156+
shape = CircleShape,
157+
)
158+
.padding(MaterialTheme.paddings.micro),
159+
) {
160+
repeat(pageCount) { index ->
161+
val dotColor by animateColorAsState(
162+
targetValue = if (currentPage == index) PrimaryComponentColor else SecondaryComponentColor,
163+
label = "indicator-animation",
164+
)
165+
166+
Box(
167+
modifier = Modifier
168+
.padding(MaterialTheme.paddings.micro)
169+
.size(IndicatorSize)
170+
.drawBehind {
171+
drawCircle(dotColor)
172+
},
173+
)
109174
}
110175
}
111176
}
112177

113-
private val ColorIndicatorCurrent = Color.LightGray.copy(alpha = 0.75f)
114-
private val ColorIndicator = Color.LightGray.copy(alpha = 0.25f)
178+
@Preview
179+
@Composable
180+
private fun PageIndicatorPreview() {
181+
val pageCount = 5
182+
var step by remember { mutableIntStateOf(1) }
183+
var currentPage by remember { mutableIntStateOf(0) }
184+
185+
LaunchedEffect(currentPage) {
186+
delay(1.seconds)
187+
currentPage += step
188+
189+
if (currentPage == pageCount - 1) {
190+
step = -1
191+
} else if (currentPage == 0) {
192+
step = 1
193+
}
194+
}
195+
196+
PillarboxTheme {
197+
PagerIndicator(
198+
currentPage = currentPage,
199+
pageCount = pageCount,
200+
)
201+
}
202+
}
203+
204+
private val ComponentColor = Color.LightGray
205+
private val PrimaryComponentColor = ComponentColor.copy(alpha = 0.88f)
206+
private val SecondaryComponentColor = ComponentColor.copy(alpha = 0.33f)
207+
private val SurfaceComponentColor = ComponentColor.copy(alpha = 0.25f)
115208
private val IndicatorSize = 12.dp

0 commit comments

Comments
 (0)