diff --git a/demos/shortform/src/main/java/androidx/media3/demo/shortform/MediaSourceManager.kt b/demos/shortform/src/main/java/androidx/media3/demo/shortform/MediaSourceManager.kt index 44485ac363..d53d664c2f 100644 --- a/demos/shortform/src/main/java/androidx/media3/demo/shortform/MediaSourceManager.kt +++ b/demos/shortform/src/main/java/androidx/media3/demo/shortform/MediaSourceManager.kt @@ -82,7 +82,7 @@ class MediaSourceManager( } } - operator fun get(mediaItem: MediaItem): MediaSource { + operator fun get(mediaItem: MediaItem): PreloadMediaSource { if (!sourceMap.containsKey(mediaItem)) { add(mediaItem) } diff --git a/demos/shortform/src/main/java/androidx/media3/demo/shortform/PlayerPool.kt b/demos/shortform/src/main/java/androidx/media3/demo/shortform/PlayerPool.kt index 8e2b35e4c5..6340835b71 100644 --- a/demos/shortform/src/main/java/androidx/media3/demo/shortform/PlayerPool.kt +++ b/demos/shortform/src/main/java/androidx/media3/demo/shortform/PlayerPool.kt @@ -19,6 +19,7 @@ import android.content.Context import android.os.Handler import android.os.Looper import androidx.annotation.OptIn +import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer 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?) { synchronized(playerMap) { // Remove token from set of views requesting players & remove potential callbacks diff --git a/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerActivity.kt b/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerActivity.kt index fd0c430191..6ddca36e7f 100644 --- a/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerActivity.kt +++ b/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerActivity.kt @@ -43,20 +43,11 @@ class ViewPagerActivity : AppCompatActivity() { 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.offscreenPageLimit = 1 viewPagerView.registerOnPageChangeCallback( 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) { - Log.d("viewpager", "onPageSelected: position=$position") + adapter.play(position) } } ) diff --git a/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaAdapter.kt b/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaAdapter.kt index d01389d5f8..434d729233 100644 --- a/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaAdapter.kt +++ b/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaAdapter.kt @@ -30,7 +30,7 @@ import androidx.media3.demo.shortform.R import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultRenderersFactory 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.upstream.DefaultBandwidthMeter import androidx.media3.exoplayer.util.EventLogger @@ -47,6 +47,7 @@ class ViewPagerMediaAdapter( private val mediaSourceManager: MediaSourceManager private var viewCounter = 0 private var playerPool: PlayerPool + private val holderMap: MutableMap init { playbackThread.start() @@ -61,9 +62,10 @@ class ViewPagerMediaAdapter( renderersFactory, DefaultBandwidthMeter.getSingletonInstance(context) ) + holderMap = mutableMapOf() mediaSourceManager = MediaSourceManager( - ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context)), + DefaultMediaSourceFactory(DefaultDataSource.Factory(context)), playbackThread.looper, loadControl.allocator, renderersFactory, @@ -99,6 +101,14 @@ class ViewPagerMediaAdapter( 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 { // Effectively infinite scroll return Int.MAX_VALUE @@ -106,7 +116,6 @@ class ViewPagerMediaAdapter( override fun onViewRecycled(holder: ViewPagerMediaHolder) { super.onViewRecycled(holder) - Log.d("viewpager", "Recycling the view") } fun onDestroy() { @@ -115,6 +124,10 @@ class ViewPagerMediaAdapter( mediaSourceManager.release() } + fun play(position: Int) { + holderMap[position]?.let { holder -> holder.player?.let { playerPool.play(it) } } + } + inner class Factory : PlayerPool.PlayerFactory { private var playerCounter = 0 @@ -122,10 +135,10 @@ class ViewPagerMediaAdapter( val loadControl = DefaultLoadControl.Builder() .setBufferDurationsMs( - DefaultLoadControl.DEFAULT_MIN_BUFFER_MS, - DefaultLoadControl.DEFAULT_MAX_BUFFER_MS, - /* bufferForPlaybackMs= */ 0, - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS + /* minBufferMs= */ 15_000, + /* maxBufferMs= */ 15_000, + /* bufferForPlaybackMs= */ 500, + /* bufferForPlaybackAfterRebufferMs= */ 1_000 ) .build() val player = ExoPlayer.Builder(context).setLoadControl(loadControl).build() diff --git a/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaHolder.kt b/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaHolder.kt index 307d6131ba..98c54d5d65 100644 --- a/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaHolder.kt +++ b/demos/shortform/src/main/java/androidx/media3/demo/shortform/viewpager/ViewPagerMediaHolder.kt @@ -18,25 +18,27 @@ 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.UnstableApi import androidx.media3.demo.shortform.PlayerPool import androidx.media3.demo.shortform.R 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.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 { 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 token: Int = -1 - private lateinit var mediaSource: MediaSource + private lateinit var mediaSource: PreloadMediaSource init { // 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) { Log.d("viewpager", "onViewAttachedToWindow: $viewCounter") isInView = true @@ -59,36 +70,37 @@ class ViewPagerMediaHolder( override fun onViewDetachedFromWindow(view: View) { Log.d("viewpager", "onViewDetachedFromWindow: $viewCounter") 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.token = token } - @OptIn(UnstableApi::class) fun releasePlayer(player: ExoPlayer?) { - playerPool.releasePlayer(token, player ?: this.player) - this.player = null + playerPool.releasePlayer(token, player ?: exoPlayer) + this.exoPlayer = null playerView.player = null } - @OptIn(UnstableApi::class) fun setupPlayer(player: ExoPlayer) { if (!isInView) { releasePlayer(player) } else { - if (player != this.player) { - releasePlayer(this.player) + if (player != exoPlayer) { + releasePlayer(exoPlayer) } player.run { repeatMode = ExoPlayer.REPEAT_MODE_ONE setMediaSource(mediaSource) seekTo(currentPosition) - playWhenReady = true - this@ViewPagerMediaHolder.player = player + this@ViewPagerMediaHolder.exoPlayer = player player.prepare() playerView.player = player }