From cd90497a59ab0b658137dd37bd8710d56ff9d16a Mon Sep 17 00:00:00 2001 From: Stefan <84-stefancruz@users.noreply.gitlab.futo.org> Date: Mon, 6 Apr 2026 16:56:09 +0100 Subject: [PATCH] Improve request modifier support for casting and downloads --- .../futo/platformplayer/UISlideOverlays.kt | 10 +- .../api/http/ManagedHttpClient.kt | 21 ++- .../platformplayer/casting/StateCasting.kt | 13 +- .../platformplayer/downloads/VideoDownload.kt | 140 ++++++++++-------- .../services/DownloadService.kt | 10 ++ .../platformplayer/states/StateDownloads.kt | 5 +- 6 files changed, 119 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index 2a1f1219..cf6c69fd 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -32,6 +32,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.platforms.js.JSClient +import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource import com.futo.platformplayer.downloads.VideoLocal @@ -382,7 +383,8 @@ class UISlideOverlays { val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items) StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { - val masterPlaylistResponse = ManagedHttpClient().get(sourceUrl) + val modifier = if (source is JSSource && source.hasRequestModifier) source.getRequestModifier() else null + val masterPlaylistResponse = ManagedHttpClient().get(sourceUrl, HashMap(), modifier) check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" } val masterPlaylistContent = masterPlaylistResponse.body?.string() @@ -515,7 +517,7 @@ class UISlideOverlays { slideUpMenuOverlay.onOK.subscribe { //TODO: Fix SubtitleRawSource issue - StateDownloads.instance.download(video, selectedVideoVariant, selectedAudioVariant, null); + StateDownloads.instance.download(video, selectedVideoVariant, selectedAudioVariant, null, videoModifier = modifier, audioModifier = modifier); slideUpMenuOverlay.hide() } @@ -526,11 +528,11 @@ class UISlideOverlays { if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) { withContext(Dispatchers.Main) { if (source is IHLSManifestSource) { - StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, resolvedPlaylistUrl), null, null) + StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, resolvedPlaylistUrl), null, null, videoModifier = modifier) UIDialogs.toast(container.context, "Variant video HLS playlist download started") slideUpMenuOverlay.hide() } else if (source is IHLSManifestAudioSource) { - StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, resolvedPlaylistUrl), null) + StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, resolvedPlaylistUrl), null, audioModifier = modifier) UIDialogs.toast(container.context, "Variant audio HLS playlist download started") slideUpMenuOverlay.hide() } else { diff --git a/app/src/main/java/com/futo/platformplayer/api/http/ManagedHttpClient.kt b/app/src/main/java/com/futo/platformplayer/api/http/ManagedHttpClient.kt index 089c8106..fd0430c5 100644 --- a/app/src/main/java/com/futo/platformplayer/api/http/ManagedHttpClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/http/ManagedHttpClient.kt @@ -23,6 +23,7 @@ import java.time.Duration import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager +import com.futo.platformplayer.api.media.models.modifier.IRequestModifier import kotlin.system.measureTimeMillis open class ManagedHttpClient { @@ -89,10 +90,16 @@ open class ManagedHttpClient { return clonedClient; } - fun tryHead(url: String): Map? { + private fun applyModifier(url: String, headers: MutableMap, modifier: IRequestModifier?): Pair> { + if (modifier == null) return Pair(url, headers) + val modified = modifier.modifyRequest(url, headers) + return Pair(modified.url ?: url, modified.headers.toMutableMap()) + } + + fun tryHead(url: String, modifier: IRequestModifier? = null): Map? { ensureNotMainThread() try { - val result = head(url); + val result = head(url, HashMap(), modifier); if(result.isOk) return result.getHeadersFlat(); else @@ -141,12 +148,14 @@ open class ManagedHttpClient { return Socket(websocket); } - fun get(url : String, headers : MutableMap = HashMap()) : Response { - return execute(Request(url, "GET", null, headers)); + fun get(url : String, headers : MutableMap = HashMap(), modifier: IRequestModifier? = null) : Response { + val (finalUrl, finalHeaders) = applyModifier(url, headers, modifier) + return execute(Request(finalUrl, "GET", null, finalHeaders)); } - fun head(url : String, headers : MutableMap = HashMap()) : Response { - return execute(Request(url, "HEAD", null, headers)); + fun head(url : String, headers : MutableMap = HashMap(), modifier: IRequestModifier? = null) : Response { + val (finalUrl, finalHeaders) = applyModifier(url, headers, modifier) + return execute(Request(finalUrl, "HEAD", null, finalHeaders)); } fun post(url : String, headers : MutableMap = HashMap()) : Response { 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 ddcb5d4a..8082ffac 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -920,6 +920,7 @@ class StateCasting { if (videoSource != null) { _castServer.addHandlerWithAllowAllOptions( HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true) + .withIRequestModifier((videoSource as? JSSource)?.getRequestModifier()) .withInjectedHost() .withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); @@ -927,6 +928,7 @@ class StateCasting { if (audioSource != null) { _castServer.addHandlerWithAllowAllOptions( HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true) + .withIRequestModifier((audioSource as? JSSource)?.getRequestModifier()) .withInjectedHost() .withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); @@ -968,8 +970,7 @@ class StateCasting { val headers = masterContext.headers.clone() headers["Content-Type"] = "application/vnd.apple.mpegurl"; - val req = requestModifier?.modifyRequest(sourceUrl, mapOf()) - val masterPlaylistResponse = _client.get(req?.url ?: sourceUrl, (req?.headers ?: mapOf()).toMutableMap()) + val masterPlaylistResponse = _client.get(sourceUrl, mutableMapOf(), requestModifier) check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" } @@ -1022,7 +1023,7 @@ class StateCasting { val vpHeaders = vpContext.headers.clone() vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; - val response = _client.get(variantPlaylistRef.url) + val response = _client.get(variantPlaylistRef.url, mutableMapOf(), requestModifier) check(response.isOk) { "Failed to get variant playlist: ${response.code}" } val vpContent = response.body?.string() @@ -1059,7 +1060,7 @@ class StateCasting { val vpHeaders = vpContext.headers.clone() vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; - val response = _client.get(mediaRendition.uri) + val response = _client.get(mediaRendition.uri, mutableMapOf(), requestModifier) check(response.isOk) { "Failed to get variant playlist: ${response.code}" } val vpContent = response.body?.string() @@ -1190,6 +1191,7 @@ class StateCasting { _castServer.addHandlerWithAllowAllOptions( HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true) + .withIRequestModifier((audioSource as? JSSource)?.getRequestModifier()) .withInjectedHost() .withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castHlsIndirectVariant"); @@ -1267,6 +1269,7 @@ class StateCasting { _castServer.addHandlerWithAllowAllOptions( HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true) + .withIRequestModifier((videoSource as? JSSource)?.getRequestModifier()) .withInjectedHost() .withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castHlsIndirectVariant"); @@ -1350,6 +1353,7 @@ class StateCasting { if (videoSource != null) { _castServer.addHandlerWithAllowAllOptions( HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true) + .withIRequestModifier((videoSource as? JSSource)?.getRequestModifier()) .withInjectedHost() .withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); @@ -1357,6 +1361,7 @@ class StateCasting { if (audioSource != null) { _castServer.addHandlerWithAllowAllOptions( HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true) + .withIRequestModifier((audioSource as? JSSource)?.getRequestModifier()) .withInjectedHost() .withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index 0c974052..0e40cbe8 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -152,6 +152,18 @@ class VideoDownload { var hasVideoRequestModifier: Boolean = false; var hasAudioRequestModifier: Boolean = false; + // Transient: IRequestModifier is a runtime object from the JS plugin engine and cannot be + // serialized. After deserialization these are null - DownloadService must re-prepare to + // recapture them from the live plugin source (see needsReprepareForAuth). + @kotlinx.serialization.Transient + private var preparedVideoRequestModifier: IRequestModifier? = null; + @kotlinx.serialization.Transient + private var preparedAudioRequestModifier: IRequestModifier? = null; + + val needsReprepareForAuth: Boolean get() = + (hasVideoRequestModifier && preparedVideoRequestModifier == null && videoSourceLive == null) || + (hasAudioRequestModifier && preparedAudioRequestModifier == null && audioSourceLive == null); + var progress: Double = 0.0; var isCancelled = false; @@ -207,7 +219,7 @@ class VideoDownload { this.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch? this.requiredCheck = optionalSources; } - constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) { + constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?, videoModifier: IRequestModifier? = null, audioModifier: IRequestModifier? = null) { this.video = SerializedPlatformVideo.fromVideo(video); this.videoDetails = SerializedPlatformVideoDetails.fromVideo(video, if (subtitleSource != null) listOf(subtitleSource) else listOf()); this.videoSource = if(videoSource is IVideoUrlSource) VideoUrlSource.fromUrlSource(videoSource) else null; @@ -216,12 +228,22 @@ class VideoDownload { this.audioSourceLive = if(audioSource is JSSource) audioSource else null; this.subtitleSource = subtitleSource; this.prepareTime = OffsetDateTime.now(); + this.preparedVideoRequestModifier = videoModifier ?: (if (videoSource is JSSource && videoSource.hasRequestModifier) videoSource.getRequestModifier() else null); + this.preparedAudioRequestModifier = audioModifier ?: (if (audioSource is JSSource && audioSource.hasRequestModifier) audioSource.getRequestModifier() else null); this.hasVideoRequestExecutor = videoSource is JSSource && videoSource.hasRequestExecutor; this.hasAudioRequestExecutor = audioSource is JSSource && audioSource.hasRequestExecutor; - this.hasVideoRequestModifier = videoSource is JSSource && videoSource.hasRequestModifier; - this.hasAudioRequestModifier = audioSource is JSSource && audioSource.hasRequestModifier; - this.requiresLiveVideoSource = this.hasVideoRequestModifier || this.hasVideoRequestExecutor || (videoSource is JSDashManifestRawSource && videoSource.hasGenerate); - this.requiresLiveAudioSource = this.hasAudioRequestModifier || this.hasAudioRequestExecutor || (audioSource is JSDashManifestRawAudioSource && audioSource.hasGenerate); + // Set modifier flags from either the source or an explicitly provided modifier + // (e.g. from the HLS picker, where the source is an HLSVariant, not JSSource). + // These flags are serialized and used by needsReprepareForAuth after restore. + this.hasVideoRequestModifier = preparedVideoRequestModifier != null; + this.hasAudioRequestModifier = preparedAudioRequestModifier != null; + // requiresLiveVideoSource means a live JSSource is needed at download time (for executors + // or DASH generation). Modifiers alone don't require a live source - they're already + // captured in preparedVideoRequestModifier and recaptured via needsReprepareForAuth. + val sourceHasVideoModifier = videoSource is JSSource && videoSource.hasRequestModifier; + val sourceHasAudioModifier = audioSource is JSSource && audioSource.hasRequestModifier; + this.requiresLiveVideoSource = sourceHasVideoModifier || this.hasVideoRequestExecutor || (videoSource is JSDashManifestRawSource && videoSource.hasGenerate); + this.requiresLiveAudioSource = sourceHasAudioModifier || this.hasAudioRequestExecutor || (audioSource is JSDashManifestRawAudioSource && audioSource.hasGenerate); this.targetVideoName = videoSource?.name; this.targetAudioName = audioSource?.name; this.targetPixelCount = if(videoSource != null) (videoSource.width * videoSource.height).toLong() else null; @@ -317,8 +339,10 @@ class VideoDownload { val videoSources = arrayListOf() for (source in original.video.videoSources) { if (source is IHLSManifestSource) { + val sourceModifier = if (source is JSSource && source.hasRequestModifier) source.getRequestModifier() else null + if (sourceModifier != null) preparedVideoRequestModifier = sourceModifier try { - val playlistResponse = client.get(source.url) + val playlistResponse = client.get(source.url, HashMap(), sourceModifier) if (playlistResponse.isOk) { val resolvedPlaylistUrl = playlistResponse.url val playlistContent = playlistResponse.body?.string() @@ -345,6 +369,8 @@ class VideoDownload { if(vsource is JSSource) { this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || vsource.hasRequestExecutor; this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (vsource is JSDashManifestRawSource && vsource.hasGenerate); + if (vsource.hasRequestModifier && preparedVideoRequestModifier == null) + preparedVideoRequestModifier = vsource.getRequestModifier() } if(vsource == null) { @@ -366,8 +392,10 @@ class VideoDownload { if (video is VideoUnMuxedSourceDescriptor) { for (source in video.audioSources) { if (source is IHLSManifestAudioSource) { + val sourceModifier = if (source is JSSource && source.hasRequestModifier) source.getRequestModifier() else null + if (sourceModifier != null) preparedAudioRequestModifier = sourceModifier try { - val playlistResponse = client.get(source.url) + val playlistResponse = client.get(source.url, HashMap(), sourceModifier) if (playlistResponse.isOk) { val resolvedPlaylistUrl = playlistResponse.url val playlistContent = playlistResponse.body?.string() @@ -402,6 +430,8 @@ class VideoDownload { if(asource is JSSource) { this.hasAudioRequestExecutor = this.hasAudioRequestExecutor || asource.hasRequestExecutor; this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate); + if (asource.hasRequestModifier && preparedAudioRequestModifier == null) + preparedAudioRequestModifier = asource.getRequestModifier() } if(asource == null) { @@ -498,10 +528,16 @@ class VideoDownload { } } + val videoModifier = preparedVideoRequestModifier if(actualVideoSource is IVideoUrlSource) videoFileSize = when (videoSource!!.container) { - "application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, if (actualVideoSource is JSSource) actualVideoSource else null, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback) - else -> downloadFileSource("Video", client, if (actualVideoSource is JSSource) actualVideoSource else null, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback) + "application/vnd.apple.mpegurl" -> { + // HLS segments are concatenated into an MP4 file during download, + // so override the container for local playback/casting + videoOverrideContainer = "video/mp4"; + downloadHlsSource(context, "Video", client, videoModifier, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback) + } + else -> downloadFileSource("Video", client, videoModifier, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback) } else if(actualVideoSource is JSDashManifestRawSource) { if(actualAudioSource == null) @@ -542,10 +578,16 @@ class VideoDownload { } } + val audioModifier = preparedAudioRequestModifier if(actualAudioSource is IAudioUrlSource) audioFileSize = when (audioSource!!.container) { - "application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, if (actualAudioSource is JSSource) actualAudioSource else null, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback) - else -> downloadFileSource("Audio", client, if (actualAudioSource is JSSource) actualAudioSource else null, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback) + "application/vnd.apple.mpegurl" -> { + // HLS segments are concatenated into an MP4 file during download, + // so override the container for local playback/casting + audioOverrideContainer = "audio/mp4"; + downloadHlsSource(context, "Audio", client, audioModifier, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback) + } + else -> downloadFileSource("Audio", client, audioModifier, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback) } else if(actualAudioSource is JSDashManifestRawAudioSource) { audioFileSize = downloadDashFileSource("Audio", client, actualAudioSource, File(downloadDir, audioFileName!!), progressCallback, 2); @@ -659,15 +701,11 @@ class VideoDownload { } } - private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, source: JSSource?, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long { + private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, modifier: IRequestModifier?, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long { if (targetFile.exists()) targetFile.delete() var downloadedTotalLength = 0L - val modifier = if (source is JSSource && source.hasRequestModifier) - source.getRequestModifier() - else - null fun downloadBytes(url: String, rangeStart: Long? = null, rangeLength: Long? = null): ByteArray { val headers = mutableMapOf() @@ -681,17 +719,13 @@ class VideoDownload { } } - val modified = modifier?.modifyRequest(url, headers) - val finalUrl = modified?.url ?: url - val finalHeaders = modified?.headers?.toMutableMap() ?: headers - - val resp = client.get(finalUrl, finalHeaders) + val resp = client.get(url, headers, modifier) if (!resp.isOk) { resp.body?.close() - throw IllegalStateException("Failed to download HLS resource ($finalUrl): HTTP ${resp.code}") + throw IllegalStateException("Failed to download HLS resource ($url): HTTP ${resp.code}") } - val body = resp.body ?: throw IllegalStateException("Failed to download HLS resource ($finalUrl): Empty body") + val body = resp.body ?: throw IllegalStateException("Failed to download HLS resource ($url): Empty body") val bytes = body.bytes() body.close() return bytes @@ -706,12 +740,7 @@ class VideoDownload { val segmentFiles = arrayListOf() try { - val playlistHeaders = mutableMapOf() - val modifiedPlaylistReq = modifier?.modifyRequest(hlsUrl, playlistHeaders) - val playlistResp = client.get( - modifiedPlaylistReq?.url ?: hlsUrl, - modifiedPlaylistReq?.headers?.toMutableMap() ?: playlistHeaders - ) + val playlistResp = client.get(hlsUrl, mutableMapOf(), modifier) check(playlistResp.isOk) { "Failed to get variant playlist: ${playlistResp.code}" } @@ -960,16 +989,7 @@ class VideoDownload { Logger.i(TAG, "Downloading cue ${indexCounter}") val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString()); - val modified = modifier?.modifyRequest(url, mapOf()); - - val data = if(executor != null) - executor.executeRequest("GET", modified?.url ?: url, null, modified?.headers ?: mapOf()); - else { - val resp = client.get(modified?.url ?: url, modified?.headers?.toMutableMap() ?: mutableMapOf()); - if(!resp.isOk) - throw IllegalStateException("Dash request failed for index " + indexCounter.toString() + ", with code: " + resp.code.toString()); - resp.body!!.bytes() - } + val data = executeOrGet(client, executor, modifier, url) fileStream.write(data, 0, data.size); speedTracker.addWork(data.size.toLong()); written += data.size; @@ -989,16 +1009,7 @@ class VideoDownload { val t2 = cue2.groupValues[1]; val d2 = cue2.groupValues[2]; val url2 = foundTemplateUrl2!!.replace("\$Number\$", (index2).toString()); - val modified2 = modifier?.modifyRequest(url, mapOf()); - - val data = if(executor != null) - executor.executeRequest("GET", modified2?.url ?: url2, null, modified2?.headers ?: mapOf()); - else { - val resp = client.get(modified2?.url ?: url, modified2?.headers?.toMutableMap() ?: mutableMapOf()); - if(!resp.isOk) - throw IllegalStateException("Dash request2 failed for index " + indexCounter.toString() + ", with code: " + resp.code.toString()); - resp.body!!.bytes() - } + val data = executeOrGet(client, executor, modifier, url2) fileStream2.write(data, 0, data.size); speedTracker.addWork(data.size.toLong()); written2 += data.size; @@ -1067,7 +1078,7 @@ class VideoDownload { } } - private fun downloadFileSource(name: String, client: ManagedHttpClient, source: JSSource?, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long { + private fun downloadFileSource(name: String, client: ManagedHttpClient, modifier: IRequestModifier?, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long { if(targetFile.exists()) targetFile.delete(); @@ -1076,13 +1087,8 @@ class VideoDownload { val sourceLength: Long?; val fileStream = FileOutputStream(targetFile); - val modifier = if (source is JSSource && source.hasRequestModifier) - source.getRequestModifier(); - else - null; - try { - val head = client.tryHead(videoUrl); + val head = client.tryHead(videoUrl, modifier); val relatedPlugin = (video?.url ?: videoDetails?.url)?.let { StatePlatform.instance.getContentClient(it) }?.let { if(it is JSClient) it else null }; if(Settings.instance.downloads.byteRangeDownload && head?.containsKey("accept-ranges") == true && head.containsKey("content-length")) { @@ -1157,12 +1163,7 @@ class VideoDownload { var lastSpeed: Long = 0; - val result = if (modifier != null) { - val modified = modifier.modifyRequest(url, mapOf()) - client.get(modified.url!!, modified.headers.toMutableMap()) - } else { - client.get(url) - } + val result = client.get(url, HashMap(), modifier) if (!result.isOk) { result.body?.close() throw IllegalStateException("Failed to download source. Web[${result.code}] Error"); @@ -1375,13 +1376,12 @@ class VideoDownload { var lastException: Throwable? = null; val headers = mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}")); - val modified = modifier?.modifyRequest(url, headers); while (retryCount <= 3) { try { val toRead = rangeEnd - rangeStart; - val req = client.get(modified?.url ?: url, modified?.headers?.toMutableMap() ?: headers); + val req = client.get(url, headers.toMutableMap(), modifier); if (!req.isOk) { val bodyString = req.body?.string() req.body?.close() @@ -1512,6 +1512,18 @@ class VideoDownload { } } + private fun executeOrGet(client: ManagedHttpClient, executor: JSRequestExecutor?, modifier: IRequestModifier?, url: String, headers: Map = mapOf()): ByteArray { + if (executor != null) { + val modified = modifier?.modifyRequest(url, headers) + return executor.executeRequest("GET", modified?.url ?: url, null, modified?.headers ?: headers) + } else { + val resp = client.get(url, headers.toMutableMap(), modifier) + if (!resp.isOk) + throw IllegalStateException("Request failed for ($url) with code: ${resp.code}") + return resp.body!!.bytes() + } + } + companion object { const val TAG = "VideoDownload"; const val GROUP_PLAYLIST = "Playlist"; diff --git a/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt b/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt index 872caf83..7eededfd 100644 --- a/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt +++ b/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt @@ -231,6 +231,16 @@ class DownloadService : Service() { download.targetBitrate = download.audioSource!!.bitrate.toLong(); download.audioSource = null; } + // Force re-prepare if auth modifiers are needed but lost (e.g. after deserialization, + // since IRequestModifier is transient and cannot survive serialization). + // Must also clear sources so prepare() enters the source selection branches where + // modifiers are recaptured from the live plugin JSSource. + if(download.needsReprepareForAuth) { + Logger.w(TAG, "Video Download [${download.name}] needs re-prepare for auth modifiers"); + download.videoDetails = null; + download.videoSource = null; + download.audioSource = null; + } if(download.videoDetails == null || (!download.isVideoDownloadReady || !download.isAudioDownloadReady)) download.changeState(VideoDownload.State.PREPARING); notifyDownload(download); diff --git a/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt b/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt index d340d9ec..a4b15a44 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt @@ -10,6 +10,7 @@ import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.activities.IWithResultLauncher import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.media.PlatformID +import com.futo.platformplayer.api.media.models.modifier.IRequestModifier import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource @@ -341,8 +342,8 @@ class StateDownloads { fun download(video: IPlatformVideo, targetPixelcount: Long?, targetBitrate: Long?) { download(VideoDownload(video, targetPixelcount, targetBitrate)); } - fun download(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) { - download(VideoDownload(video, videoSource, audioSource, subtitleSource)); + fun download(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?, videoModifier: IRequestModifier? = null, audioModifier: IRequestModifier? = null) { + download(VideoDownload(video, videoSource, audioSource, subtitleSource, videoModifier, audioModifier)); } private fun download(videoState: VideoDownload, notify: Boolean = true) {