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()`
method chain to disable this behavior.
* Demo app:
* Use `DefaultPreloadManager` in the shortform demo app.
* Remove deprecated symbols:
* Remove `CronetDataSourceFactory`. Use `CronetDataSource.Factory`
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 {
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"
}
}

View File

@ -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()
}
}

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,
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

View File

@ -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)
}
}
)

View File

@ -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
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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>

View File

@ -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>