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:
parent
617f9898c3
commit
8867642681
@ -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.
|
||||
|
@ -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 {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
@ -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<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 {
|
||||
return getCached(index)
|
||||
}
|
||||
|
||||
fun get(fromIndex: Int, toIndex: Int): List<MediaItem> {
|
||||
val result: MutableList<MediaItem> = 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()
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -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<ViewPagerMediaHolder>() {
|
||||
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<Pair<MediaItem, Int>> = ArrayDeque()
|
||||
private var playerPool: PlayerPool
|
||||
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 {
|
||||
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 <position>, 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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,35 +44,5 @@
|
||||
android:hint="@string/num_of_players"
|
||||
android:inputType="numberDecimal"
|
||||
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>
|
||||
|
@ -18,6 +18,4 @@
|
||||
<string name="add_view_pager">Add view pager, please!</string>
|
||||
<string name="title_activity_view_pager">ViewPager activity</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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user