Stops Video playback when switching in and out of Tor
This commit is contained in:
@@ -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? {
|
||||||
|
|||||||
Reference in New Issue
Block a user