From a4e24f96a8ce53293a8d01ff16c0490577756c4a Mon Sep 17 00:00:00 2001 From: NickHuk Date: Thu, 24 Nov 2022 15:54:36 +0200 Subject: [PATCH 1/2] caching + logging --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 2 + .../java/com/uptech/videolist/MainActivity.kt | 49 +++++++++-- .../java/com/uptech/videolist/PlayersPool.kt | 46 +++++++---- .../com/uptech/videolist/ReusablePlayer.kt | 82 +++++++++++++++++++ .../java/com/uptech/videolist/VideoAdapter.kt | 43 +++------- app/src/main/res/layout/activity_main.xml | 1 + app/src/main/res/layout/video_item_view.xml | 5 +- 8 files changed, 177 insertions(+), 53 deletions(-) create mode 100644 app/src/main/java/com/uptech/videolist/ReusablePlayer.kt diff --git a/app/build.gradle b/app/build.gradle index 861a5ef..e341799 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -47,6 +47,8 @@ dependencies { implementation "androidx.media3:media3-exoplayer:$mediaVersion" implementation "androidx.media3:media3-ui:$mediaVersion" implementation "androidx.media3:media3-exoplayer-dash:$mediaVersion" + implementation "androidx.media3:media3-datasource:$mediaVersion" + implementation "androidx.media3:media3-exoplayer-hls:$mediaVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 68deebc..b69ccc2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ package="com.uptech.videolist"> + + adapter.playbackPositions = playbackPositions } .launchIn(this) viewModel.videoUrls - .onEach(adapter::updateVideoUrls) - .launchIn(this) + .onEach { videoUrls -> + ProgressiveMediaSource.Factory(cacheDataSourceFactory).run { + videoUrls.map { url -> createMediaSource(MediaItem.fromUri(url)) } + }.let { mediaSources -> adapter.updateVideoUrls(mediaSources) } + }.launchIn(this) } } @@ -78,4 +113,8 @@ class MainActivity : AppCompatActivity() { viewModel.releasePlayers() super.onStop() } + + companion object { + const val VIDEO_CACHE_DIR = "videoCache" + } } \ No newline at end of file diff --git a/app/src/main/java/com/uptech/videolist/PlayersPool.kt b/app/src/main/java/com/uptech/videolist/PlayersPool.kt index 03e7d08..a0c39e8 100644 --- a/app/src/main/java/com/uptech/videolist/PlayersPool.kt +++ b/app/src/main/java/com/uptech/videolist/PlayersPool.kt @@ -2,6 +2,7 @@ package com.uptech.videolist import android.content.Context import androidx.media3.common.Player +import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.ExoPlayer import kotlinx.coroutines.channels.Channel import timber.log.Timber @@ -12,23 +13,39 @@ class PlayersPool( private val context: Context, private val maxPoolSize: Int ) { - private val unlockedPlayers: MutableList = mutableListOf(ExoPlayer.Builder(context).build()) - private val lockedPlayers: MutableList = mutableListOf() + private val unlockedPlayers: MutableList = mutableListOf() + private val lockedPlayers: MutableList = mutableListOf() + private var playerIndex: Int = 0 - private val waitingQueue: Queue> = LinkedList() + private val waitingQueue: Queue> = LinkedList() @Synchronized - fun acquire(): Channel = + fun acquire(): Channel = if(unlockedPlayers.isEmpty()) { if(lockedPlayers.size >= maxPoolSize) { - Channel(capacity = 1).also { channel -> waitingQueue.offer(channel) } + Channel(capacity = 1).also { channel -> waitingQueue.offer(channel) } } else { - Channel(capacity = 1).apply { - trySend(ExoPlayer.Builder(context).build().also(lockedPlayers::add)) + Channel(capacity = 1).apply { + trySend( + ExoPlayer.Builder(context) + .setLoadControl( + DefaultLoadControl.Builder() + .setBufferDurationsMs( + 10_000, + 10_000, + 100, + 2000 + ).build() + ) + .build() + .let { exoPlayer -> ReusablePlayer(exoPlayer) } + .apply { playerId = "player${playerIndex++}" } + .also { player -> lockedPlayers.add(player) } + ) } } } else { - Channel(capacity = 1).apply { + Channel(capacity = 1).apply { trySend(unlockedPlayers.removeLast().also(lockedPlayers::add)) } }.also { @@ -36,24 +53,25 @@ class PlayersPool( } @Synchronized - fun removeFromAwaitingQueue(channel: Channel) { + fun removeFromAwaitingQueue(channel: Channel) { waitingQueue.remove(channel) } @Synchronized fun release(player: Player) { - lockedPlayers.remove(player) + lockedPlayers.removeAll { reusablePlayer -> reusablePlayer.player == player } } @Synchronized fun stop(player: Player) { - if(!reusePlayer(player)) { - lockedPlayers.remove(player) - unlockedPlayers.add(player) + val reusablePlayer = lockedPlayers.first { reusablePlayer -> reusablePlayer.player == player } + if (!reusePlayer(reusablePlayer)) { + lockedPlayers.remove(reusablePlayer) + unlockedPlayers.add(reusablePlayer) } } - private fun reusePlayer(player: Player): Boolean = + private fun reusePlayer(player: ReusablePlayer): Boolean = waitingQueue.poll()?.run { trySend(player) true diff --git a/app/src/main/java/com/uptech/videolist/ReusablePlayer.kt b/app/src/main/java/com/uptech/videolist/ReusablePlayer.kt new file mode 100644 index 0000000..69d7003 --- /dev/null +++ b/app/src/main/java/com/uptech/videolist/ReusablePlayer.kt @@ -0,0 +1,82 @@ +package com.uptech.videolist + +import androidx.media3.common.Player +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.MediaSource +import timber.log.Timber + +class ReusablePlayer( + val player: ExoPlayer +) { + var playerId: String = "" + private var playbackHistory: MutableList = mutableListOf() + private var prepareTime: Long = 0L + + init { + player.addListener( + object : Player.Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + when (playbackState) { + ExoPlayer.STATE_IDLE -> { + Timber.tag(VIDEO_TAG).d( + "Idle player number: %s", + playerId + ) + } + ExoPlayer.STATE_BUFFERING -> { + Timber.tag(VIDEO_TAG).d( + "Buffering player number: %s", + playerId + ) + } + ExoPlayer.STATE_READY -> { + Timber.tag(VIDEO_TAG).d( + "Ready player number: %s, preparation %d ms", + playerId, + System.currentTimeMillis() - prepareTime + ) + } + ExoPlayer.STATE_ENDED -> + Timber.tag(VIDEO_TAG).d( + "Ended player number: %s", + playerId + ) + else -> {} + } + } + } + ) + } + + fun setMediaSource(mediaSource: MediaSource) { + val mediaId: String = mediaSource.mediaItem.playbackProperties?.uri.toString() + .substringAfterLast('/') + if (playbackHistory.size > 0) { + if (mediaId != playbackHistory.last()) { + Timber.tag(VIDEO_TAG).d( + "Player %s rebind from %s to %s media source", + playerId, + playbackHistory.last(), + mediaId + ) + } + } else { + Timber.tag(VIDEO_TAG).d( + "Player %s first bind to %s media source", + playerId, + mediaId + ) + } + playbackHistory += mediaId + player.setMediaSource(mediaSource) + } + + fun prepare() { + prepareTime = System.currentTimeMillis() + player.prepare() + } + + companion object { + const val VIDEO_TAG = "videoTag" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/uptech/videolist/VideoAdapter.kt b/app/src/main/java/com/uptech/videolist/VideoAdapter.kt index 177d5f8..68b1578 100644 --- a/app/src/main/java/com/uptech/videolist/VideoAdapter.kt +++ b/app/src/main/java/com/uptech/videolist/VideoAdapter.kt @@ -2,8 +2,7 @@ package com.uptech.videolist import android.view.LayoutInflater import android.view.ViewGroup -import androidx.media3.common.MediaItem -import androidx.media3.common.Player +import androidx.media3.exoplayer.source.MediaSource import androidx.recyclerview.widget.RecyclerView import com.uptech.videolist.MainViewModel.PlayersAction import com.uptech.videolist.MainViewModel.PlayersAction.RELEASE @@ -20,7 +19,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import timber.log.Timber class VideoAdapter( private val playersPool: PlayersPool, @@ -29,10 +27,10 @@ class VideoAdapter( private val dispatcher: CoroutineDispatcher, private val updatePlaybackPosition: (Int, Long) -> Unit ) : RecyclerView.Adapter() { - private var videoUrls: List = listOf() + private var videoUrls: List = listOf() var playbackPositions: List = listOf() - fun updateVideoUrls(videoUrls: List) { + fun updateVideoUrls(videoUrls: List) { this.videoUrls = videoUrls notifyDataSetChanged() } @@ -62,22 +60,18 @@ class VideoAdapter( private val binding: VideoItemViewBinding ) : RecyclerView.ViewHolder(binding.root) { private lateinit var videoScope: CoroutineScope - private lateinit var playerChannel: Channel + private lateinit var playerChannel: Channel private var playJob: Job? = null private var restartJob: Job? = null - fun bind(url: String, playbackPosition: Long) { + fun bind(mediaSource: MediaSource, playbackPosition: Long) { videoScope = CoroutineScope(Job() + dispatcher) - bindPlayer(url, playbackPosition) + bindPlayer(mediaSource, playbackPosition) if(restartJob === null) { restartJob = playersActions .onEach { action -> when(action) { RELEASE -> with(binding.playerView) { - Timber.tag(VIDEO_LIST).d( - "Release player: url = %s", - url.substringAfterLast('/') - ) updatePlaybackPosition(absoluteAdapterPosition, player?.currentPosition ?: 0) player?.run { release() @@ -106,27 +100,17 @@ class VideoAdapter( } } - private fun bindPlayer(url: String, playbackPosition: Long) { + private fun bindPlayer(mediaSource: MediaSource, playbackPosition: Long) { playJob?.cancel() playJob = videoScope.launch { - Timber.tag(VIDEO_LIST).d( - "Awaiting for player url = %s, playbackPosition = %d", - url.substringAfterLast('/'), - playbackPosition - ) playersPool.acquire() .also { playerChannel = it } .receive() .run { - Timber.tag(VIDEO_LIST).d( - "Playing url = %s, playbackPosition = %d", - url.substringAfterLast('/'), - playbackPosition - ) - binding.playerView.player = this - setMediaItem(MediaItem.fromUri(url)) - playWhenReady = true - seekTo(0, playbackPosition) + binding.playerView.player = player + setMediaSource(mediaSource) + player.playWhenReady = true + player.seekTo(0, playbackPosition) prepare() } } @@ -141,11 +125,6 @@ class VideoAdapter( updatePlaybackPosition(absoluteAdapterPosition, currentPosition) playersPool.stop(this) } - Timber.tag(VIDEO_LIST).d( - "Player detached: url = %s, playback position = %d", - videoUrls[absoluteAdapterPosition].substringAfterLast('/'), - player?.currentPosition - ) videoScope.cancel() player = null } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index df56b14..3c84c31 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -6,6 +6,7 @@ android:layout_height="match_parent" tools:context=".MainActivity" > + - From bec8e0728cbd17a405792327c42293e185d5b78e Mon Sep 17 00:00:00 2001 From: NickHuk Date: Thu, 24 Nov 2022 16:18:00 +0200 Subject: [PATCH 2/2] bind player to URL functionality --- .../java/com/uptech/videolist/MainActivity.kt | 4 +- .../com/uptech/videolist/NoOpPlayersPool.kt | 46 ++++++++++ .../java/com/uptech/videolist/PlayersPool.kt | 85 ++---------------- .../uptech/videolist/ReusablePlayersPool.kt | 86 +++++++++++++++++++ .../java/com/uptech/videolist/VideoAdapter.kt | 2 +- 5 files changed, 141 insertions(+), 82 deletions(-) create mode 100644 app/src/main/java/com/uptech/videolist/NoOpPlayersPool.kt create mode 100644 app/src/main/java/com/uptech/videolist/ReusablePlayersPool.kt diff --git a/app/src/main/java/com/uptech/videolist/MainActivity.kt b/app/src/main/java/com/uptech/videolist/MainActivity.kt index 4627bab..1506fb5 100644 --- a/app/src/main/java/com/uptech/videolist/MainActivity.kt +++ b/app/src/main/java/com/uptech/videolist/MainActivity.kt @@ -31,11 +31,11 @@ class MainActivity : AppCompatActivity() { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T = MainViewModel( - PlayersPool( + NoOpPlayersPool( applicationContext, //use predefined number of codecs if there are not enough hardware codecs available on //device. P.S. as tests show app doesn't use more than 4 codecs instances - 4//minOf(4, availableCodecsNum()) + //4minOf(4, availableCodecsNum()) ) ) as T } diff --git a/app/src/main/java/com/uptech/videolist/NoOpPlayersPool.kt b/app/src/main/java/com/uptech/videolist/NoOpPlayersPool.kt new file mode 100644 index 0000000..0cf6ea0 --- /dev/null +++ b/app/src/main/java/com/uptech/videolist/NoOpPlayersPool.kt @@ -0,0 +1,46 @@ +package com.uptech.videolist + +import android.content.Context +import androidx.media3.common.Player +import androidx.media3.exoplayer.DefaultLoadControl +import androidx.media3.exoplayer.ExoPlayer +import kotlinx.coroutines.channels.Channel + +class NoOpPlayersPool( + private val context: Context +) : PlayersPool { + private val playerMap: MutableMap = mutableMapOf() + private var playerIndex: Int = 0 + + override fun acquire(url: String): Channel = + playerMap[url]?.let { reusablePlayer -> + Channel(capacity = 1).apply { + trySend(reusablePlayer) + } + } ?: Channel(capacity = 1).apply { + trySend( + ExoPlayer.Builder(context) + .setLoadControl( + DefaultLoadControl.Builder() + .setBufferDurationsMs( + 10_000, + 10_000, + 100, + 2000 + ).build() + ) + .build() + .let { exoPlayer -> ReusablePlayer(exoPlayer) } + .apply { playerId = "player${playerIndex++}" } + .also { reusablePlayer -> playerMap[url] = reusablePlayer } + ) + } + + override fun removeFromAwaitingQueue(channel: Channel) {} + + override fun release(player: Player) {} + + override fun stop(player: Player) {} + + override fun releaseAll() {} +} \ No newline at end of file diff --git a/app/src/main/java/com/uptech/videolist/PlayersPool.kt b/app/src/main/java/com/uptech/videolist/PlayersPool.kt index a0c39e8..17623b9 100644 --- a/app/src/main/java/com/uptech/videolist/PlayersPool.kt +++ b/app/src/main/java/com/uptech/videolist/PlayersPool.kt @@ -1,86 +1,13 @@ package com.uptech.videolist -import android.content.Context import androidx.media3.common.Player -import androidx.media3.exoplayer.DefaultLoadControl -import androidx.media3.exoplayer.ExoPlayer import kotlinx.coroutines.channels.Channel -import timber.log.Timber -import java.util.LinkedList -import java.util.Queue -class PlayersPool( - private val context: Context, - private val maxPoolSize: Int -) { - private val unlockedPlayers: MutableList = mutableListOf() - private val lockedPlayers: MutableList = mutableListOf() - private var playerIndex: Int = 0 +interface PlayersPool { - private val waitingQueue: Queue> = LinkedList() - - @Synchronized - fun acquire(): Channel = - if(unlockedPlayers.isEmpty()) { - if(lockedPlayers.size >= maxPoolSize) { - Channel(capacity = 1).also { channel -> waitingQueue.offer(channel) } - } else { - Channel(capacity = 1).apply { - trySend( - ExoPlayer.Builder(context) - .setLoadControl( - DefaultLoadControl.Builder() - .setBufferDurationsMs( - 10_000, - 10_000, - 100, - 2000 - ).build() - ) - .build() - .let { exoPlayer -> ReusablePlayer(exoPlayer) } - .apply { playerId = "player${playerIndex++}" } - .also { player -> lockedPlayers.add(player) } - ) - } - } - } else { - Channel(capacity = 1).apply { - trySend(unlockedPlayers.removeLast().also(lockedPlayers::add)) - } - }.also { - Timber.tag(VIDEO_LIST).d("pool size = %s", lockedPlayers.size + unlockedPlayers.size) - } - - @Synchronized - fun removeFromAwaitingQueue(channel: Channel) { - waitingQueue.remove(channel) - } - - @Synchronized - fun release(player: Player) { - lockedPlayers.removeAll { reusablePlayer -> reusablePlayer.player == player } - } - - @Synchronized - fun stop(player: Player) { - val reusablePlayer = lockedPlayers.first { reusablePlayer -> reusablePlayer.player == player } - if (!reusePlayer(reusablePlayer)) { - lockedPlayers.remove(reusablePlayer) - unlockedPlayers.add(reusablePlayer) - } - } - - private fun reusePlayer(player: ReusablePlayer): Boolean = - waitingQueue.poll()?.run { - trySend(player) - true - } ?: false - - @Synchronized - fun releaseAll() { - waitingQueue.clear() - unlockedPlayers.addAll(lockedPlayers) - lockedPlayers.clear() - } + fun acquire(url: String = ""): Channel + fun removeFromAwaitingQueue(channel: Channel) + fun release(player: Player) + fun stop(player: Player) + fun releaseAll() } \ No newline at end of file diff --git a/app/src/main/java/com/uptech/videolist/ReusablePlayersPool.kt b/app/src/main/java/com/uptech/videolist/ReusablePlayersPool.kt new file mode 100644 index 0000000..b79f585 --- /dev/null +++ b/app/src/main/java/com/uptech/videolist/ReusablePlayersPool.kt @@ -0,0 +1,86 @@ +package com.uptech.videolist + +import android.content.Context +import androidx.media3.common.Player +import androidx.media3.exoplayer.DefaultLoadControl +import androidx.media3.exoplayer.ExoPlayer +import kotlinx.coroutines.channels.Channel +import timber.log.Timber +import java.util.LinkedList +import java.util.Queue + +class ReusablePlayersPool( + private val context: Context, + private val maxPoolSize: Int +) : PlayersPool { + private val unlockedPlayers: MutableList = mutableListOf() + private val lockedPlayers: MutableList = mutableListOf() + private var playerIndex: Int = 0 + + private val waitingQueue: Queue> = LinkedList() + + @Synchronized + override fun acquire(url: String): Channel = + if(unlockedPlayers.isEmpty()) { + if(lockedPlayers.size >= maxPoolSize) { + Channel(capacity = 1).also { channel -> waitingQueue.offer(channel) } + } else { + Channel(capacity = 1).apply { + trySend( + ExoPlayer.Builder(context) + .setLoadControl( + DefaultLoadControl.Builder() + .setBufferDurationsMs( + 10_000, + 10_000, + 100, + 2000 + ).build() + ) + .build() + .let { exoPlayer -> ReusablePlayer(exoPlayer) } + .apply { playerId = "player${playerIndex++}" } + .also { player -> lockedPlayers.add(player) } + ) + } + } + } else { + Channel(capacity = 1).apply { + trySend(unlockedPlayers.removeLast().also(lockedPlayers::add)) + } + }.also { + Timber.tag(VIDEO_LIST).d("pool size = %s", lockedPlayers.size + unlockedPlayers.size) + } + + @Synchronized + override fun removeFromAwaitingQueue(channel: Channel) { + waitingQueue.remove(channel) + } + + @Synchronized + override fun release(player: Player) { + lockedPlayers.removeAll { reusablePlayer -> reusablePlayer.player == player } + } + + @Synchronized + override fun stop(player: Player) { + val reusablePlayer = lockedPlayers.first { reusablePlayer -> reusablePlayer.player == player } + if (!reusePlayer(reusablePlayer)) { + lockedPlayers.remove(reusablePlayer) + unlockedPlayers.add(reusablePlayer) + } + } + + private fun reusePlayer(player: ReusablePlayer): Boolean = + waitingQueue.poll()?.run { + trySend(player) + true + } ?: false + + @Synchronized + override fun releaseAll() { + waitingQueue.clear() + unlockedPlayers.addAll(lockedPlayers) + lockedPlayers.clear() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/uptech/videolist/VideoAdapter.kt b/app/src/main/java/com/uptech/videolist/VideoAdapter.kt index 68b1578..365be36 100644 --- a/app/src/main/java/com/uptech/videolist/VideoAdapter.kt +++ b/app/src/main/java/com/uptech/videolist/VideoAdapter.kt @@ -103,7 +103,7 @@ class VideoAdapter( private fun bindPlayer(mediaSource: MediaSource, playbackPosition: Long) { playJob?.cancel() playJob = videoScope.launch { - playersPool.acquire() + playersPool.acquire(mediaSource.mediaItem.playbackProperties?.uri.toString()) .also { playerChannel = it } .receive() .run {