diff --git a/app/src/main/java/com/vitorpamplona/amethyst/MultiPlayerPlaybackManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/MultiPlayerPlaybackManager.kt index 022fbbf96..f6ee5ad58 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/MultiPlayerPlaybackManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/MultiPlayerPlaybackManager.kt @@ -12,11 +12,15 @@ import androidx.media3.common.Player.STATE_READY import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import com.vitorpamplona.amethyst.ui.MainActivity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import kotlin.math.abs -class MultiPlayerPlaybackManager(private val dataSourceFactory: androidx.media3.exoplayer.source.MediaSource.Factory? = null) { - private val cachedPositions = LruCache(100) - +class MultiPlayerPlaybackManager( + private val dataSourceFactory: androidx.media3.exoplayer.source.MediaSource.Factory? = null, + private val cachedPositions: VideoViewedPositionCache +) { // protects from LruCache killing playing sessions private val playingMap = mutableMapOf() @@ -87,7 +91,7 @@ class MultiPlayerPlaybackManager(private val dataSourceFactory: androidx.media3. when (playbackState) { STATE_IDLE -> { if (player.currentPosition > 5 * 60) { // 5 seconds - cachedPositions.put(uri, player.currentPosition) + cachedPositions.add(uri, player.currentPosition) } } STATE_READY -> { @@ -99,7 +103,7 @@ class MultiPlayerPlaybackManager(private val dataSourceFactory: androidx.media3. } else -> { 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() { - cache.evictAll() + GlobalScope.launch(Dispatchers.Main) { + cache.evictAll() + playingMap.forEach { + it.value.player.release() + it.value.release() + } + playingMap.clear() + } } fun playingContent(): Collection { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/PlaybackService.kt b/app/src/main/java/com/vitorpamplona/amethyst/PlaybackService.kt index a9d2cf496..1b8ebac64 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/PlaybackService.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/PlaybackService.kt @@ -4,6 +4,7 @@ import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.hls.HlsMediaSource +import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.session.DefaultMediaNotificationProvider import androidx.media3.session.MediaSession @@ -12,14 +13,18 @@ import com.vitorpamplona.amethyst.service.HttpClient @UnstableApi // Extend MediaSessionService class PlaybackService : MediaSessionService() { - private val managerHls by lazy { - MultiPlayerPlaybackManager(HlsMediaSource.Factory(OkHttpDataSource.Factory(HttpClient.getHttpClient()))) + private var videoViewedPositionCache = VideoViewedPositionCache() + + 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))) - } - private val managerLocal by lazy { - MultiPlayerPlaybackManager() + + fun newProgressiveDataSource(): MediaSource.Factory { + return ProgressiveMediaSource.Factory(VideoCache.get(applicationContext, HttpClient.getHttpClient())) } // Create your Player and MediaSession in the onCreate lifecycle event @@ -27,20 +32,36 @@ class PlaybackService : MediaSessionService() { override fun 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( DefaultMediaNotificationProvider.Builder(applicationContext).build() ) } override fun onDestroy() { - managerHls.releaseAppPlayers() - managerLocal.releaseAppPlayers() - managerProgressive.releaseAppPlayers() + managerHls?.releaseAppPlayers() + managerLocal?.releaseAppPlayers() + managerProgressive?.releaseAppPlayers() super.onDestroy() } - fun getAppropriateMediaSessionManager(fileName: String): MultiPlayerPlaybackManager { + fun getAppropriateMediaSessionManager(fileName: String): MultiPlayerPlaybackManager? { return if (fileName.startsWith("file")) { managerLocal } else if (fileName.endsWith("m3u8")) { @@ -55,34 +76,34 @@ class PlaybackService : MediaSessionService() { super.onUpdateNotification(session, startInForegroundRequired) // Overrides the notification with any player actually playing - managerHls.playingContent().forEach { + managerHls?.playingContent()?.forEach { if (it.player.isPlaying) { super.onUpdateNotification(it, startInForegroundRequired) } } - managerLocal.playingContent().forEach { + managerLocal?.playingContent()?.forEach { if (it.player.isPlaying) { super.onUpdateNotification(session, startInForegroundRequired) } } - managerProgressive.playingContent().forEach { + managerProgressive?.playingContent()?.forEach { if (it.player.isPlaying) { super.onUpdateNotification(session, startInForegroundRequired) } } // Overrides again with playing with audio - managerHls.playingContent().forEach { + managerHls?.playingContent()?.forEach { if (it.player.isPlaying && it.player.volume > 0) { super.onUpdateNotification(it, startInForegroundRequired) } } - managerLocal.playingContent().forEach { + managerLocal?.playingContent()?.forEach { if (it.player.isPlaying && it.player.volume > 0) { super.onUpdateNotification(session, startInForegroundRequired) } } - managerProgressive.playingContent().forEach { + managerProgressive?.playingContent()?.forEach { if (it.player.isPlaying && it.player.volume > 0) { super.onUpdateNotification(session, startInForegroundRequired) } @@ -98,6 +119,6 @@ class PlaybackService : MediaSessionService() { val manager = getAppropriateMediaSessionManager(uri) - return manager.getMediaSession(id, uri, callbackUri, context = this, applicationContext = applicationContext) + return manager?.getMediaSession(id, uri, callbackUri, context = this, applicationContext = applicationContext) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/VideoCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/VideoCache.kt index 50dcb3cc5..0dadaaa6d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/VideoCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/VideoCache.kt @@ -7,7 +7,7 @@ import androidx.media3.datasource.cache.CacheDataSource import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor import androidx.media3.datasource.cache.SimpleCache import androidx.media3.datasource.okhttp.OkHttpDataSource -import com.vitorpamplona.amethyst.service.HttpClient +import okhttp3.OkHttpClient @UnstableApi object VideoCache { @@ -22,7 +22,7 @@ import com.vitorpamplona.amethyst.service.HttpClient @Synchronized @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) - private fun init(context: Context) { + private fun init(context: Context, client: OkHttpClient) { exoDatabaseProvider = StandaloneDatabaseProvider(context) simpleCache = SimpleCache( @@ -31,22 +31,22 @@ import com.vitorpamplona.amethyst.service.HttpClient exoDatabaseProvider ) - renewCacheFactory() + renewCacheFactory(client) } // This method should be called when proxy setting changes. - fun renewCacheFactory() { + fun renewCacheFactory(client: OkHttpClient) { cacheDataSourceFactory = CacheDataSource.Factory() .setCache(simpleCache) .setUpstreamDataSourceFactory( - OkHttpDataSource.Factory(HttpClient.getHttpClient()) + OkHttpDataSource.Factory(client) ) .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) { - init(context) + init(context, client) } return cacheDataSourceFactory diff --git a/app/src/main/java/com/vitorpamplona/amethyst/VideoViewedPositionCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/VideoViewedPositionCache.kt index 7d3bee4ac..29dfa193a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/VideoViewedPositionCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/VideoViewedPositionCache.kt @@ -2,8 +2,8 @@ package com.vitorpamplona.amethyst import android.util.LruCache -object VideoViewedPositionCache { - val cachedPosition = LruCache(10) +class VideoViewedPositionCache { + val cachedPosition = LruCache(100) fun add(uri: String, position: Long) { cachedPosition.put(uri, position) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt index bf2448ece..d9986573e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt @@ -5,19 +5,29 @@ import okhttp3.OkHttpClient import java.net.InetSocketAddress import java.net.Proxy import java.time.Duration +import kotlin.properties.Delegates 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?) { - this.proxy = account?.proxy + this.internalProxy = account?.proxy } fun getHttpClient(): OkHttpClient { - val seconds = if (proxy != null) 20L else 10L + val seconds = if (internalProxy != null) 20L else 10L val duration = Duration.ofSeconds(seconds) return OkHttpClient.Builder() - .proxy(proxy) + .proxy(internalProxy) .readTimeout(duration) .connectTimeout(duration) .writeTimeout(duration) @@ -25,7 +35,7 @@ object HttpClient { } fun getProxy(): Proxy? { - return proxy + return internalProxy } fun initProxy(useProxy: Boolean, hostname: String, port: Int): Proxy? {