Stops Video playback when switching in and out of Tor

This commit is contained in:
Vitor Pamplona
2023-07-21 10:44:02 -04:00
parent 8dd5705f02
commit fe4a4b6fad
5 changed files with 80 additions and 38 deletions
@@ -12,11 +12,15 @@ import androidx.media3.common.Player.STATE_READY
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.amethyst.ui.MainActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlin.math.abs import kotlin.math.abs
class MultiPlayerPlaybackManager(private val dataSourceFactory: androidx.media3.exoplayer.source.MediaSource.Factory? = null) { class MultiPlayerPlaybackManager(
private val cachedPositions = LruCache<String, Long>(100) private val dataSourceFactory: androidx.media3.exoplayer.source.MediaSource.Factory? = null,
private val cachedPositions: VideoViewedPositionCache
) {
// protects from LruCache killing playing sessions // protects from LruCache killing playing sessions
private val playingMap = mutableMapOf<String, MediaSession>() private val playingMap = mutableMapOf<String, MediaSession>()
@@ -87,7 +91,7 @@ class MultiPlayerPlaybackManager(private val dataSourceFactory: androidx.media3.
when (playbackState) { when (playbackState) {
STATE_IDLE -> { STATE_IDLE -> {
if (player.currentPosition > 5 * 60) { // 5 seconds if (player.currentPosition > 5 * 60) { // 5 seconds
cachedPositions.put(uri, player.currentPosition) cachedPositions.add(uri, player.currentPosition)
} }
} }
STATE_READY -> { STATE_READY -> {
@@ -99,7 +103,7 @@ class MultiPlayerPlaybackManager(private val dataSourceFactory: androidx.media3.
} }
else -> { else -> {
if (player.currentPosition > 5 * 60) { // 5 seconds if (player.currentPosition > 5 * 60) { // 5 seconds
cachedPositions.put(uri, player.currentPosition) cachedPositions.add(uri, player.currentPosition)
} }
} }
} }
@@ -112,7 +116,14 @@ class MultiPlayerPlaybackManager(private val dataSourceFactory: androidx.media3.
} }
fun releaseAppPlayers() { fun releaseAppPlayers() {
cache.evictAll() GlobalScope.launch(Dispatchers.Main) {
cache.evictAll()
playingMap.forEach {
it.value.player.release()
it.value.release()
}
playingMap.clear()
}
} }
fun playingContent(): Collection<MediaSession> { fun playingContent(): Collection<MediaSession> {
@@ -4,6 +4,7 @@ import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.hls.HlsMediaSource
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.session.DefaultMediaNotificationProvider import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
@@ -12,14 +13,18 @@ import com.vitorpamplona.amethyst.service.HttpClient
@UnstableApi // Extend MediaSessionService @UnstableApi // Extend MediaSessionService
class PlaybackService : MediaSessionService() { class PlaybackService : MediaSessionService() {
private val managerHls by lazy { private var videoViewedPositionCache = VideoViewedPositionCache()
MultiPlayerPlaybackManager(HlsMediaSource.Factory(OkHttpDataSource.Factory(HttpClient.getHttpClient())))
private var managerHls: MultiPlayerPlaybackManager? = null
private var managerProgressive: MultiPlayerPlaybackManager? = null
private var managerLocal: MultiPlayerPlaybackManager? = null
fun newHslDataSource(): MediaSource.Factory {
return HlsMediaSource.Factory(OkHttpDataSource.Factory(HttpClient.getHttpClient()))
} }
private val managerProgressive by lazy {
MultiPlayerPlaybackManager(ProgressiveMediaSource.Factory(VideoCache.get(applicationContext))) fun newProgressiveDataSource(): MediaSource.Factory {
} return ProgressiveMediaSource.Factory(VideoCache.get(applicationContext, HttpClient.getHttpClient()))
private val managerLocal by lazy {
MultiPlayerPlaybackManager()
} }
// Create your Player and MediaSession in the onCreate lifecycle event // Create your Player and MediaSession in the onCreate lifecycle event
@@ -27,20 +32,36 @@ class PlaybackService : MediaSessionService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
managerHls = MultiPlayerPlaybackManager(newHslDataSource(), videoViewedPositionCache)
managerProgressive = MultiPlayerPlaybackManager(newProgressiveDataSource(), videoViewedPositionCache)
managerLocal = MultiPlayerPlaybackManager(cachedPositions = videoViewedPositionCache)
// Stop all videos and recreates all managers when the proxy changes.
HttpClient.proxyChangeListeners.add {
val toDestroyHls = managerHls
val toDestroyProgressive = managerProgressive
managerHls = MultiPlayerPlaybackManager(newHslDataSource(), videoViewedPositionCache)
managerProgressive = MultiPlayerPlaybackManager(newProgressiveDataSource(), videoViewedPositionCache)
toDestroyHls?.releaseAppPlayers()
toDestroyProgressive?.releaseAppPlayers()
}
setMediaNotificationProvider( setMediaNotificationProvider(
DefaultMediaNotificationProvider.Builder(applicationContext).build() DefaultMediaNotificationProvider.Builder(applicationContext).build()
) )
} }
override fun onDestroy() { override fun onDestroy() {
managerHls.releaseAppPlayers() managerHls?.releaseAppPlayers()
managerLocal.releaseAppPlayers() managerLocal?.releaseAppPlayers()
managerProgressive.releaseAppPlayers() managerProgressive?.releaseAppPlayers()
super.onDestroy() super.onDestroy()
} }
fun getAppropriateMediaSessionManager(fileName: String): MultiPlayerPlaybackManager { fun getAppropriateMediaSessionManager(fileName: String): MultiPlayerPlaybackManager? {
return if (fileName.startsWith("file")) { return if (fileName.startsWith("file")) {
managerLocal managerLocal
} else if (fileName.endsWith("m3u8")) { } else if (fileName.endsWith("m3u8")) {
@@ -55,34 +76,34 @@ class PlaybackService : MediaSessionService() {
super.onUpdateNotification(session, startInForegroundRequired) super.onUpdateNotification(session, startInForegroundRequired)
// Overrides the notification with any player actually playing // Overrides the notification with any player actually playing
managerHls.playingContent().forEach { managerHls?.playingContent()?.forEach {
if (it.player.isPlaying) { if (it.player.isPlaying) {
super.onUpdateNotification(it, startInForegroundRequired) super.onUpdateNotification(it, startInForegroundRequired)
} }
} }
managerLocal.playingContent().forEach { managerLocal?.playingContent()?.forEach {
if (it.player.isPlaying) { if (it.player.isPlaying) {
super.onUpdateNotification(session, startInForegroundRequired) super.onUpdateNotification(session, startInForegroundRequired)
} }
} }
managerProgressive.playingContent().forEach { managerProgressive?.playingContent()?.forEach {
if (it.player.isPlaying) { if (it.player.isPlaying) {
super.onUpdateNotification(session, startInForegroundRequired) super.onUpdateNotification(session, startInForegroundRequired)
} }
} }
// Overrides again with playing with audio // Overrides again with playing with audio
managerHls.playingContent().forEach { managerHls?.playingContent()?.forEach {
if (it.player.isPlaying && it.player.volume > 0) { if (it.player.isPlaying && it.player.volume > 0) {
super.onUpdateNotification(it, startInForegroundRequired) super.onUpdateNotification(it, startInForegroundRequired)
} }
} }
managerLocal.playingContent().forEach { managerLocal?.playingContent()?.forEach {
if (it.player.isPlaying && it.player.volume > 0) { if (it.player.isPlaying && it.player.volume > 0) {
super.onUpdateNotification(session, startInForegroundRequired) super.onUpdateNotification(session, startInForegroundRequired)
} }
} }
managerProgressive.playingContent().forEach { managerProgressive?.playingContent()?.forEach {
if (it.player.isPlaying && it.player.volume > 0) { if (it.player.isPlaying && it.player.volume > 0) {
super.onUpdateNotification(session, startInForegroundRequired) super.onUpdateNotification(session, startInForegroundRequired)
} }
@@ -98,6 +119,6 @@ class PlaybackService : MediaSessionService() {
val manager = getAppropriateMediaSessionManager(uri) val manager = getAppropriateMediaSessionManager(uri)
return manager.getMediaSession(id, uri, callbackUri, context = this, applicationContext = applicationContext) return manager?.getMediaSession(id, uri, callbackUri, context = this, applicationContext = applicationContext)
} }
} }
@@ -7,7 +7,7 @@ import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.datasource.okhttp.OkHttpDataSource
import com.vitorpamplona.amethyst.service.HttpClient import okhttp3.OkHttpClient
@UnstableApi object VideoCache { @UnstableApi object VideoCache {
@@ -22,7 +22,7 @@ import com.vitorpamplona.amethyst.service.HttpClient
@Synchronized @Synchronized
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
private fun init(context: Context) { private fun init(context: Context, client: OkHttpClient) {
exoDatabaseProvider = StandaloneDatabaseProvider(context) exoDatabaseProvider = StandaloneDatabaseProvider(context)
simpleCache = SimpleCache( simpleCache = SimpleCache(
@@ -31,22 +31,22 @@ import com.vitorpamplona.amethyst.service.HttpClient
exoDatabaseProvider exoDatabaseProvider
) )
renewCacheFactory() renewCacheFactory(client)
} }
// This method should be called when proxy setting changes. // This method should be called when proxy setting changes.
fun renewCacheFactory() { fun renewCacheFactory(client: OkHttpClient) {
cacheDataSourceFactory = CacheDataSource.Factory() cacheDataSourceFactory = CacheDataSource.Factory()
.setCache(simpleCache) .setCache(simpleCache)
.setUpstreamDataSourceFactory( .setUpstreamDataSourceFactory(
OkHttpDataSource.Factory(HttpClient.getHttpClient()) OkHttpDataSource.Factory(client)
) )
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR) .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
} }
fun get(context: Context): CacheDataSource.Factory { fun get(context: Context, client: OkHttpClient): CacheDataSource.Factory {
if (!this::simpleCache.isInitialized) { if (!this::simpleCache.isInitialized) {
init(context) init(context, client)
} }
return cacheDataSourceFactory return cacheDataSourceFactory
@@ -2,8 +2,8 @@ package com.vitorpamplona.amethyst
import android.util.LruCache import android.util.LruCache
object VideoViewedPositionCache { class VideoViewedPositionCache {
val cachedPosition = LruCache<String, Long>(10) val cachedPosition = LruCache<String, Long>(100)
fun add(uri: String, position: Long) { fun add(uri: String, position: Long) {
cachedPosition.put(uri, position) cachedPosition.put(uri, position)
@@ -5,19 +5,29 @@ import okhttp3.OkHttpClient
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Proxy import java.net.Proxy
import java.time.Duration import java.time.Duration
import kotlin.properties.Delegates
object HttpClient { object HttpClient {
private var proxy: Proxy? = null var proxyChangeListeners = ArrayList<() -> Unit>()
// fires off every time value of the property changes
private var internalProxy: Proxy? by Delegates.observable(null) { _, oldValue, newValue ->
if (oldValue != newValue) {
proxyChangeListeners.forEach {
it()
}
}
}
fun start(account: Account?) { fun start(account: Account?) {
this.proxy = account?.proxy this.internalProxy = account?.proxy
} }
fun getHttpClient(): OkHttpClient { fun getHttpClient(): OkHttpClient {
val seconds = if (proxy != null) 20L else 10L val seconds = if (internalProxy != null) 20L else 10L
val duration = Duration.ofSeconds(seconds) val duration = Duration.ofSeconds(seconds)
return OkHttpClient.Builder() return OkHttpClient.Builder()
.proxy(proxy) .proxy(internalProxy)
.readTimeout(duration) .readTimeout(duration)
.connectTimeout(duration) .connectTimeout(duration)
.writeTimeout(duration) .writeTimeout(duration)
@@ -25,7 +35,7 @@ object HttpClient {
} }
fun getProxy(): Proxy? { fun getProxy(): Proxy? {
return proxy return internalProxy
} }
fun initProxy(useProxy: Boolean, hostname: String, port: Int): Proxy? { fun initProxy(useProxy: Boolean, hostname: String, port: Int): Proxy? {