Optimize short form content demo app

PiperOrigin-RevId: 592225900
This commit is contained in:
bachinger 2023-12-19 07:05:56 -08:00 committed by Copybara-Service
parent 7ca26f898d
commit 8940900c69
5 changed files with 69 additions and 33 deletions

View file

@ -82,7 +82,7 @@ class MediaSourceManager(
} }
} }
operator fun get(mediaItem: MediaItem): MediaSource { operator fun get(mediaItem: MediaItem): PreloadMediaSource {
if (!sourceMap.containsKey(mediaItem)) { if (!sourceMap.containsKey(mediaItem)) {
add(mediaItem) add(mediaItem)
} }

View file

@ -19,6 +19,7 @@ import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.LoadControl import androidx.media3.exoplayer.LoadControl
@ -80,6 +81,25 @@ class PlayerPool(
} }
} }
/** Calls [Player.play()] for the given player and pauses all other players. */
fun play(player: Player) {
pauseAllPlayers(player)
player.play()
}
/**
* Pauses all players.
*
* @param keepOngoingPlayer The optional player that should keep playing if not paused.
*/
fun pauseAllPlayers(keepOngoingPlayer: Player? = null) {
playerMap.values.forEach {
if (it != keepOngoingPlayer) {
it.pause()
}
}
}
fun releasePlayer(token: Int, player: ExoPlayer?) { fun releasePlayer(token: Int, player: ExoPlayer?) {
synchronized(playerMap) { synchronized(playerMap) {
// Remove token from set of views requesting players & remove potential callbacks // Remove token from set of views requesting players & remove potential callbacks

View file

@ -43,20 +43,11 @@ class ViewPagerActivity : AppCompatActivity() {
Log.d("viewpager", "Backward cache is of size: ${mediaItemDatabase.lCacheSize}") Log.d("viewpager", "Backward cache is of size: ${mediaItemDatabase.lCacheSize}")
Log.d("viewpager", "Forward cache is of size: ${mediaItemDatabase.rCacheSize}") Log.d("viewpager", "Forward cache is of size: ${mediaItemDatabase.rCacheSize}")
viewPagerView = findViewById(R.id.viewPager) viewPagerView = findViewById(R.id.viewPager)
viewPagerView.offscreenPageLimit = 1
viewPagerView.registerOnPageChangeCallback( viewPagerView.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {}
override fun onPageScrollStateChanged(state: Int) {
Log.d("viewpager", "onPageScrollStateChanged: state=$state")
}
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
Log.d("viewpager", "onPageSelected: position=$position") adapter.play(position)
} }
} }
) )

View file

@ -30,7 +30,7 @@ import androidx.media3.demo.shortform.R
import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
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.media3.exoplayer.util.EventLogger
@ -47,6 +47,7 @@ class ViewPagerMediaAdapter(
private val mediaSourceManager: MediaSourceManager private val mediaSourceManager: MediaSourceManager
private var viewCounter = 0 private var viewCounter = 0
private var playerPool: PlayerPool private var playerPool: PlayerPool
private val holderMap: MutableMap<Int, ViewPagerMediaHolder>
init { init {
playbackThread.start() playbackThread.start()
@ -61,9 +62,10 @@ class ViewPagerMediaAdapter(
renderersFactory, renderersFactory,
DefaultBandwidthMeter.getSingletonInstance(context) DefaultBandwidthMeter.getSingletonInstance(context)
) )
holderMap = mutableMapOf()
mediaSourceManager = mediaSourceManager =
MediaSourceManager( MediaSourceManager(
ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context)), DefaultMediaSourceFactory(DefaultDataSource.Factory(context)),
playbackThread.looper, playbackThread.looper,
loadControl.allocator, loadControl.allocator,
renderersFactory, renderersFactory,
@ -99,6 +101,14 @@ class ViewPagerMediaAdapter(
mediaSourceManager.addAll(reachableMediaItems) mediaSourceManager.addAll(reachableMediaItems)
} }
override fun onViewAttachedToWindow(holder: ViewPagerMediaHolder) {
holderMap[holder.currentToken] = holder
}
override fun onViewDetachedFromWindow(holder: ViewPagerMediaHolder) {
holderMap.remove(holder.currentToken)
}
override fun getItemCount(): Int { override fun getItemCount(): Int {
// Effectively infinite scroll // Effectively infinite scroll
return Int.MAX_VALUE return Int.MAX_VALUE
@ -106,7 +116,6 @@ class ViewPagerMediaAdapter(
override fun onViewRecycled(holder: ViewPagerMediaHolder) { override fun onViewRecycled(holder: ViewPagerMediaHolder) {
super.onViewRecycled(holder) super.onViewRecycled(holder)
Log.d("viewpager", "Recycling the view")
} }
fun onDestroy() { fun onDestroy() {
@ -115,6 +124,10 @@ class ViewPagerMediaAdapter(
mediaSourceManager.release() mediaSourceManager.release()
} }
fun play(position: Int) {
holderMap[position]?.let { holder -> holder.player?.let { playerPool.play(it) } }
}
inner class Factory : PlayerPool.PlayerFactory { inner class Factory : PlayerPool.PlayerFactory {
private var playerCounter = 0 private var playerCounter = 0
@ -122,10 +135,10 @@ class ViewPagerMediaAdapter(
val loadControl = val loadControl =
DefaultLoadControl.Builder() DefaultLoadControl.Builder()
.setBufferDurationsMs( .setBufferDurationsMs(
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS, /* minBufferMs= */ 15_000,
DefaultLoadControl.DEFAULT_MAX_BUFFER_MS, /* maxBufferMs= */ 15_000,
/* bufferForPlaybackMs= */ 0, /* bufferForPlaybackMs= */ 500,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS /* bufferForPlaybackAfterRebufferMs= */ 1_000
) )
.build() .build()
val player = ExoPlayer.Builder(context).setLoadControl(loadControl).build() val player = ExoPlayer.Builder(context).setLoadControl(loadControl).build()

View file

@ -18,25 +18,27 @@ package androidx.media3.demo.shortform.viewpager
import android.util.Log 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.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.MediaSource import androidx.media3.exoplayer.source.preload.PreloadMediaSource
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.
class ViewPagerMediaHolder( class ViewPagerMediaHolder(
itemView: View, itemView: View,
private val viewCounter: Int, private val viewCounter: Int,
private val playerPool: PlayerPool private val playerPool: PlayerPool
) : RecyclerView.ViewHolder(itemView), View.OnAttachStateChangeListener { ) : 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 player: ExoPlayer? = null private var exoPlayer: ExoPlayer? = null
private var isInView: Boolean = false private var isInView: Boolean = false
private var token: Int = -1 private var token: Int = -1
private lateinit var mediaSource: MediaSource private lateinit var mediaSource: PreloadMediaSource
init { init {
// Define click listener for the ViewHolder's View // Define click listener for the ViewHolder's View
@ -47,7 +49,16 @@ class ViewPagerMediaHolder(
} }
} }
@OptIn(UnstableApi::class) val currentToken: Int
get() {
return token
}
val player: Player?
get() {
return exoPlayer
}
override fun onViewAttachedToWindow(view: View) { override fun onViewAttachedToWindow(view: View) {
Log.d("viewpager", "onViewAttachedToWindow: $viewCounter") Log.d("viewpager", "onViewAttachedToWindow: $viewCounter")
isInView = true isInView = true
@ -59,36 +70,37 @@ class ViewPagerMediaHolder(
override fun onViewDetachedFromWindow(view: View) { override fun onViewDetachedFromWindow(view: View) {
Log.d("viewpager", "onViewDetachedFromWindow: $viewCounter") Log.d("viewpager", "onViewDetachedFromWindow: $viewCounter")
isInView = false isInView = false
releasePlayer(player) 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: MediaSource) { fun bindData(token: Int, mediaSource: PreloadMediaSource) {
this.mediaSource = mediaSource this.mediaSource = mediaSource
this.token = token this.token = token
} }
@OptIn(UnstableApi::class)
fun releasePlayer(player: ExoPlayer?) { fun releasePlayer(player: ExoPlayer?) {
playerPool.releasePlayer(token, player ?: this.player) playerPool.releasePlayer(token, player ?: exoPlayer)
this.player = null this.exoPlayer = null
playerView.player = null playerView.player = null
} }
@OptIn(UnstableApi::class)
fun setupPlayer(player: ExoPlayer) { fun setupPlayer(player: ExoPlayer) {
if (!isInView) { if (!isInView) {
releasePlayer(player) releasePlayer(player)
} else { } else {
if (player != this.player) { if (player != exoPlayer) {
releasePlayer(this.player) releasePlayer(exoPlayer)
} }
player.run { player.run {
repeatMode = ExoPlayer.REPEAT_MODE_ONE repeatMode = ExoPlayer.REPEAT_MODE_ONE
setMediaSource(mediaSource) setMediaSource(mediaSource)
seekTo(currentPosition) seekTo(currentPosition)
playWhenReady = true this@ViewPagerMediaHolder.exoPlayer = player
this@ViewPagerMediaHolder.player = player
player.prepare() player.prepare()
playerView.player = player playerView.player = player
} }