mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Improve request modifier support for casting and downloads
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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<String, String>? {
|
||||
private fun applyModifier(url: String, headers: MutableMap<String, String>, modifier: IRequestModifier?): Pair<String, MutableMap<String, String>> {
|
||||
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<String, String>? {
|
||||
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<String, String> = HashMap<String, String>()) : Response {
|
||||
return execute(Request(url, "GET", null, headers));
|
||||
fun get(url : String, headers : MutableMap<String, String> = HashMap<String, String>(), modifier: IRequestModifier? = null) : Response {
|
||||
val (finalUrl, finalHeaders) = applyModifier(url, headers, modifier)
|
||||
return execute(Request(finalUrl, "GET", null, finalHeaders));
|
||||
}
|
||||
|
||||
fun head(url : String, headers : MutableMap<String, String> = HashMap<String, String>()) : Response {
|
||||
return execute(Request(url, "HEAD", null, headers));
|
||||
fun head(url : String, headers : MutableMap<String, String> = HashMap<String, String>(), modifier: IRequestModifier? = null) : Response {
|
||||
val (finalUrl, finalHeaders) = applyModifier(url, headers, modifier)
|
||||
return execute(Request(finalUrl, "HEAD", null, finalHeaders));
|
||||
}
|
||||
|
||||
fun post(url : String, headers : MutableMap<String, String> = HashMap<String, String>()) : Response {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<IVideoSource>()
|
||||
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<String, String>()
|
||||
@@ -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<File>()
|
||||
try {
|
||||
val playlistHeaders = mutableMapOf<String, String>()
|
||||
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<String, String> = 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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user