Skip to content

Commit

Permalink
Redo story from scratch
Browse files Browse the repository at this point in the history
  • Loading branch information
StaehliJ committed Oct 9, 2024
1 parent 25f47ac commit ef160d4
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
Expand Down Expand Up @@ -57,8 +58,23 @@ fun OptimizedStory(storyViewModel: StoryViewModel = viewModel()) {
val mediaItems = storyViewModel.mediaItems
val pagerState = rememberPagerState { mediaItems.size }
val settledPage by remember { derivedStateOf { pagerState.settledPage } }
val currentPage by remember { derivedStateOf { pagerState.currentPage } }
LaunchedEffect(settledPage) {
storyViewModel.setActivePage(settledPage)
storyViewModel.play(storyViewModel.getPlayer(settledPage))
}
LaunchedEffect(currentPage) {
storyViewModel.setCurrentPage(currentPage)
}

val movablePlayerView = remember {
(0 until storyViewModel.playerCount).map { index ->
movableContentOf {
Box {
val player = remember { storyViewModel.getPlayer(index) }
PlayerView(player, modifier = Modifier.fillMaxSize())
}
}
}
}

Box(modifier = Modifier.fillMaxSize()) {
Expand All @@ -70,46 +86,10 @@ fun OptimizedStory(storyViewModel: StoryViewModel = viewModel()) {
beyondViewportPageCount = 0,
state = pagerState,
) { page ->
val player = remember { storyViewModel.getConfiguredPlayerForPageNumber(page) }

val progress by remember {
player.currentPositionAsFlow(100.milliseconds)
.map { it / player.duration.coerceAtLeast(1L).toFloat() }
}.collectAsState(0f)

val isBuffering by remember {
player.playbackStateAsFlow().map { it == Player.STATE_BUFFERING }
}.collectAsState(false)

Box(
modifier = Modifier.fillMaxSize(),
) {
PlayerSurface(
modifier = Modifier.fillMaxHeight(),
scaleMode = ScaleMode.Crop,
surfaceType = SurfaceType.Texture,
player = player,
defaultAspectRatio = 9 / 16f,
)

if (isBuffering) {
CircularProgressIndicator(
color = Color.White,
modifier = Modifier.align(Alignment.Center),
)
}

LinearProgressIndicator(
progress = { progress },
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
color = PrimaryComponentColor,
trackColor = SecondaryComponentColor,
gapSize = 0.dp,
drawStopIndicator = {},
)
LaunchedEffect(page) {
storyViewModel.setupPlayerForPage(page)
}
movablePlayerView[page % movablePlayerView.size]()
}

PagerIndicator(
Expand All @@ -122,6 +102,49 @@ fun OptimizedStory(storyViewModel: StoryViewModel = viewModel()) {
}
}

@Composable
private fun PlayerView(player: Player, modifier: Modifier = Modifier) {
val progress by remember {
player.currentPositionAsFlow(100.milliseconds)
.map { it / player.duration.coerceAtLeast(1L).toFloat() }
}.collectAsState(0f)

val isBuffering by remember {
player.playbackStateAsFlow().map { it == Player.STATE_BUFFERING }
}.collectAsState(false)

Box(
modifier = modifier,
) {
PlayerSurface(
modifier = Modifier
.fillMaxHeight(),
scaleMode = ScaleMode.Crop,
surfaceType = SurfaceType.Texture,
player = player,
defaultAspectRatio = 9 / 16f,
)

if (isBuffering) {
CircularProgressIndicator(
color = Color.White,
modifier = Modifier.align(Alignment.Center),
)
}

LinearProgressIndicator(
progress = { progress },
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
color = PrimaryComponentColor,
trackColor = SecondaryComponentColor,
gapSize = 0.dp,
drawStopIndicator = {},
)
}
}

@Composable
private fun PagerIndicator(
currentPage: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,78 +5,184 @@
package ch.srgssr.pillarbox.demo.ui.showcases.layouts

import android.app.Application
import android.os.HandlerThread
import android.os.Process
import android.util.SparseArray
import androidx.core.util.forEach
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.DefaultRendererCapabilitiesList
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.preload.DefaultPreloadManager
import androidx.media3.exoplayer.source.preload.DefaultPreloadManager.Status
import androidx.media3.exoplayer.source.preload.DefaultPreloadManager.Status.STAGE_LOADED_TO_POSITION_MS
import androidx.media3.exoplayer.source.preload.TargetPreloadStatusControl
import androidx.media3.exoplayer.upstream.DefaultAllocator
import ch.srgssr.pillarbox.core.business.source.SRGAssetLoader
import ch.srgssr.pillarbox.demo.shared.data.Playlist
import ch.srgssr.pillarbox.player.PillarboxBandwidthMeter
import ch.srgssr.pillarbox.player.PillarboxExoPlayer
import ch.srgssr.pillarbox.player.PillarboxPreloadManager
import ch.srgssr.pillarbox.player.PillarboxLoadControl
import ch.srgssr.pillarbox.player.PillarboxRenderersFactory
import ch.srgssr.pillarbox.player.PillarboxTrackSelector
import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory
import kotlin.math.abs
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

/**
* [ViewModel] that manages multiple [Player]s that can be used in a story-like layout.
*/
class StoryViewModel(application: Application) : AndroidViewModel(application) {
private val preloadManager = PillarboxPreloadManager(
context = application,
mediaSourceFactory = PillarboxMediaSourceFactory(application).apply {
addAssetLoader(SRGAssetLoader(application))
},
playerFactory = { playbackLooper ->
PillarboxExoPlayer(
context = application,
playbackLooper = playbackLooper,
).apply {
repeatMode = Player.REPEAT_MODE_ONE
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
prepare()
}
},

private val playbackThread: HandlerThread = HandlerThread("StoryMode-playback", Process.THREAD_PRIORITY_AUDIO).apply {
start()
}
private val playbackLooper = playbackThread.looper
private val allocator = DefaultAllocator(false, C.DEFAULT_BUFFER_SEGMENT_SIZE)
private val mediaSourceFactory = PillarboxMediaSourceFactory(application).apply {
addAssetLoader(SRGAssetLoader(application))
}
private val loadControl = PillarboxLoadControl(
bufferDurations = PillarboxLoadControl.BufferDurations(
minBufferDuration = 5.seconds,
maxBufferDuration = 20.seconds,
bufferForPlayback = 500.milliseconds,
bufferForPlaybackAfterRebuffer = 1_000.milliseconds,
),
allocator
)

private val preloadManager =
DefaultPreloadManager(
StoryPreloadStatusControl(),
mediaSourceFactory,
PillarboxTrackSelector(application).apply {
init({}, PillarboxBandwidthMeter(application))
},
PillarboxBandwidthMeter(application),
DefaultRendererCapabilitiesList.Factory(PillarboxRenderersFactory(application)),
allocator,
playbackLooper,
)

private var currentPage = C.INDEX_UNSET

private val players = SparseArray<PillarboxExoPlayer>(3).apply {
for (i in 0 until 3) {
put(
i,
PillarboxExoPlayer(
context = application,
playbackLooper = playbackLooper,
loadControl = loadControl
).apply {
repeatMode = Player.REPEAT_MODE_ONE
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
prepare()
}
)
}
}

/**
* Player count
*/
val playerCount: Int = players.size()

/**
* The list of items to play.
*/
val mediaItems: List<MediaItem> = Playlist.VideoUrns.items.map { it.toMediaItem() }
val mediaItems: List<MediaItem> = (Playlist.StoryUrns.items + Playlist.VideoUrns.items).map { it.toMediaItem() }

init {
mediaItems.forEachIndexed { index, mediaItem ->
preloadManager.add(mediaItem, index)
}
preloadManager.setCurrentPlayingIndex(0)
preloadManager.invalidate()

players.forEach { key, _ -> setupPlayerForPage(key) }
}

/**
* Set the [pageNumber] as the currently active page.
* Set up player for the [page].
*
* @param pageNumber The currently active page.
* @param page The page.
*/
fun setActivePage(pageNumber: Int) {
if (preloadManager.currentPlayingIndex == pageNumber) return
preloadManager.getCurrentlyPlayingPlayer()?.pause()
preloadManager.currentPlayingIndex = pageNumber
preloadManager.invalidate()
preloadManager.getCurrentlyPlayingPlayer()?.play()
fun setupPlayerForPage(page: Int) {
val player = getPlayer(page)
val mediaSource = getMediaSourceForPage(page)
if (mediaSource.mediaItem == player.currentMediaItem) return
player.setMediaSource(mediaSource)
if (player.playbackState == Player.STATE_IDLE) {
player.prepare()
}
}

private fun getMediaSourceForPage(page: Int): MediaSource = checkNotNull(preloadManager.getMediaSource(mediaItems[page]))

/**
* Get the [PillarboxExoPlayer] instance for page [pageNumber], with its media source set.
* @param page The page.
* @return the player for the [page].
*/
fun getPlayer(page: Int): PillarboxExoPlayer {
return players[page % players.size()]
}

/**
* Set the current page, do nothing if it is already the current.
*
* @param pageNumber The page number.
* @param page The current page
*/
fun getConfiguredPlayerForPageNumber(pageNumber: Int): PillarboxExoPlayer {
val mediaSource = checkNotNull(preloadManager.getMediaSource(mediaItems[pageNumber]))
val player = checkNotNull(preloadManager.getPlayer(pageNumber))
player.setMediaSource(mediaSource)
fun setCurrentPage(page: Int) {
if (currentPage == page) return
currentPage = page
preloadManager.setCurrentPlayingIndex(currentPage)
preloadManager.invalidate()
}

return player
/**
* Play
*
* @param player to play all others are paused.
*/
fun play(player: PillarboxExoPlayer) {
if (player.playWhenReady) return
players.forEach { _, value ->
value.pause()
value.seekToDefaultPosition()
if (value == player) {
player.play()
}
}
}

override fun onCleared() {
super.onCleared()
preloadManager.release()
players.forEach { _, value ->
value.release()
}
playbackThread.quitSafely()
}

/**
* Default implementation of [TargetPreloadStatusControl] that will preload the first second of the `n ± 1` item, and the first half-second of
* the `n ± 2,3` item, where `n` is the index of the current item.
*/
@Suppress("MagicNumber")
inner class StoryPreloadStatusControl : TargetPreloadStatusControl<Int> {
override fun getTargetPreloadStatus(rankingData: Int): TargetPreloadStatusControl.PreloadStatus? {
val offset = abs(rankingData - currentPage)

return when (offset) {
1 -> Status(STAGE_LOADED_TO_POSITION_MS, 1.seconds.inWholeMicroseconds)
2, 3, 4 -> Status(STAGE_LOADED_TO_POSITION_MS, 500.milliseconds.inWholeMicroseconds)
else -> null
}
}
}
}

0 comments on commit ef160d4

Please sign in to comment.