diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt index 91012177d..79c5c42f0 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt @@ -5,8 +5,6 @@ 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 @@ -14,19 +12,15 @@ 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.PillarboxLoadControl -import ch.srgssr.pillarbox.player.PillarboxRenderersFactory +import ch.srgssr.pillarbox.player.PillarboxPreloadManager import ch.srgssr.pillarbox.player.PillarboxTrackSelector import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory import kotlin.math.abs @@ -38,14 +32,21 @@ import kotlin.time.Duration.Companion.seconds */ class StoryViewModel(application: Application) : AndroidViewModel(application) { - 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 preloadManager = + PillarboxPreloadManager( + context = application, + targetPreloadStatusControl = StoryPreloadStatusControl(), + mediaSourceFactory = mediaSourceFactory, + trackSelector = PillarboxTrackSelector(application).apply { + parameters = parameters.buildUpon() + .setForceLowestBitrate(true) + .build() + } + ) + private val loadControl = PillarboxLoadControl( bufferDurations = PillarboxLoadControl.BufferDurations( minBufferDuration = 5.seconds, @@ -53,22 +54,9 @@ class StoryViewModel(application: Application) : AndroidViewModel(application) { bufferForPlayback = 500.milliseconds, bufferForPlaybackAfterRebuffer = 1_000.milliseconds, ), - allocator + preloadManager.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(3).apply { @@ -77,7 +65,7 @@ class StoryViewModel(application: Application) : AndroidViewModel(application) { i, PillarboxExoPlayer( context = application, - playbackLooper = playbackLooper, + playbackLooper = preloadManager.playbackLooper, loadControl = loadControl ).apply { repeatMode = Player.REPEAT_MODE_ONE @@ -102,7 +90,7 @@ class StoryViewModel(application: Application) : AndroidViewModel(application) { mediaItems.forEachIndexed { index, mediaItem -> preloadManager.add(mediaItem, index) } - preloadManager.setCurrentPlayingIndex(0) + preloadManager.currentPlayingIndex = 0 preloadManager.invalidate() players.forEach { key, _ -> setupPlayerForPage(key) } @@ -141,7 +129,7 @@ class StoryViewModel(application: Application) : AndroidViewModel(application) { fun setCurrentPage(page: Int) { if (currentPage == page) return currentPage = page - preloadManager.setCurrentPlayingIndex(currentPage) + preloadManager.currentPlayingIndex = currentPage preloadManager.invalidate() } @@ -162,16 +150,15 @@ class StoryViewModel(application: Application) : AndroidViewModel(application) { } override fun onCleared() { - preloadManager.release() players.forEach { _, value -> value.release() } - playbackThread.quitSafely() + preloadManager.release() } /** * 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. + * the `n ± 2,3,4` item, where `n` is the index of the current item. */ @Suppress("MagicNumber") inner class StoryPreloadStatusControl : TargetPreloadStatusControl { @@ -180,7 +167,7 @@ class StoryViewModel(application: Application) : AndroidViewModel(application) { 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) + 2, 3, 4 -> Status(STAGE_LOADED_TO_POSITION_MS, 1.milliseconds.inWholeMicroseconds) else -> null } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPreloadManager.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPreloadManager.kt index 5caddccb4..6c9839e92 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPreloadManager.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPreloadManager.kt @@ -11,7 +11,6 @@ import android.os.Process import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.exoplayer.DefaultRendererCapabilitiesList -import androidx.media3.exoplayer.LoadControl import androidx.media3.exoplayer.RendererCapabilitiesList import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.preload.DefaultPreloadManager @@ -19,6 +18,7 @@ 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.trackselection.TrackSelector +import androidx.media3.exoplayer.upstream.Allocator import androidx.media3.exoplayer.upstream.BandwidthMeter import androidx.media3.exoplayer.upstream.DefaultAllocator import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory @@ -27,72 +27,32 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds /** - * Helper class for the Media3's [DefaultPreloadManager]. The main difference between this class and [DefaultPreloadManager] is the addition of the - * [PlayerPool] argument. It allows the dynamic creation of a fixed number of [PillarboxExoPlayer] instances. - * - * This class provides the same methods as [DefaultPreloadManager] plus [getPlayer] and [getCurrentlyPlayingPlayer] to get an instance of a - * [PillarboxExoPlayer]. + * Helper class for the Media3's [DefaultPreloadManager]. * * @param context The current [Context]. * @param targetPreloadStatusControl The [TargetPreloadStatusControl] to decide when to preload an item and for how long. - * @param mediaSourceFactory The [MediaSource.Factory] to create each [MediaSource]. + * @param mediaSourceFactory The [PillarboxMediaSourceFactory] to create each [MediaSource]. * @param trackSelector The [TrackSelector] for this preload manager. * @param bandwidthMeter The [BandwidthMeter] for this preload manager. * @param rendererCapabilitiesListFactory The [RendererCapabilitiesList.Factory] for this preload manager. - * @param loadControl The [LoadControl] for this preload manager. + * @property allocator The [Allocator] for this preload manager. Have to be the same as the one used by the Player. * @param playbackThread The [Thread] on which the players run. - * @param playersCount The maximum number of [PillarboxExoPlayer] to create. - * @param playerFactory Called when a new [PillarboxExoPlayer] instance is necessary (up to `playersCount` times). The provided `Looper` **must** - * be passed to [PillarboxExoPlayer]'s constructor. * * @see DefaultPreloadManager */ class PillarboxPreloadManager( context: Context, targetPreloadStatusControl: TargetPreloadStatusControl? = null, - mediaSourceFactory: MediaSource.Factory = PillarboxMediaSourceFactory(context), - trackSelector: TrackSelector = PillarboxTrackSelector(context).apply { - init({}, PillarboxBandwidthMeter(context)) - }, + mediaSourceFactory: PillarboxMediaSourceFactory = PillarboxMediaSourceFactory(context), + trackSelector: TrackSelector = PillarboxTrackSelector(context), bandwidthMeter: BandwidthMeter = PillarboxBandwidthMeter(context), rendererCapabilitiesListFactory: RendererCapabilitiesList.Factory = DefaultRendererCapabilitiesList.Factory( PillarboxRenderersFactory(context) ), - private val loadControl: LoadControl = PillarboxLoadControl( - bufferDurations = PillarboxLoadControl.BufferDurations( - minBufferDuration = 5.seconds, - maxBufferDuration = 20.seconds, - bufferForPlayback = 500.milliseconds, - ), - allocator = DefaultAllocator(false, C.DEFAULT_BUFFER_SEGMENT_SIZE), - ), + val allocator: DefaultAllocator = DefaultAllocator(false, C.DEFAULT_BUFFER_SEGMENT_SIZE), private val playbackThread: HandlerThread = HandlerThread("PillarboxPreloadManager:Playback", Process.THREAD_PRIORITY_AUDIO), - playersCount: Int = 3, - playerFactory: (playbackLooper: Looper) -> PillarboxExoPlayer = { playbackLooper -> - PillarboxExoPlayer( - context = context, - loadControl = loadControl, - playbackLooper = playbackLooper, - ) - }, ) { - private val playerPool = PlayerPool( - playersCount = playersCount, - playerFactory = { playerFactory(playbackThread.looper) }, - ) - - // We use a lazy creation so the playbackThread can be started first - private val preloadManager by lazy { - DefaultPreloadManager( - targetPreloadStatusControl ?: DefaultTargetPreloadStatusControl(), - mediaSourceFactory, - trackSelector, - bandwidthMeter, - rendererCapabilitiesListFactory, - loadControl.allocator, - playbackThread.looper, - ) - } + private val preloadManager: DefaultPreloadManager /** * The index of the currently playing media item. @@ -113,8 +73,24 @@ class PillarboxPreloadManager( val sourceCount: Int get() = preloadManager.sourceCount + /** + * Playback looper to use with PillarboxExoPlayer. + */ + val playbackLooper: Looper + init { playbackThread.start() + playbackLooper = playbackThread.looper + trackSelector.init({}, bandwidthMeter) + preloadManager = DefaultPreloadManager( + targetPreloadStatusControl ?: DefaultTargetPreloadStatusControl(), + mediaSourceFactory, + trackSelector, + bandwidthMeter, + rendererCapabilitiesListFactory, + allocator, + playbackLooper, + ) } /** @@ -166,9 +142,8 @@ class PillarboxPreloadManager( * @see DefaultPreloadManager.release */ fun release() { - playerPool.release() preloadManager.release() - playbackThread.quit() + playbackThread.quitSafely() } /** @@ -202,25 +177,6 @@ class PillarboxPreloadManager( preloadManager.reset() } - /** - * Get a [PillarboxExoPlayer] for the given [index]. If the desired player has not been created yet, [PlayerPool.playerFactory] will be called. - * - * @param index The index of the [PillarboxExoPlayer] to retrieve. - * @return The desired [PillarboxExoPlayer], or `null` if [index] is negative. - */ - fun getPlayer(index: Int): PillarboxExoPlayer? { - return playerPool.getPlayerAtPosition(index) - } - - /** - * Get the currently playing [PillarboxExoPlayer]. - * - * @return The currently playing [PillarboxExoPlayer], or `null` if there is no active player. - */ - fun getCurrentlyPlayingPlayer(): PillarboxExoPlayer? { - return getPlayer(currentPlayingIndex) - } - /** * 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. diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PlayerPool.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PlayerPool.kt deleted file mode 100644 index d7638e20e..000000000 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PlayerPool.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player - -import android.util.SparseArray - -/** - * Pool of [playersCount] [PillarboxExoPlayer]. - * - * @param playersCount The maximum number of [PillarboxExoPlayer] managed by this pool. - * @param playerFactory The factory method to create a new [PillarboxExoPlayer]. - */ -class PlayerPool( - private val playersCount: Int, - private val playerFactory: () -> PillarboxExoPlayer, -) { - private val players: SparseArray - - init { - require(playersCount > 0) { - "playersCount must be greater than 0, but was $playersCount" - } - - players = SparseArray(playersCount) - } - - /** - * Get a [PillarboxExoPlayer] for the given [position]. If the desired player has not been created yet, [playerFactory] will be called. - * - * @param position The position of the [PillarboxExoPlayer] to retrieve. - * @return The desired [PillarboxExoPlayer], or `null` if [position] is negative. - */ - fun getPlayerAtPosition(position: Int): PillarboxExoPlayer? { - if (position < 0) { - return null - } - - val index = position % playersCount - - return players[index] ?: playerFactory().also { - players[index] = it - } - } - - /** - * Release this pool. This will also release all the managed [PillarboxExoPlayer]. - */ - fun release() { - repeat(playersCount) { index -> - players[index]?.release() - } - players.clear() - } -}