Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package="com.uptech.videolist">

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

<application
android:name=".App"
Expand Down
51 changes: 45 additions & 6 deletions app/src/main/java/com/uptech/videolist/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
package com.uptech.videolist

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
import androidx.media3.common.util.Util
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.FileDataSource
import androidx.media3.datasource.cache.CacheDataSink
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.NoOpCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import com.uptech.videolist.databinding.ActivityMainBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.io.File

class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels(
Expand All @@ -20,11 +31,11 @@ class MainActivity : AppCompatActivity() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): 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
minOf(4, availableCodecsNum())
//4minOf(4, availableCodecsNum())
)
) as T
}
Expand All @@ -40,18 +51,42 @@ class MainActivity : AppCompatActivity() {
)
}

lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.videoList.adapter = adapter
val videoCacheDir = File(applicationContext.externalCacheDir, VIDEO_CACHE_DIR)
val videoCache = SimpleCache(
videoCacheDir,
NoOpCacheEvictor(),
StandaloneDatabaseProvider(this)
)
val cacheSink = CacheDataSink.Factory()
.setCache(videoCache)
val upstreamFactory = DefaultDataSource.Factory(
this,
DefaultHttpDataSource.Factory()
)
val downStreamFactory = FileDataSource.Factory()
val cacheDataSourceFactory = CacheDataSource.Factory()
.setCache(videoCache)
.setCacheWriteDataSinkFactory(cacheSink)
.setCacheReadDataSourceFactory(downStreamFactory)
.setUpstreamDataSourceFactory(upstreamFactory)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
lifecycleScope.launch {
viewModel.playbackPositions
.onEach { playbackPositions -> 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)
}
}

Expand All @@ -78,4 +113,8 @@ class MainActivity : AppCompatActivity() {
viewModel.releasePlayers()
super.onStop()
}

companion object {
const val VIDEO_CACHE_DIR = "videoCache"
}
}
46 changes: 46 additions & 0 deletions app/src/main/java/com/uptech/videolist/NoOpPlayersPool.kt
Original file line number Diff line number Diff line change
@@ -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<String, ReusablePlayer> = mutableMapOf()
private var playerIndex: Int = 0

override fun acquire(url: String): Channel<ReusablePlayer> =
playerMap[url]?.let { reusablePlayer ->
Channel<ReusablePlayer>(capacity = 1).apply {
trySend(reusablePlayer)
}
} ?: Channel<ReusablePlayer>(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<ReusablePlayer>) {}

override fun release(player: Player) {}

override fun stop(player: Player) {}

override fun releaseAll() {}
}
67 changes: 6 additions & 61 deletions app/src/main/java/com/uptech/videolist/PlayersPool.kt
Original file line number Diff line number Diff line change
@@ -1,68 +1,13 @@
package com.uptech.videolist

import android.content.Context
import androidx.media3.common.Player
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<Player> = mutableListOf(ExoPlayer.Builder(context).build())
private val lockedPlayers: MutableList<Player> = mutableListOf()
interface PlayersPool {

private val waitingQueue: Queue<Channel<Player>> = LinkedList()

@Synchronized
fun acquire(): Channel<Player> =
if(unlockedPlayers.isEmpty()) {
if(lockedPlayers.size >= maxPoolSize) {
Channel<Player>(capacity = 1).also { channel -> waitingQueue.offer(channel) }
} else {
Channel<Player>(capacity = 1).apply {
trySend(ExoPlayer.Builder(context).build().also(lockedPlayers::add))
}
}
} else {
Channel<Player>(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<Player>) {
waitingQueue.remove(channel)
}

@Synchronized
fun release(player: Player) {
lockedPlayers.remove(player)
}

@Synchronized
fun stop(player: Player) {
if(!reusePlayer(player)) {
lockedPlayers.remove(player)
unlockedPlayers.add(player)
}
}

private fun reusePlayer(player: Player): Boolean =
waitingQueue.poll()?.run {
trySend(player)
true
} ?: false

@Synchronized
fun releaseAll() {
waitingQueue.clear()
unlockedPlayers.addAll(lockedPlayers)
lockedPlayers.clear()
}
fun acquire(url: String = ""): Channel<ReusablePlayer>
fun removeFromAwaitingQueue(channel: Channel<ReusablePlayer>)
fun release(player: Player)
fun stop(player: Player)
fun releaseAll()
}
82 changes: 82 additions & 0 deletions app/src/main/java/com/uptech/videolist/ReusablePlayer.kt
Original file line number Diff line number Diff line change
@@ -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<String> = 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"
}
}
Loading