diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 565fbdde..159b5810 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -525,6 +525,10 @@ class Settings : FragmentedStorageFileJson() { @Serializable(with = FlexibleBooleanSerializer::class) var keepScreenOn: Boolean = true; + @FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 1) + @Serializable(with = FlexibleBooleanSerializer::class) + var alwaysProxyRequests: Boolean = false; + /*TODO: Should we have a different casting quality? @FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3) @DropdownFieldOptionsId(R.array.preferred_quality_array) diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index fbda6bfa..5e99462e 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -7,6 +7,7 @@ import android.os.Looper import android.util.Base64 import android.util.Log import com.futo.platformplayer.BuildConfig +import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.server.ManagedHttpServer @@ -452,14 +453,22 @@ class StateCasting { } } } else { + val proxyStreams = Settings.instance.casting.alwaysProxyRequests; + val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"; + val id = UUID.randomUUID(); + if (videoSource is IVideoUrlSource) { + val videoPath = "/video-${id}" + val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl(); Logger.i(TAG, "Casting as singular video"); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble(), speed); + ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed); } else if (audioSource is IAudioUrlSource) { + val audioPath = "/audio-${id}" + val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl(); Logger.i(TAG, "Casting as singular audio"); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble(), speed); + ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed); } else if(videoSource is IHLSManifestSource) { - if (ad is ChromecastCastingDevice) { + if (proxyStreams || ad is ChromecastCastingDevice) { Logger.i(TAG, "Casting as proxied HLS"); castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed); } else { @@ -467,7 +476,7 @@ class StateCasting { ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed); } } else if(audioSource is IHLSManifestAudioSource) { - if (ad is ChromecastCastingDevice) { + if (proxyStreams || ad is ChromecastCastingDevice) { Logger.i(TAG, "Casting as proxied audio HLS"); castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed); } else { @@ -667,8 +676,11 @@ class StateCasting { val audioUrl = url + audioPath; val subtitleUrl = url + subtitlePath; + val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl); + Logger.v(TAG) { "Dash manifest: $dashContent" }; + _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl), + HttpConstantHandler("GET", dashPath, dashContent, "application/dash+xml") .withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); @@ -699,13 +711,17 @@ class StateCasting { private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List { val ad = activeDevice ?: return listOf(); + val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice; val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"; val id = UUID.randomUUID(); - val subtitlePath = "/subtitle-${id}"; - val videoUrl = videoSource?.getVideoUrl(); - val audioUrl = audioSource?.getAudioUrl(); + val videoPath = "/video-${id}" + val audioPath = "/audio-${id}" + val subtitlePath = "/subtitle-${id}" + + val videoUrl = if(proxyStreams) url + videoPath else videoSource?.getVideoUrl(); + val audioUrl = if(proxyStreams) url + audioPath else audioSource?.getAudioUrl(); val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) { return@withContext subtitleSource.getSubtitlesURI(); @@ -734,13 +750,28 @@ class StateCasting { } } + if (videoSource != null) { + _castServer.addHandlerWithAllowAllOptions( + HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true) + .withInjectedHost() + .withHeader("Access-Control-Allow-Origin", "*"), true + ).withTag("cast"); + } + if (audioSource != null) { + _castServer.addHandlerWithAllowAllOptions( + HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true) + .withInjectedHost() + .withHeader("Access-Control-Allow-Origin", "*"), true + ).withTag("cast"); + } + val content = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl); Logger.i(TAG, "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl)."); + Logger.v(TAG) { "Dash manifest: $content" }; ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), speed); - return listOf(videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: ""); - } + return listOf(videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString()); } private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List { _castServer.removeAllHandlers("castProxiedHlsMaster") @@ -1044,7 +1075,7 @@ class StateCasting { private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List { val ad = activeDevice ?: return listOf(); - val proxyStreams = ad !is FCastCastingDevice; + val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice; val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"; val id = UUID.randomUUID(); @@ -1090,8 +1121,11 @@ class StateCasting { } } + val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl); + Logger.v(TAG) { "Dash manifest: $dashContent" }; + _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", dashPath, DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl), + HttpConstantHandler("GET", dashPath, dashContent, "application/dash+xml") .withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 41d94a82..c072e9a0 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -161,6 +161,7 @@ import java.time.OffsetDateTime import kotlin.math.abs import kotlin.math.roundToLong +@androidx.media3.common.util.UnstableApi class VideoDetailView : ConstraintLayout { private val TAG = "VideoDetailView" diff --git a/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt b/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt index 77125de8..ddfa89e9 100644 --- a/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt +++ b/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt @@ -15,7 +15,9 @@ import android.media.AudioManager import android.media.AudioManager.OnAudioFocusChangeListener import android.media.MediaMetadata import android.os.Build +import android.os.Handler import android.os.IBinder +import android.os.Looper import android.os.SystemClock import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.session.MediaSessionCompat @@ -57,6 +59,15 @@ class MediaPlaybackService : Service() { private var _audioFocusLossTime_ms: Long? = null private var _playbackState = PlaybackStateCompat.STATE_NONE; + private val _updateIntervalMs: Long = 5 * 60 * 1000 + private val _handler: Handler = Handler(Looper.getMainLooper()) + private val _updateRunnable: Runnable = object : Runnable { + override fun run() { + updateMediaSession(null) + _handler.postDelayed(this, _updateIntervalMs) + } + } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Logger.v(TAG, "onStartCommand"); @@ -74,6 +85,8 @@ class MediaPlaybackService : Service() { _callOnStarted?.invoke(this); _instance = this; + + _handler.postDelayed(_updateRunnable, _updateIntervalMs) } catch(ex: Throwable) { Logger.e(TAG, "Failed to start MediaPlaybackService due to: " + ex.message, ex); @@ -143,6 +156,7 @@ class MediaPlaybackService : Service() { override fun onDestroy() { Logger.v(TAG, "onDestroy"); _instance = null; + _handler.removeCallbacks(_updateRunnable) MediaControlReceiver.onPauseReceived.emit(); super.onDestroy(); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4cba9f1c..ea17dc8a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -66,6 +66,8 @@ Enabled Keep screen on Keep screen on while casting + Always proxy requests + Always proxy requests when casting data through the device. Discover Find new video sources to add These sources have been disabled