Use DefaultPreloadManager in shortform demo app

The `MediaSourceManager` is removed and its functionalities are replaced by `DefaultPreloadManager`.

PiperOrigin-RevId: 621884502
This commit is contained in:
tianyifeng 2024-04-04 09:33:20 -07:00 committed by Copybara-Service
parent 617f9898c3
commit 8867642681
10 changed files with 174 additions and 358 deletions

View File

@ -165,6 +165,7 @@
`TestPlayerRunHelper.run(player).ignoringNonFatalErrors().untilXXX()` `TestPlayerRunHelper.run(player).ignoringNonFatalErrors().untilXXX()`
method chain to disable this behavior. method chain to disable this behavior.
* Demo app: * Demo app:
* Use `DefaultPreloadManager` in the shortform demo app.
* Remove deprecated symbols: * Remove deprecated symbols:
* Remove `CronetDataSourceFactory`. Use `CronetDataSource.Factory` * Remove `CronetDataSourceFactory`. Use `CronetDataSource.Factory`
instead. instead.

View File

@ -51,53 +51,14 @@ class MainActivity : AppCompatActivity() {
} }
) )
var mediaItemsBackwardCacheSize = 2
val mediaItemsBCacheSizeView = findViewById<EditText>(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<EditText>(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<View>(R.id.view_pager_button).setOnClickListener { findViewById<View>(R.id.view_pager_button).setOnClickListener {
startActivity( startActivity(
Intent(this, ViewPagerActivity::class.java) Intent(this, ViewPagerActivity::class.java).putExtra(NUM_PLAYERS_EXTRA, numberOfPlayers)
.putExtra(NUM_PLAYERS_EXTRA, numberOfPlayers)
.putExtra(MEDIA_ITEMS_BACKWARD_CACHE_SIZE, mediaItemsBackwardCacheSize)
.putExtra(MEDIA_ITEMS_FORWARD_CACHE_SIZE, mediaItemsForwardCacheSize)
) )
} }
} }
companion object { 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" const val NUM_PLAYERS_EXTRA = "number_of_players"
} }
} }

View File

@ -16,51 +16,22 @@
package androidx.media3.demo.shortform package androidx.media3.demo.shortform
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.util.Log
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
@UnstableApi @UnstableApi
class MediaItemDatabase() { class MediaItemDatabase {
var lCacheSize: Int = 2 private val mediaUris =
var rCacheSize: Int = 7
private val mediaItems =
mutableListOf( mutableListOf(
MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_1.mp4"), "https://storage.googleapis.com/exoplayer-test-media-0/shortform_1.mp4",
MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_2.mp4"), "https://storage.googleapis.com/exoplayer-test-media-0/shortform_2.mp4",
MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_3.mp4"), "https://storage.googleapis.com/exoplayer-test-media-0/shortform_3.mp4",
MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_4.mp4"), "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_6.mp4",
) )
// Effective sliding window of size = lCacheSize + 1 + rCacheSize
private val slidingWindowCache = HashMap<Int, MediaItem>()
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 { fun get(index: Int): MediaItem {
return getCached(index) val uri = mediaUris.get(index.mod(mediaUris.size))
} return return MediaItem.Builder().setUri(uri).setMediaId(index.toString()).build()
fun get(fromIndex: Int, toIndex: Int): List<MediaItem> {
val result: MutableList<MediaItem> = mutableListOf()
for (i in fromIndex..toIndex) {
result.add(get(i))
}
return result
} }
} }

View File

@ -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<MediaItem, PreloadMediaSource> = 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<MediaItem>) {
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<RendererCapabilities> {
val renderers =
renderersFactory.createRenderers(
Util.createHandlerForCurrentOrMainLooper(),
object : VideoRendererEventListener {},
object : AudioRendererEventListener {},
{ _: CueGroup? -> },
) { _: Metadata ->
}
val capabilities = ArrayList<RendererCapabilities>()
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.
}
}
}

View File

@ -39,7 +39,7 @@ class PlayerPool(
playbackLooper: Looper, playbackLooper: Looper,
loadControl: LoadControl, loadControl: LoadControl,
renderersFactory: RenderersFactory, renderersFactory: RenderersFactory,
bandwidthMeter: BandwidthMeter bandwidthMeter: BandwidthMeter,
) { ) {
/** Creates a player instance to be used by the pool. */ /** Creates a player instance to be used by the pool. */
@ -56,12 +56,6 @@ class PlayerPool(
fun acquirePlayer(token: Int, callback: (ExoPlayer) -> Unit) { fun acquirePlayer(token: Int, callback: (ExoPlayer) -> Unit) {
synchronized(playerMap) { 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 // Add token to set of views requesting players
playerRequestTokenSet.add(token) playerRequestTokenSet.add(token)
acquirePlayerInternal(token, callback) acquirePlayerInternal(token, callback)
@ -75,6 +69,12 @@ class PlayerPool(
playerMap[playerNumber]?.let { callback.invoke(it) } playerMap[playerNumber]?.let { callback.invoke(it) }
playerRequestTokenSet.remove(token) playerRequestTokenSet.remove(token)
return 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)) { } else if (playerRequestTokenSet.contains(token)) {
Handler(Looper.getMainLooper()).postDelayed({ acquirePlayerInternal(token, callback) }, 500) Handler(Looper.getMainLooper()).postDelayed({ acquirePlayerInternal(token, callback) }, 500)
} }
@ -130,7 +130,7 @@ class PlayerPool(
private val playbackLooper: Looper, private val playbackLooper: Looper,
private val loadControl: LoadControl, private val loadControl: LoadControl,
private val renderersFactory: RenderersFactory, private val renderersFactory: RenderersFactory,
private val bandwidthMeter: BandwidthMeter private val bandwidthMeter: BandwidthMeter,
) : PlayerFactory { ) : PlayerFactory {
private var playerCounter = 0 private var playerCounter = 0

View File

@ -31,23 +31,21 @@ class ViewPagerActivity : AppCompatActivity() {
private var numberOfPlayers = 3 private var numberOfPlayers = 3
private var mediaItemDatabase = MediaItemDatabase() private var mediaItemDatabase = MediaItemDatabase()
companion object {
private const val TAG = "ViewPagerActivity"
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_pager) setContentView(R.layout.activity_view_pager)
numberOfPlayers = intent.getIntExtra(MainActivity.NUM_PLAYERS_EXTRA, numberOfPlayers) numberOfPlayers = intent.getIntExtra(MainActivity.NUM_PLAYERS_EXTRA, numberOfPlayers)
mediaItemDatabase.lCacheSize = Log.d(TAG, "Using a pool of $numberOfPlayers players")
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}")
viewPagerView = findViewById(R.id.viewPager) viewPagerView = findViewById(R.id.viewPager)
viewPagerView.offscreenPageLimit = 1 viewPagerView.offscreenPageLimit = 1
viewPagerView.registerOnPageChangeCallback( viewPagerView.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
adapter.play(position) adapter.onPageSelected(position)
} }
} }
) )

View File

@ -18,40 +18,62 @@ package androidx.media3.demo.shortform.viewpager
import android.content.Context import android.content.Context
import android.os.HandlerThread import android.os.HandlerThread
import android.os.Process import android.os.Process
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup 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.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.demo.shortform.MediaItemDatabase import androidx.media3.demo.shortform.MediaItemDatabase
import androidx.media3.demo.shortform.MediaSourceManager
import androidx.media3.demo.shortform.PlayerPool import androidx.media3.demo.shortform.PlayerPool
import androidx.media3.demo.shortform.R import androidx.media3.demo.shortform.R
import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.DefaultRendererCapabilitiesList
import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory 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.trackselection.DefaultTrackSelector
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
import androidx.media3.exoplayer.util.EventLogger
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
@UnstableApi @UnstableApi
class ViewPagerMediaAdapter( class ViewPagerMediaAdapter(
private val mediaItemDatabase: MediaItemDatabase, private val mediaItemDatabase: MediaItemDatabase,
numberOfPlayers: Int, numberOfPlayers: Int,
private val context: Context context: Context,
) : RecyclerView.Adapter<ViewPagerMediaHolder>() { ) : RecyclerView.Adapter<ViewPagerMediaHolder>() {
private val playbackThread: HandlerThread = private val playbackThread: HandlerThread =
HandlerThread("playback-thread", Process.THREAD_PRIORITY_AUDIO) HandlerThread("playback-thread", Process.THREAD_PRIORITY_AUDIO)
private val mediaSourceManager: MediaSourceManager private val preloadManager: DefaultPreloadManager
private var viewCounter = 0 private val currentMediaItemsAndIndexes: ArrayDeque<Pair<MediaItem, Int>> = ArrayDeque()
private var playerPool: PlayerPool private var playerPool: PlayerPool
private val holderMap: MutableMap<Int, ViewPagerMediaHolder> private val holderMap: MutableMap<Int, ViewPagerMediaHolder>
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 { init {
playbackThread.start() 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) val renderersFactory = DefaultRenderersFactory(context)
playerPool = playerPool =
PlayerPool( PlayerPool(
@ -60,53 +82,72 @@ class ViewPagerMediaAdapter(
playbackThread.looper, playbackThread.looper,
loadControl, loadControl,
renderersFactory, renderersFactory,
DefaultBandwidthMeter.getSingletonInstance(context) DefaultBandwidthMeter.getSingletonInstance(context),
) )
holderMap = mutableMapOf() holderMap = mutableMapOf()
mediaSourceManager = val trackSelector = DefaultTrackSelector(context)
MediaSourceManager( trackSelector.init({}, DefaultBandwidthMeter.getSingletonInstance(context))
DefaultMediaSourceFactory(DefaultDataSource.Factory(context)), preloadManager =
playbackThread.looper, DefaultPreloadManager(
DefaultPreloadControl(),
DefaultMediaSourceFactory(context),
trackSelector,
DefaultBandwidthMeter.getSingletonInstance(context),
DefaultRendererCapabilitiesList.Factory(renderersFactory),
loadControl.allocator, loadControl.allocator,
renderersFactory, playbackThread.looper,
DefaultTrackSelector(context),
DefaultBandwidthMeter.getSingletonInstance(context)
) )
for (i in 0 until MANAGED_ITEM_COUNT) {
addMediaItem(index = i, isAddingToRightEnd = true)
}
preloadManager.invalidate()
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewPagerMediaHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewPagerMediaHolder {
Log.d("viewpager", "onCreateViewHolder: $viewCounter")
val view = val view =
LayoutInflater.from(parent.context).inflate(R.layout.media_item_view_pager, parent, false) 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) view.addOnAttachStateChangeListener(holder)
return holder return holder
} }
override fun onBindViewHolder(holder: ViewPagerMediaHolder, position: Int) { 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) val mediaItem = mediaItemDatabase.get(position)
Log.d("viewpager", "onBindViewHolder: Getting item at position $position") Log.d(TAG, "onBindViewHolder: Getting item at position $position")
holder.bindData(position, mediaSourceManager[mediaItem]) var currentMediaSource = preloadManager.getMediaSource(mediaItem)
// We are moving to <position>, so should prepare the next couple of items if (currentMediaSource == null) {
// Potentially most of those are already cached on the database side because of the sliding preloadManager.add(mediaItem, position)
// window and we would only require one more item at index=mediaItemHorizon currentMediaSource = preloadManager.getMediaSource(mediaItem)!!
val mediaItemHorizon = position + mediaItemDatabase.rCacheSize }
val reachableMediaItems = holder.bindData(currentMediaSource)
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)
} }
override fun onViewAttachedToWindow(holder: ViewPagerMediaHolder) { 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) { override fun onViewDetachedFromWindow(holder: ViewPagerMediaHolder) {
holderMap.remove(holder.currentToken) holderMap.remove(holder.bindingAdapterPosition)
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
@ -114,38 +155,55 @@ class ViewPagerMediaAdapter(
return Int.MAX_VALUE return Int.MAX_VALUE
} }
override fun onViewRecycled(holder: ViewPagerMediaHolder) {
super.onViewRecycled(holder)
}
fun onDestroy() { fun onDestroy() {
playbackThread.quit() preloadManager.release()
playerPool.destroyPlayers() playerPool.destroyPlayers()
mediaSourceManager.release() playbackThread.quit()
} }
fun play(position: Int) { fun onPageSelected(position: Int) {
holderMap[position]?.let { holder -> holder.player?.let { playerPool.play(it) } } currentPlayingIndex = position
holderMap[position]?.playIfPossible()
preloadManager.setCurrentPlayingIndex(position)
preloadManager.invalidate()
} }
inner class Factory : PlayerPool.PlayerFactory { private fun addMediaItem(index: Int, isAddingToRightEnd: Boolean) {
private var playerCounter = 0 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 { private fun removeMediaItem(isRemovingFromRightEnd: Boolean) {
val loadControl = if (currentMediaItemsAndIndexes.size <= MANAGED_ITEM_COUNT) {
DefaultLoadControl.Builder() return
.setBufferDurationsMs( }
/* minBufferMs= */ 15_000, val itemAndIndex =
/* maxBufferMs= */ 15_000, if (isRemovingFromRightEnd) {
/* bufferForPlaybackMs= */ 500, currentMediaItemsAndIndexes.removeLast()
/* bufferForPlaybackAfterRebufferMs= */ 1_000 } else {
) currentMediaItemsAndIndexes.removeFirst()
.build() }
val player = ExoPlayer.Builder(context).setLoadControl(loadControl).build() Log.d(TAG, "removeMediaItem: Removing item at index ${itemAndIndex.second}")
player.addAnalyticsListener(EventLogger("player-$playerCounter")) preloadManager.remove(itemAndIndex.first)
playerCounter++ }
player.repeatMode = ExoPlayer.REPEAT_MODE_ONE
return player inner class DefaultPreloadControl : TargetPreloadStatusControl<Int> {
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
} }
} }
} }

View File

@ -15,30 +15,30 @@
*/ */
package androidx.media3.demo.shortform.viewpager package androidx.media3.demo.shortform.viewpager
import android.util.Log
import android.view.View import android.view.View
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.media3.common.Player import androidx.media3.common.util.Log
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.demo.shortform.PlayerPool import androidx.media3.demo.shortform.PlayerPool
import androidx.media3.demo.shortform.R import androidx.media3.demo.shortform.R
import androidx.media3.exoplayer.ExoPlayer 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.media3.ui.PlayerView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@OptIn(UnstableApi::class) // Using PreloadMediaSource. @OptIn(UnstableApi::class)
class ViewPagerMediaHolder( class ViewPagerMediaHolder(itemView: View, private val playerPool: PlayerPool) :
itemView: View, RecyclerView.ViewHolder(itemView), View.OnAttachStateChangeListener {
private val viewCounter: Int,
private val playerPool: PlayerPool
) : RecyclerView.ViewHolder(itemView), View.OnAttachStateChangeListener {
private val playerView: PlayerView = itemView.findViewById(R.id.player_view) private val playerView: PlayerView = itemView.findViewById(R.id.player_view)
private var exoPlayer: ExoPlayer? = null private var exoPlayer: ExoPlayer? = null
private var isInView: Boolean = false 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 { init {
// Define click listener for the ViewHolder's View // Define click listener for the ViewHolder's View
@ -49,46 +49,44 @@ class ViewPagerMediaHolder(
} }
} }
val currentToken: Int val player: ExoPlayer?
get() {
return token
}
val player: Player?
get() { get() {
return exoPlayer return exoPlayer
} }
override fun onViewAttachedToWindow(view: View) { override fun onViewAttachedToWindow(view: View) {
Log.d("viewpager", "onViewAttachedToWindow: $viewCounter") Log.d(TAG, "onViewAttachedToWindow: $bindingAdapterPosition")
isInView = true isInView = true
if (player == null) { if (player == null) {
playerPool.acquirePlayer(token, ::setupPlayer) playerPool.acquirePlayer(bindingAdapterPosition, ::setupPlayer)
} }
} }
override fun onViewDetachedFromWindow(view: View) { override fun onViewDetachedFromWindow(view: View) {
Log.d("viewpager", "onViewDetachedFromWindow: $viewCounter") Log.d(TAG, "onViewDetachedFromWindow: $bindingAdapterPosition")
isInView = false isInView = false
releasePlayer(exoPlayer) 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.mediaSource = mediaSource
this.token = token
} }
fun releasePlayer(player: ExoPlayer?) { fun playIfPossible() {
playerPool.releasePlayer(token, player ?: exoPlayer) 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 this.exoPlayer = null
playerView.player = null playerView.player = null
} }
fun setupPlayer(player: ExoPlayer) { private fun setupPlayer(player: ExoPlayer) {
if (!isInView) { if (!isInView) {
releasePlayer(player) releasePlayer(player)
} else { } else {
@ -103,6 +101,10 @@ class ViewPagerMediaHolder(
this@ViewPagerMediaHolder.exoPlayer = player this@ViewPagerMediaHolder.exoPlayer = player
player.prepare() player.prepare()
playerView.player = player playerView.player = player
if (pendingPlayRequestUponSetupPlayer) {
playerPool.play(player)
pendingPlayRequestUponSetupPlayer = false
}
} }
} }
} }

View File

@ -44,35 +44,5 @@
android:hint="@string/num_of_players" android:hint="@string/num_of_players"
android:inputType="numberDecimal" android:inputType="numberDecimal"
android:textColorHint="@color/grey" /> android:textColorHint="@color/grey" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/media_items_b_cache_size"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="20dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_gravity="center_horizontal"
android:background="@color/purple_700"
android:gravity="center"
android:hint="@string/how_many_previous_videos_cached"
android:inputType="numberDecimal"
android:textColorHint="@color/grey" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/media_items_f_cache_size"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="20dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_gravity="center_horizontal"
android:background="@color/purple_700"
android:gravity="center"
android:hint="@string/how_many_future_videos_cached"
android:inputType="numberDecimal"
android:textColorHint="@color/grey" />
</LinearLayout> </LinearLayout>

View File

@ -18,6 +18,4 @@
<string name="add_view_pager">Add view pager, please!</string> <string name="add_view_pager">Add view pager, please!</string>
<string name="title_activity_view_pager">ViewPager activity</string> <string name="title_activity_view_pager">ViewPager activity</string>
<string name="num_of_players">How Many Players?</string> <string name="num_of_players">How Many Players?</string>
<string name="how_many_previous_videos_cached">How Many Previous Videos Cached</string>
<string name="how_many_future_videos_cached">How Many Future Videos Cached</string>
</resources> </resources>