diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 32e79a970b..d0c1cff582 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -165,6 +165,7 @@ `TestPlayerRunHelper.run(player).ignoringNonFatalErrors().untilXXX()` method chain to disable this behavior. * Demo app: + * Use `DefaultPreloadManager` in the shortform demo app. * Remove deprecated symbols: * Remove `CronetDataSourceFactory`. Use `CronetDataSource.Factory` instead. diff --git a/demos/shortform/src/main/java/androidx/media3/demo/shortform/MainActivity.kt b/demos/shortform/src/main/java/androidx/media3/demo/shortform/MainActivity.kt index 4e842d09e3..06a8434e42 100644 --- a/demos/shortform/src/main/java/androidx/media3/demo/shortform/MainActivity.kt +++ b/demos/shortform/src/main/java/androidx/media3/demo/shortform/MainActivity.kt @@ -51,53 +51,14 @@ class MainActivity : AppCompatActivity() { } ) - var mediaItemsBackwardCacheSize = 2 - val mediaItemsBCacheSizeView = findViewById(R.id.media_items_b_cache_size) - mediaItemsBCacheSizeView.addTextChangedListener( - object : TextWatcher { - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit - - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit - - override fun afterTextChanged(s: Editable) { - val newText = mediaItemsBCacheSizeView.text.toString() - if (newText != "") { - mediaItemsBackwardCacheSize = max(1, min(newText.toInt(), 20)) - } - } - } - ) - - var mediaItemsForwardCacheSize = 3 - val mediaItemsFCacheSizeView = findViewById(R.id.media_items_f_cache_size) - mediaItemsFCacheSizeView.addTextChangedListener( - object : TextWatcher { - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit - - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit - - override fun afterTextChanged(s: Editable) { - val newText = mediaItemsFCacheSizeView.text.toString() - if (newText != "") { - mediaItemsForwardCacheSize = max(1, min(newText.toInt(), 20)) - } - } - } - ) - findViewById(R.id.view_pager_button).setOnClickListener { startActivity( - Intent(this, ViewPagerActivity::class.java) - .putExtra(NUM_PLAYERS_EXTRA, numberOfPlayers) - .putExtra(MEDIA_ITEMS_BACKWARD_CACHE_SIZE, mediaItemsBackwardCacheSize) - .putExtra(MEDIA_ITEMS_FORWARD_CACHE_SIZE, mediaItemsForwardCacheSize) + Intent(this, ViewPagerActivity::class.java).putExtra(NUM_PLAYERS_EXTRA, numberOfPlayers) ) } } companion object { - const val MEDIA_ITEMS_BACKWARD_CACHE_SIZE = "media_items_backward_cache_size" - const val MEDIA_ITEMS_FORWARD_CACHE_SIZE = "media_items_forward_cache_size" const val NUM_PLAYERS_EXTRA = "number_of_players" } } diff --git a/demos/shortform/src/main/java/androidx/media3/demo/shortform/MediaItemDatabase.kt b/demos/shortform/src/main/java/androidx/media3/demo/shortform/MediaItemDatabase.kt index 3a7845900e..8a78703a25 100644 --- a/demos/shortform/src/main/java/androidx/media3/demo/shortform/MediaItemDatabase.kt +++ b/demos/shortform/src/main/java/androidx/media3/demo/shortform/MediaItemDatabase.kt @@ -16,51 +16,22 @@ package androidx.media3.demo.shortform import androidx.media3.common.MediaItem -import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi @UnstableApi -class MediaItemDatabase() { +class MediaItemDatabase { - var lCacheSize: Int = 2 - var rCacheSize: Int = 7 - private val mediaItems = + private val mediaUris = mutableListOf( - MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_1.mp4"), - MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_2.mp4"), - MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_3.mp4"), - MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_4.mp4"), - MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_6.mp4") + "https://storage.googleapis.com/exoplayer-test-media-0/shortform_1.mp4", + "https://storage.googleapis.com/exoplayer-test-media-0/shortform_2.mp4", + "https://storage.googleapis.com/exoplayer-test-media-0/shortform_3.mp4", + "https://storage.googleapis.com/exoplayer-test-media-0/shortform_4.mp4", + "https://storage.googleapis.com/exoplayer-test-media-0/shortform_6.mp4", ) - // Effective sliding window of size = lCacheSize + 1 + rCacheSize - private val slidingWindowCache = HashMap() - - private fun getRaw(index: Int): MediaItem { - return mediaItems[index.mod(mediaItems.size)] - } - - private fun getCached(index: Int): MediaItem { - var mediaItem = slidingWindowCache[index] - if (mediaItem == null) { - mediaItem = getRaw(index) - slidingWindowCache[index] = mediaItem - Log.d("viewpager", "Put URL ${mediaItem.localConfiguration?.uri} into sliding cache") - slidingWindowCache.remove(index - lCacheSize - 1) - slidingWindowCache.remove(index + rCacheSize + 1) - } - return mediaItem - } - fun get(index: Int): MediaItem { - return getCached(index) - } - - fun get(fromIndex: Int, toIndex: Int): List { - val result: MutableList = mutableListOf() - for (i in fromIndex..toIndex) { - result.add(get(i)) - } - return result + val uri = mediaUris.get(index.mod(mediaUris.size)) + return return MediaItem.Builder().setUri(uri).setMediaId(index.toString()).build() } } diff --git a/demos/shortform/src/main/java/androidx/media3/demo/shortform/MediaSourceManager.kt b/demos/shortform/src/main/java/androidx/media3/demo/shortform/MediaSourceManager.kt deleted file mode 100644 index ae12b627be..0000000000 --- a/demos/shortform/src/main/java/androidx/media3/demo/shortform/MediaSourceManager.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package androidx.media3.demo.shortform - -import android.os.Handler -import android.os.HandlerThread -import android.os.Looper -import android.os.Process -import androidx.media3.common.MediaItem -import androidx.media3.common.Metadata -import androidx.media3.common.text.CueGroup -import androidx.media3.common.util.UnstableApi -import androidx.media3.common.util.Util -import androidx.media3.exoplayer.RendererCapabilities -import androidx.media3.exoplayer.RenderersFactory -import androidx.media3.exoplayer.audio.AudioRendererEventListener -import androidx.media3.exoplayer.source.MediaSource -import androidx.media3.exoplayer.source.preload.PreloadMediaSource -import androidx.media3.exoplayer.trackselection.TrackSelector -import androidx.media3.exoplayer.upstream.Allocator -import androidx.media3.exoplayer.upstream.BandwidthMeter -import androidx.media3.exoplayer.video.VideoRendererEventListener - -@UnstableApi -class MediaSourceManager( - mediaSourceFactory: MediaSource.Factory, - preloadLooper: Looper, - allocator: Allocator, - renderersFactory: RenderersFactory, - trackSelector: TrackSelector, - bandwidthMeter: BandwidthMeter, -) { - private val mediaSourcesThread = HandlerThread("playback-thread", Process.THREAD_PRIORITY_AUDIO) - private var handler: Handler - private var sourceMap: MutableMap = HashMap() - private var preloadMediaSourceFactory: PreloadMediaSource.Factory - - init { - mediaSourcesThread.start() - handler = Handler(mediaSourcesThread.looper) - trackSelector.init({}, bandwidthMeter) - preloadMediaSourceFactory = - PreloadMediaSource.Factory( - mediaSourceFactory, - PreloadControlImpl(targetPreloadPositionUs = 5_000_000L), - trackSelector, - bandwidthMeter, - getRendererCapabilities(renderersFactory = renderersFactory), - allocator, - preloadLooper, - ) - } - - fun add(mediaItem: MediaItem) { - if (!sourceMap.containsKey(mediaItem)) { - val preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem) - sourceMap[mediaItem] = preloadMediaSource - handler.post { preloadMediaSource.preload(/* startPositionUs= */ 0L) } - } - } - - fun addAll(mediaItems: List) { - mediaItems.forEach { - if (!sourceMap.containsKey(it)) { - add(it) - } - } - } - - operator fun get(mediaItem: MediaItem): PreloadMediaSource { - if (!sourceMap.containsKey(mediaItem)) { - add(mediaItem) - } - return sourceMap[mediaItem]!! - } - - /** Releases the instance. The instance can't be used after being released. */ - fun release() { - sourceMap.keys.forEach { sourceMap[it]!!.releasePreloadMediaSource() } - handler.removeCallbacksAndMessages(null) - mediaSourcesThread.quit() - } - - @UnstableApi - private fun getRendererCapabilities( - renderersFactory: RenderersFactory - ): Array { - val renderers = - renderersFactory.createRenderers( - Util.createHandlerForCurrentOrMainLooper(), - object : VideoRendererEventListener {}, - object : AudioRendererEventListener {}, - { _: CueGroup? -> }, - ) { _: Metadata -> - } - val capabilities = ArrayList() - for (i in renderers.indices) { - capabilities.add(renderers[i].capabilities) - } - return capabilities.toTypedArray() - } - - companion object { - private const val TAG = "MSManager" - } - - private class PreloadControlImpl(private val targetPreloadPositionUs: Long) : - PreloadMediaSource.PreloadControl { - - override fun onTimelineRefreshed(mediaSource: PreloadMediaSource): Boolean { - return true - } - - override fun onPrepared(mediaSource: PreloadMediaSource): Boolean { - return true - } - - override fun onContinueLoadingRequested( - mediaSource: PreloadMediaSource, - bufferedPositionUs: Long, - ): Boolean { - return bufferedPositionUs < targetPreloadPositionUs - } - - override fun onUsedByPlayer(mediaSource: PreloadMediaSource) { - // Implementation is no-op until the whole class is removed with the adoption of - // DefaultPreloadManager. - } - } -} diff --git a/demos/shortform/src/main/java/androidx/media3/demo/shortform/PlayerPool.kt b/demos/shortform/src/main/java/androidx/media3/demo/shortform/PlayerPool.kt index 6340835b71..e17e26b634 100644 --- a/demos/shortform/src/main/java/androidx/media3/demo/shortform/PlayerPool.kt +++ b/demos/shortform/src/main/java/androidx/media3/demo/shortform/PlayerPool.kt @@ -39,7 +39,7 @@ class PlayerPool( playbackLooper: Looper, loadControl: LoadControl, renderersFactory: RenderersFactory, - bandwidthMeter: BandwidthMeter + bandwidthMeter: BandwidthMeter, ) { /** Creates a player instance to be used by the pool. */ @@ -56,12 +56,6 @@ class PlayerPool( fun acquirePlayer(token: Int, callback: (ExoPlayer) -> Unit) { synchronized(playerMap) { - if (playerMap.size < numberOfPlayers) { - val player = playerFactory.createPlayer() - playerMap[playerMap.size] = player - callback.invoke(player) - return - } // Add token to set of views requesting players playerRequestTokenSet.add(token) acquirePlayerInternal(token, callback) @@ -75,6 +69,12 @@ class PlayerPool( playerMap[playerNumber]?.let { callback.invoke(it) } playerRequestTokenSet.remove(token) return + } else if (playerMap.size < numberOfPlayers) { + val player = playerFactory.createPlayer() + playerMap[playerMap.size] = player + callback.invoke(player) + playerRequestTokenSet.remove(token) + return } else if (playerRequestTokenSet.contains(token)) { Handler(Looper.getMainLooper()).postDelayed({ acquirePlayerInternal(token, callback) }, 500) } @@ -130,7 +130,7 @@ class PlayerPool( private val playbackLooper: Looper, private val loadControl: LoadControl, private val renderersFactory: RenderersFactory, - private val bandwidthMeter: BandwidthMeter + private val bandwidthMeter: BandwidthMeter, ) : PlayerFactory { private var playerCounter = 0 diff --git a/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerActivity.kt b/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerActivity.kt index 6ddca36e7f..2ed9b2925c 100644 --- a/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerActivity.kt +++ b/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerActivity.kt @@ -31,23 +31,21 @@ class ViewPagerActivity : AppCompatActivity() { private var numberOfPlayers = 3 private var mediaItemDatabase = MediaItemDatabase() + companion object { + private const val TAG = "ViewPagerActivity" + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_view_pager) numberOfPlayers = intent.getIntExtra(MainActivity.NUM_PLAYERS_EXTRA, numberOfPlayers) - mediaItemDatabase.lCacheSize = - intent.getIntExtra(MainActivity.MEDIA_ITEMS_BACKWARD_CACHE_SIZE, mediaItemDatabase.lCacheSize) - mediaItemDatabase.rCacheSize = - intent.getIntExtra(MainActivity.MEDIA_ITEMS_FORWARD_CACHE_SIZE, mediaItemDatabase.rCacheSize) - Log.d("viewpager", "Using a pool of $numberOfPlayers players") - Log.d("viewpager", "Backward cache is of size: ${mediaItemDatabase.lCacheSize}") - Log.d("viewpager", "Forward cache is of size: ${mediaItemDatabase.rCacheSize}") + Log.d(TAG, "Using a pool of $numberOfPlayers players") viewPagerView = findViewById(R.id.viewPager) viewPagerView.offscreenPageLimit = 1 viewPagerView.registerOnPageChangeCallback( object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { - adapter.play(position) + adapter.onPageSelected(position) } } ) diff --git a/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaAdapter.kt b/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaAdapter.kt index 434d729233..97438f9480 100644 --- a/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaAdapter.kt +++ b/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaAdapter.kt @@ -18,40 +18,62 @@ package androidx.media3.demo.shortform.viewpager import android.content.Context import android.os.HandlerThread import android.os.Process -import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi -import androidx.media3.datasource.DefaultDataSource import androidx.media3.demo.shortform.MediaItemDatabase -import androidx.media3.demo.shortform.MediaSourceManager import androidx.media3.demo.shortform.PlayerPool import androidx.media3.demo.shortform.R import androidx.media3.exoplayer.DefaultLoadControl +import androidx.media3.exoplayer.DefaultRendererCapabilitiesList import androidx.media3.exoplayer.DefaultRenderersFactory -import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.exoplayer.source.preload.DefaultPreloadManager +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.DefaultTrackSelector import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter -import androidx.media3.exoplayer.util.EventLogger import androidx.recyclerview.widget.RecyclerView +import kotlin.math.abs @UnstableApi class ViewPagerMediaAdapter( private val mediaItemDatabase: MediaItemDatabase, numberOfPlayers: Int, - private val context: Context + context: Context, ) : RecyclerView.Adapter() { private val playbackThread: HandlerThread = HandlerThread("playback-thread", Process.THREAD_PRIORITY_AUDIO) - private val mediaSourceManager: MediaSourceManager - private var viewCounter = 0 + private val preloadManager: DefaultPreloadManager + private val currentMediaItemsAndIndexes: ArrayDeque> = ArrayDeque() private var playerPool: PlayerPool private val holderMap: MutableMap + private var currentPlayingIndex: Int = C.INDEX_UNSET + + companion object { + private const val TAG = "ViewPagerMediaAdapter" + private const val LOAD_CONTROL_MIN_BUFFER_MS = 5_000 + private const val LOAD_CONTROL_MAX_BUFFER_MS = 20_000 + private const val LOAD_CONTROL_BUFFER_FOR_PLAYBACK_MS = 500 + private const val MANAGED_ITEM_COUNT = 10 + private const val ITEM_ADD_REMOVE_COUNT = 4 + } init { playbackThread.start() - val loadControl = DefaultLoadControl() + val loadControl = + DefaultLoadControl.Builder() + .setBufferDurationsMs( + LOAD_CONTROL_MIN_BUFFER_MS, + LOAD_CONTROL_MAX_BUFFER_MS, + LOAD_CONTROL_BUFFER_FOR_PLAYBACK_MS, + DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, + ) + .setPrioritizeTimeOverSizeThresholds(true) + .build() val renderersFactory = DefaultRenderersFactory(context) playerPool = PlayerPool( @@ -60,53 +82,72 @@ class ViewPagerMediaAdapter( playbackThread.looper, loadControl, renderersFactory, - DefaultBandwidthMeter.getSingletonInstance(context) + DefaultBandwidthMeter.getSingletonInstance(context), ) holderMap = mutableMapOf() - mediaSourceManager = - MediaSourceManager( - DefaultMediaSourceFactory(DefaultDataSource.Factory(context)), - playbackThread.looper, + val trackSelector = DefaultTrackSelector(context) + trackSelector.init({}, DefaultBandwidthMeter.getSingletonInstance(context)) + preloadManager = + DefaultPreloadManager( + DefaultPreloadControl(), + DefaultMediaSourceFactory(context), + trackSelector, + DefaultBandwidthMeter.getSingletonInstance(context), + DefaultRendererCapabilitiesList.Factory(renderersFactory), loadControl.allocator, - renderersFactory, - DefaultTrackSelector(context), - DefaultBandwidthMeter.getSingletonInstance(context) + playbackThread.looper, ) + for (i in 0 until MANAGED_ITEM_COUNT) { + addMediaItem(index = i, isAddingToRightEnd = true) + } + preloadManager.invalidate() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewPagerMediaHolder { - Log.d("viewpager", "onCreateViewHolder: $viewCounter") val view = LayoutInflater.from(parent.context).inflate(R.layout.media_item_view_pager, parent, false) - val holder = ViewPagerMediaHolder(view, viewCounter++, playerPool) + val holder = ViewPagerMediaHolder(view, playerPool) view.addOnAttachStateChangeListener(holder) return holder } override fun onBindViewHolder(holder: ViewPagerMediaHolder, position: Int) { - // TODO could give more information to the database about which item to supply - // e.g. based on how long the previous item was in view (i.e. "popularity" of content) - // need to measure how long it's been since the last onBindViewHolder call val mediaItem = mediaItemDatabase.get(position) - Log.d("viewpager", "onBindViewHolder: Getting item at position $position") - holder.bindData(position, mediaSourceManager[mediaItem]) - // We are moving to , so should prepare the next couple of items - // Potentially most of those are already cached on the database side because of the sliding - // window and we would only require one more item at index=mediaItemHorizon - val mediaItemHorizon = position + mediaItemDatabase.rCacheSize - val reachableMediaItems = - mediaItemDatabase.get(fromIndex = position + 1, toIndex = mediaItemHorizon) - // Same as with the data retrieval, most items will have been converted to MediaSources and - // prepared already, but not on the first swipe - mediaSourceManager.addAll(reachableMediaItems) + Log.d(TAG, "onBindViewHolder: Getting item at position $position") + var currentMediaSource = preloadManager.getMediaSource(mediaItem) + if (currentMediaSource == null) { + preloadManager.add(mediaItem, position) + currentMediaSource = preloadManager.getMediaSource(mediaItem)!! + } + holder.bindData(currentMediaSource) } override fun onViewAttachedToWindow(holder: ViewPagerMediaHolder) { - holderMap[holder.currentToken] = holder + val holderBindingAdapterPosition = holder.bindingAdapterPosition + holderMap[holderBindingAdapterPosition] = holder + + if (!currentMediaItemsAndIndexes.isEmpty()) { + val leftMostIndex = currentMediaItemsAndIndexes.first().second + val rightMostIndex = currentMediaItemsAndIndexes.last().second + + if (rightMostIndex - holderBindingAdapterPosition <= 2) { + Log.d(TAG, "onViewAttachedToWindow: Approaching to the rightmost item") + for (i in 1 until ITEM_ADD_REMOVE_COUNT + 1) { + addMediaItem(index = rightMostIndex + i, isAddingToRightEnd = true) + removeMediaItem(isRemovingFromRightEnd = false) + } + } else if (holderBindingAdapterPosition - leftMostIndex <= 2) { + Log.d(TAG, "onViewAttachedToWindow: Approaching to the leftmost item") + for (i in 1 until ITEM_ADD_REMOVE_COUNT + 1) { + addMediaItem(index = leftMostIndex - i, isAddingToRightEnd = false) + removeMediaItem(isRemovingFromRightEnd = true) + } + } + } } override fun onViewDetachedFromWindow(holder: ViewPagerMediaHolder) { - holderMap.remove(holder.currentToken) + holderMap.remove(holder.bindingAdapterPosition) } override fun getItemCount(): Int { @@ -114,38 +155,55 @@ class ViewPagerMediaAdapter( return Int.MAX_VALUE } - override fun onViewRecycled(holder: ViewPagerMediaHolder) { - super.onViewRecycled(holder) - } - fun onDestroy() { - playbackThread.quit() + preloadManager.release() playerPool.destroyPlayers() - mediaSourceManager.release() + playbackThread.quit() } - fun play(position: Int) { - holderMap[position]?.let { holder -> holder.player?.let { playerPool.play(it) } } + fun onPageSelected(position: Int) { + currentPlayingIndex = position + holderMap[position]?.playIfPossible() + preloadManager.setCurrentPlayingIndex(position) + preloadManager.invalidate() } - inner class Factory : PlayerPool.PlayerFactory { - private var playerCounter = 0 + private fun addMediaItem(index: Int, isAddingToRightEnd: Boolean) { + if (index < 0) { + return + } + Log.d(TAG, "addMediaItem: Adding item at index $index") + val mediaItem = mediaItemDatabase.get(index) + preloadManager.add(mediaItem, index) + if (isAddingToRightEnd) { + currentMediaItemsAndIndexes.addLast(Pair(mediaItem, index)) + } else { + currentMediaItemsAndIndexes.addFirst(Pair(mediaItem, index)) + } + } - override fun createPlayer(): ExoPlayer { - val loadControl = - DefaultLoadControl.Builder() - .setBufferDurationsMs( - /* minBufferMs= */ 15_000, - /* maxBufferMs= */ 15_000, - /* bufferForPlaybackMs= */ 500, - /* bufferForPlaybackAfterRebufferMs= */ 1_000 - ) - .build() - val player = ExoPlayer.Builder(context).setLoadControl(loadControl).build() - player.addAnalyticsListener(EventLogger("player-$playerCounter")) - playerCounter++ - player.repeatMode = ExoPlayer.REPEAT_MODE_ONE - return player + private fun removeMediaItem(isRemovingFromRightEnd: Boolean) { + if (currentMediaItemsAndIndexes.size <= MANAGED_ITEM_COUNT) { + return + } + val itemAndIndex = + if (isRemovingFromRightEnd) { + currentMediaItemsAndIndexes.removeLast() + } else { + currentMediaItemsAndIndexes.removeFirst() + } + Log.d(TAG, "removeMediaItem: Removing item at index ${itemAndIndex.second}") + preloadManager.remove(itemAndIndex.first) + } + + inner class DefaultPreloadControl : TargetPreloadStatusControl { + override fun getTargetPreloadStatus(rankingData: Int): DefaultPreloadManager.Status? { + if (abs(rankingData - currentPlayingIndex) == 2) { + return DefaultPreloadManager.Status(STAGE_LOADED_TO_POSITION_MS, 500L) + } else if (abs(rankingData - currentPlayingIndex) == 1) { + return DefaultPreloadManager.Status(STAGE_LOADED_TO_POSITION_MS, 1000L) + } + return null } } } diff --git a/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaHolder.kt b/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaHolder.kt index 98c54d5d65..8f9eb0bb50 100644 --- a/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaHolder.kt +++ b/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaHolder.kt @@ -15,30 +15,30 @@ */ package androidx.media3.demo.shortform.viewpager -import android.util.Log import android.view.View import androidx.annotation.OptIn -import androidx.media3.common.Player +import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi import androidx.media3.demo.shortform.PlayerPool import androidx.media3.demo.shortform.R import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.source.preload.PreloadMediaSource +import androidx.media3.exoplayer.source.MediaSource import androidx.media3.ui.PlayerView import androidx.recyclerview.widget.RecyclerView -@OptIn(UnstableApi::class) // Using PreloadMediaSource. -class ViewPagerMediaHolder( - itemView: View, - private val viewCounter: Int, - private val playerPool: PlayerPool -) : RecyclerView.ViewHolder(itemView), View.OnAttachStateChangeListener { +@OptIn(UnstableApi::class) +class ViewPagerMediaHolder(itemView: View, private val playerPool: PlayerPool) : + RecyclerView.ViewHolder(itemView), View.OnAttachStateChangeListener { private val playerView: PlayerView = itemView.findViewById(R.id.player_view) private var exoPlayer: ExoPlayer? = null private var isInView: Boolean = false - private var token: Int = -1 + private var pendingPlayRequestUponSetupPlayer: Boolean = false - private lateinit var mediaSource: PreloadMediaSource + private lateinit var mediaSource: MediaSource + + companion object { + private const val TAG = "ViewPagerMediaHolder" + } init { // Define click listener for the ViewHolder's View @@ -49,46 +49,44 @@ class ViewPagerMediaHolder( } } - val currentToken: Int - get() { - return token - } - - val player: Player? + val player: ExoPlayer? get() { return exoPlayer } override fun onViewAttachedToWindow(view: View) { - Log.d("viewpager", "onViewAttachedToWindow: $viewCounter") + Log.d(TAG, "onViewAttachedToWindow: $bindingAdapterPosition") isInView = true if (player == null) { - playerPool.acquirePlayer(token, ::setupPlayer) + playerPool.acquirePlayer(bindingAdapterPosition, ::setupPlayer) } } override fun onViewDetachedFromWindow(view: View) { - Log.d("viewpager", "onViewDetachedFromWindow: $viewCounter") + Log.d(TAG, "onViewDetachedFromWindow: $bindingAdapterPosition") isInView = false releasePlayer(exoPlayer) - // This is a hacky way of keep preloading sources that are removed from players. This does only - // work because the demo app cycles endlessly through the same 5 URIs. Preloading is still - // uncoordinated meaning it just preloading as soon as this method is called. - mediaSource.preload(0) } - fun bindData(token: Int, mediaSource: PreloadMediaSource) { + fun bindData(mediaSource: MediaSource) { this.mediaSource = mediaSource - this.token = token } - fun releasePlayer(player: ExoPlayer?) { - playerPool.releasePlayer(token, player ?: exoPlayer) + fun playIfPossible() { + player?.let { playerPool.play(it) } + if (player == null) { + Log.d(TAG, "playIfPossible: The player hasn't been setup yet") + pendingPlayRequestUponSetupPlayer = true + } + } + + private fun releasePlayer(player: ExoPlayer?) { + playerPool.releasePlayer(bindingAdapterPosition, player ?: exoPlayer) this.exoPlayer = null playerView.player = null } - fun setupPlayer(player: ExoPlayer) { + private fun setupPlayer(player: ExoPlayer) { if (!isInView) { releasePlayer(player) } else { @@ -103,6 +101,10 @@ class ViewPagerMediaHolder( this@ViewPagerMediaHolder.exoPlayer = player player.prepare() playerView.player = player + if (pendingPlayRequestUponSetupPlayer) { + playerPool.play(player) + pendingPlayRequestUponSetupPlayer = false + } } } } diff --git a/demos/shortform/src/main/res/layout/activity_main.xml b/demos/shortform/src/main/res/layout/activity_main.xml index 34d1dea9d3..79fad17427 100644 --- a/demos/shortform/src/main/res/layout/activity_main.xml +++ b/demos/shortform/src/main/res/layout/activity_main.xml @@ -44,35 +44,5 @@ android:hint="@string/num_of_players" android:inputType="numberDecimal" android:textColorHint="@color/grey" /> - - diff --git a/demos/shortform/src/main/res/values/strings.xml b/demos/shortform/src/main/res/values/strings.xml index 0c68ec91a4..2ef4b487a5 100644 --- a/demos/shortform/src/main/res/values/strings.xml +++ b/demos/shortform/src/main/res/values/strings.xml @@ -18,6 +18,4 @@ Add view pager, please! ViewPager activity How Many Players? - How Many Previous Videos Cached - How Many Future Videos Cached