Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Smooth seeking #391

Merged
merged 21 commits into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.media3.exoplayer.LoadControl
import ch.srgssr.pillarbox.core.business.akamai.AkamaiTokenDataSource
import ch.srgssr.pillarbox.core.business.integrationlayer.service.DefaultMediaCompositionDataSource
import ch.srgssr.pillarbox.core.business.tracker.DefaultMediaItemTrackerRepository
import ch.srgssr.pillarbox.player.PillarboxLoadControl
import ch.srgssr.pillarbox.player.PillarboxPlayer
import ch.srgssr.pillarbox.player.SeekIncrement
import ch.srgssr.pillarbox.player.data.MediaItemSource
Expand Down Expand Up @@ -42,7 +43,7 @@ object DefaultPillarbox {
mediaCompositionDataSource = DefaultMediaCompositionDataSource(),
),
dataSourceFactory: DataSource.Factory = AkamaiTokenDataSource.Factory(),
loadControl: LoadControl = DefaultLoadControl(),
loadControl: LoadControl = PillarboxLoadControl(),
): PillarboxPlayer {
return PillarboxPlayer(
context = context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package ch.srgssr.pillarbox.demo.shared.di

import android.content.Context
import androidx.media3.exoplayer.SeekParameters
import ch.srg.dataProvider.integrationlayer.dependencies.modules.IlServiceModule
import ch.srg.dataProvider.integrationlayer.dependencies.modules.OkHttpModule
import ch.srgssr.dataprovider.paging.DataProviderPaging
Expand All @@ -16,7 +15,6 @@ import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlHost
import ch.srgssr.pillarbox.core.business.integrationlayer.service.Vector.getVector
import ch.srgssr.pillarbox.demo.shared.data.MixedMediaItemSource
import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.ILRepository
import ch.srgssr.pillarbox.player.PillarboxLoadControl
import ch.srgssr.pillarbox.player.PillarboxPlayer
import java.net.URL

Expand Down Expand Up @@ -47,10 +45,7 @@ object PlayerModule {
return DefaultPillarbox(
context = context,
mediaItemSource = provideMixedItemSource(context, ilHost),
loadControl = PillarboxLoadControl(smoothSeeking = true)
).apply {
setSeekParameters(SeekParameters.CLOSEST_SYNC)
}
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ fun PlayerView(
player = player,
scaleMode = scaleMode
) {
if (isBuffering) {
if (isBuffering && !isSliderDragged) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center), color = Color.White)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.media3.common.Player
import ch.srgssr.pillarbox.player.PillarboxExoPlayer
import ch.srgssr.pillarbox.player.extension.canSeek
import ch.srgssr.pillarbox.ui.ProgressTrackerState
import ch.srgssr.pillarbox.ui.SimpleProgressTrackerState
Expand All @@ -41,7 +42,7 @@ fun rememberProgressTrackerState(
coroutineScope: CoroutineScope = rememberCoroutineScope()
): ProgressTrackerState {
return remember(player, smoothTracker) {
if (smoothTracker) {
if (smoothTracker && player is PillarboxExoPlayer) {
SmoothProgressTrackerState(player, coroutineScope)
} else {
SimpleProgressTrackerState(player, coroutineScope)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
Expand All @@ -17,18 +16,22 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.media3.common.Player
import ch.srgssr.pillarbox.core.business.DefaultPillarbox
import ch.srgssr.pillarbox.demo.R
import ch.srgssr.pillarbox.demo.shared.data.DemoItem
import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
import ch.srgssr.pillarbox.demo.ui.player.controls.PlayerPlaybackRow
import ch.srgssr.pillarbox.demo.ui.player.controls.PlayerTimeSlider
import ch.srgssr.pillarbox.demo.ui.player.controls.rememberProgressTrackerState
Expand All @@ -42,17 +45,35 @@ import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface
*/
@Composable
fun SmoothSeekingShowcase() {
val smoothSeekingViewModel: SmoothSeekingViewModel = viewModel()
val player = smoothSeekingViewModel.player
val context = LocalContext.current
val player = remember {
DefaultPillarbox(
context = context,
mediaItemSource = PlayerModule.provideMixedItemSource(context)
).apply {
addMediaItem(DemoItem.UnifiedStreamingOnDemand_Dash_TrickPlay.toMediaItem())
addMediaItem(DemoItem.UnifiedStreamingOnDemandTrickplay.toMediaItem())
addMediaItem(DemoItem.UnifiedStreamingOnDemand_Dash_FragmentedMP4.toMediaItem())
addMediaItem(DemoItem.OnDemandHLS.toMediaItem())
addMediaItem(DemoItem.GoogleDashH265.toMediaItem())
}
}
DisposableEffect(Unit) {
player.prepare()
player.play()
onDispose {
player.release()
}
}
var smoothSeekingEnabled by remember {
mutableStateOf(false)
}

Column {
Box(modifier = Modifier.aspectRatio(16 / 9f)) {
Box {
val playbackState by player.playbackStateAsState()
val isBuffering = playbackState == Player.STATE_BUFFERING
PlayerSurface(player = player) {
PlayerSurface(player = player, defaultAspectRatio = 16 / 9f) {
if (isBuffering) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center), color = Color.White)
Expand Down Expand Up @@ -86,7 +107,6 @@ fun SmoothSeekingShowcase() {
checked = smoothSeekingEnabled,
onCheckedChange = { enabled ->
smoothSeekingEnabled = enabled
smoothSeekingViewModel.setSmoothSeekingEnabled(enabled)
}
)

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.player

import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.SeekParameters

/**
* Pillarbox [ExoPlayer] interface extension.
*/
interface PillarboxExoPlayer : ExoPlayer {

/**
* Listener
*/
interface Listener : Player.Listener {
/**
* On smooth seeking enabled changed
*
* @param smoothSeekingEnabled The new value of [smoothSeekingEnabled]
*/
fun onSmoothSeekingEnabledChanged(smoothSeekingEnabled: Boolean)
}

/**
* Smooth seeking enabled
*
* When [smoothSeekingEnabled] is true, next seek events is send only after the current is done.
*
* To have the best result it is important to
* 1) Pause the player while seeking.
* 2) Set the [ExoPlayer.setSeekParameters] to [SeekParameters.CLOSEST_SYNC].
*/
var smoothSeekingEnabled: Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,45 +19,37 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

/**
* Pillarbox [LoadControl] implementation that optimize content loading for smooth seeking.
* Pillarbox [LoadControl] implementation that optimize content loading.
*
* @param bufferDurations Buffer duration when [smoothSeeking] is not enabled.
* @property smoothSeeking If enabled, use an optimized [LoadControl].
* @param bufferDurations Buffer durations to set [DefaultLoadControl.Builder.setBufferDurationsMs].
* @param allocator The [DefaultAllocator] to use in the internal [DefaultLoadControl].
*/
class PillarboxLoadControl(
bufferDurations: BufferDurations = BufferDurations(),
var smoothSeeking: Boolean = false,
bufferDurations: BufferDurations = DEFAULT_BUFFER_DURATIONS,
private val allocator: DefaultAllocator = DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
) : LoadControl {

private val fastSeekLoadControl: DefaultLoadControl = DefaultLoadControl.Builder()
.setAllocator(allocator)
.setDurations(FAST_SEEK_DURATIONS)
.setPrioritizeTimeOverSizeThresholds(true)
.build()
private val defaultLoadControl: DefaultLoadControl = DefaultLoadControl.Builder()
.setAllocator(allocator)
.setDurations(bufferDurations)
.setBufferDurationsMs(
bufferDurations.minBufferDuration.inWholeMilliseconds.toInt(),
bufferDurations.maxBufferDuration.inWholeMilliseconds.toInt(),
bufferDurations.bufferForPlayback.inWholeMilliseconds.toInt(),
bufferDurations.bufferForPlaybackAfterRebuffer.inWholeMilliseconds.toInt(),
)
.setPrioritizeTimeOverSizeThresholds(true)
.setBackBuffer(BACK_BUFFER_DURATION_MS, true)
.build()
private val activeLoadControl: LoadControl
get() {
return if (smoothSeeking) fastSeekLoadControl else defaultLoadControl
}

override fun onPrepared() {
fastSeekLoadControl.onPrepared()
defaultLoadControl.onPrepared()
}

override fun onStopped() {
fastSeekLoadControl.onStopped()
defaultLoadControl.onStopped()
}

override fun onReleased() {
fastSeekLoadControl.onReleased()
defaultLoadControl.onReleased()
}

Expand All @@ -66,19 +58,19 @@ class PillarboxLoadControl(
}

override fun getBackBufferDurationUs(): Long {
return BACK_BUFFER_DURATION_MS
return defaultLoadControl.backBufferDurationUs
}

override fun retainBackBufferFromKeyframe(): Boolean {
return true
return defaultLoadControl.retainBackBufferFromKeyframe()
}

override fun shouldContinueLoading(
playbackPositionUs: Long,
bufferedDurationUs: Long,
playbackSpeed: Float
): Boolean {
return activeLoadControl.shouldContinueLoading(playbackPositionUs, bufferedDurationUs, playbackSpeed)
return defaultLoadControl.shouldContinueLoading(playbackPositionUs, bufferedDurationUs, playbackSpeed)
}

override fun onTracksSelected(
Expand All @@ -88,20 +80,9 @@ class PillarboxLoadControl(
trackGroups: TrackGroupArray,
trackSelections: Array<out ExoTrackSelection>
) {
fastSeekLoadControl.onTracksSelected(timeline, mediaPeriodId, renderers, trackGroups, trackSelections)
defaultLoadControl.onTracksSelected(timeline, mediaPeriodId, renderers, trackGroups, trackSelections)
}

@Deprecated("Deprecated in Java")
override fun onTracksSelected(
renderers: Array<out Renderer>,
trackGroups: TrackGroupArray,
trackSelections: Array<out ExoTrackSelection>
) {
fastSeekLoadControl.onTracksSelected(renderers, trackGroups, trackSelections)
defaultLoadControl.onTracksSelected(renderers, trackGroups, trackSelections)
}

override fun shouldStartPlayback(
timeline: Timeline,
mediaPeriodId: MediaSource.MediaPeriodId,
Expand All @@ -110,17 +91,7 @@ class PillarboxLoadControl(
rebuffering: Boolean,
targetLiveOffsetUs: Long
): Boolean {
return activeLoadControl.shouldStartPlayback(timeline, mediaPeriodId, bufferedDurationUs, playbackSpeed, rebuffering, targetLiveOffsetUs)
}

@Deprecated("Deprecated in Java")
override fun shouldStartPlayback(
bufferedDurationUs: Long,
playbackSpeed: Float,
rebuffering: Boolean,
targetLiveOffsetUs: Long
): Boolean {
return activeLoadControl.shouldStartPlayback(bufferedDurationUs, playbackSpeed, rebuffering, targetLiveOffsetUs)
return defaultLoadControl.shouldStartPlayback(timeline, mediaPeriodId, bufferedDurationUs, playbackSpeed, rebuffering, targetLiveOffsetUs)
}

/**
Expand All @@ -140,22 +111,12 @@ class PillarboxLoadControl(
val bufferForPlaybackAfterRebuffer: Duration = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS.milliseconds,
)

private companion object SmoothLoadControl {
private const val BACK_BUFFER_DURATION_MS = 6_000L
private val FAST_SEEK_DURATIONS = BufferDurations(
minBufferDuration = 2.seconds,
maxBufferDuration = 2.seconds,
bufferForPlayback = 2.seconds,
bufferForPlaybackAfterRebuffer = 2.seconds,
private companion object {
private const val BACK_BUFFER_DURATION_MS = 4_000
private val DEFAULT_BUFFER_DURATIONS = BufferDurations(
bufferForPlayback = 500.milliseconds,
bufferForPlaybackAfterRebuffer = 1.seconds,
minBufferDuration = 1.seconds
)

private fun DefaultLoadControl.Builder.setDurations(durations: BufferDurations): DefaultLoadControl.Builder {
return setBufferDurationsMs(
durations.minBufferDuration.inWholeMilliseconds.toInt(),
durations.maxBufferDuration.inWholeMilliseconds.toInt(),
durations.bufferForPlayback.inWholeMilliseconds.toInt(),
durations.bufferForPlaybackAfterRebuffer.inWholeMilliseconds.toInt(),
)
}
}
}
Loading