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.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
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.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.JSDashManifestRawAudioSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
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)
|
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
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}" }
|
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
||||||
|
|
||||||
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||||
@@ -515,7 +517,7 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
slideUpMenuOverlay.onOK.subscribe {
|
slideUpMenuOverlay.onOK.subscribe {
|
||||||
//TODO: Fix SubtitleRawSource issue
|
//TODO: Fix SubtitleRawSource issue
|
||||||
StateDownloads.instance.download(video, selectedVideoVariant, selectedAudioVariant, null);
|
StateDownloads.instance.download(video, selectedVideoVariant, selectedAudioVariant, null, videoModifier = modifier, audioModifier = modifier);
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,11 +528,11 @@ class UISlideOverlays {
|
|||||||
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
|
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (source is IHLSManifestSource) {
|
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")
|
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else if (source is IHLSManifestAudioSource) {
|
} 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")
|
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import java.time.Duration
|
|||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.TrustManager
|
import javax.net.ssl.TrustManager
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
|
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
open class ManagedHttpClient {
|
open class ManagedHttpClient {
|
||||||
@@ -89,10 +90,16 @@ open class ManagedHttpClient {
|
|||||||
return clonedClient;
|
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()
|
ensureNotMainThread()
|
||||||
try {
|
try {
|
||||||
val result = head(url);
|
val result = head(url, HashMap(), modifier);
|
||||||
if(result.isOk)
|
if(result.isOk)
|
||||||
return result.getHeadersFlat();
|
return result.getHeadersFlat();
|
||||||
else
|
else
|
||||||
@@ -141,12 +148,14 @@ open class ManagedHttpClient {
|
|||||||
return Socket(websocket);
|
return Socket(websocket);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get(url : String, headers : MutableMap<String, String> = HashMap<String, String>()) : Response {
|
fun get(url : String, headers : MutableMap<String, String> = HashMap<String, String>(), modifier: IRequestModifier? = null) : Response {
|
||||||
return execute(Request(url, "GET", null, headers));
|
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 {
|
fun head(url : String, headers : MutableMap<String, String> = HashMap<String, String>(), modifier: IRequestModifier? = null) : Response {
|
||||||
return execute(Request(url, "HEAD", null, headers));
|
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 {
|
fun post(url : String, headers : MutableMap<String, String> = HashMap<String, String>()) : Response {
|
||||||
|
|||||||
@@ -920,6 +920,7 @@ class StateCasting {
|
|||||||
if (videoSource != null) {
|
if (videoSource != null) {
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
||||||
|
.withIRequestModifier((videoSource as? JSSource)?.getRequestModifier())
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
@@ -927,6 +928,7 @@ class StateCasting {
|
|||||||
if (audioSource != null) {
|
if (audioSource != null) {
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
||||||
|
.withIRequestModifier((audioSource as? JSSource)?.getRequestModifier())
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
@@ -968,8 +970,7 @@ class StateCasting {
|
|||||||
val headers = masterContext.headers.clone()
|
val headers = masterContext.headers.clone()
|
||||||
headers["Content-Type"] = "application/vnd.apple.mpegurl";
|
headers["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||||
|
|
||||||
val req = requestModifier?.modifyRequest(sourceUrl, mapOf())
|
val masterPlaylistResponse = _client.get(sourceUrl, mutableMapOf(), requestModifier)
|
||||||
val masterPlaylistResponse = _client.get(req?.url ?: sourceUrl, (req?.headers ?: mapOf()).toMutableMap())
|
|
||||||
|
|
||||||
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
||||||
|
|
||||||
@@ -1022,7 +1023,7 @@ class StateCasting {
|
|||||||
val vpHeaders = vpContext.headers.clone()
|
val vpHeaders = vpContext.headers.clone()
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
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}" }
|
check(response.isOk) { "Failed to get variant playlist: ${response.code}" }
|
||||||
|
|
||||||
val vpContent = response.body?.string()
|
val vpContent = response.body?.string()
|
||||||
@@ -1059,7 +1060,7 @@ class StateCasting {
|
|||||||
val vpHeaders = vpContext.headers.clone()
|
val vpHeaders = vpContext.headers.clone()
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
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}" }
|
check(response.isOk) { "Failed to get variant playlist: ${response.code}" }
|
||||||
|
|
||||||
val vpContent = response.body?.string()
|
val vpContent = response.body?.string()
|
||||||
@@ -1190,6 +1191,7 @@ class StateCasting {
|
|||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
||||||
|
.withIRequestModifier((audioSource as? JSSource)?.getRequestModifier())
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castHlsIndirectVariant");
|
).withTag("castHlsIndirectVariant");
|
||||||
@@ -1267,6 +1269,7 @@ class StateCasting {
|
|||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
||||||
|
.withIRequestModifier((videoSource as? JSSource)?.getRequestModifier())
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("castHlsIndirectVariant");
|
).withTag("castHlsIndirectVariant");
|
||||||
@@ -1350,6 +1353,7 @@ class StateCasting {
|
|||||||
if (videoSource != null) {
|
if (videoSource != null) {
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)
|
||||||
|
.withIRequestModifier((videoSource as? JSSource)?.getRequestModifier())
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
@@ -1357,6 +1361,7 @@ class StateCasting {
|
|||||||
if (audioSource != null) {
|
if (audioSource != null) {
|
||||||
_castServer.addHandlerWithAllowAllOptions(
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true)
|
||||||
|
.withIRequestModifier((audioSource as? JSSource)?.getRequestModifier())
|
||||||
.withInjectedHost()
|
.withInjectedHost()
|
||||||
.withHeader("Access-Control-Allow-Origin", "*"), true
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
).withTag("cast");
|
).withTag("cast");
|
||||||
|
|||||||
@@ -152,6 +152,18 @@ class VideoDownload {
|
|||||||
var hasVideoRequestModifier: Boolean = false;
|
var hasVideoRequestModifier: Boolean = false;
|
||||||
var hasAudioRequestModifier: 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 progress: Double = 0.0;
|
||||||
var isCancelled = false;
|
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.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch?
|
||||||
this.requiredCheck = optionalSources;
|
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.video = SerializedPlatformVideo.fromVideo(video);
|
||||||
this.videoDetails = SerializedPlatformVideoDetails.fromVideo(video, if (subtitleSource != null) listOf(subtitleSource) else listOf());
|
this.videoDetails = SerializedPlatformVideoDetails.fromVideo(video, if (subtitleSource != null) listOf(subtitleSource) else listOf());
|
||||||
this.videoSource = if(videoSource is IVideoUrlSource) VideoUrlSource.fromUrlSource(videoSource) else null;
|
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.audioSourceLive = if(audioSource is JSSource) audioSource else null;
|
||||||
this.subtitleSource = subtitleSource;
|
this.subtitleSource = subtitleSource;
|
||||||
this.prepareTime = OffsetDateTime.now();
|
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.hasVideoRequestExecutor = videoSource is JSSource && videoSource.hasRequestExecutor;
|
||||||
this.hasAudioRequestExecutor = audioSource is JSSource && audioSource.hasRequestExecutor;
|
this.hasAudioRequestExecutor = audioSource is JSSource && audioSource.hasRequestExecutor;
|
||||||
this.hasVideoRequestModifier = videoSource is JSSource && videoSource.hasRequestModifier;
|
// Set modifier flags from either the source or an explicitly provided modifier
|
||||||
this.hasAudioRequestModifier = audioSource is JSSource && audioSource.hasRequestModifier;
|
// (e.g. from the HLS picker, where the source is an HLSVariant, not JSSource).
|
||||||
this.requiresLiveVideoSource = this.hasVideoRequestModifier || this.hasVideoRequestExecutor || (videoSource is JSDashManifestRawSource && videoSource.hasGenerate);
|
// These flags are serialized and used by needsReprepareForAuth after restore.
|
||||||
this.requiresLiveAudioSource = this.hasAudioRequestModifier || this.hasAudioRequestExecutor || (audioSource is JSDashManifestRawAudioSource && audioSource.hasGenerate);
|
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.targetVideoName = videoSource?.name;
|
||||||
this.targetAudioName = audioSource?.name;
|
this.targetAudioName = audioSource?.name;
|
||||||
this.targetPixelCount = if(videoSource != null) (videoSource.width * videoSource.height).toLong() else null;
|
this.targetPixelCount = if(videoSource != null) (videoSource.width * videoSource.height).toLong() else null;
|
||||||
@@ -317,8 +339,10 @@ class VideoDownload {
|
|||||||
val videoSources = arrayListOf<IVideoSource>()
|
val videoSources = arrayListOf<IVideoSource>()
|
||||||
for (source in original.video.videoSources) {
|
for (source in original.video.videoSources) {
|
||||||
if (source is IHLSManifestSource) {
|
if (source is IHLSManifestSource) {
|
||||||
|
val sourceModifier = if (source is JSSource && source.hasRequestModifier) source.getRequestModifier() else null
|
||||||
|
if (sourceModifier != null) preparedVideoRequestModifier = sourceModifier
|
||||||
try {
|
try {
|
||||||
val playlistResponse = client.get(source.url)
|
val playlistResponse = client.get(source.url, HashMap(), sourceModifier)
|
||||||
if (playlistResponse.isOk) {
|
if (playlistResponse.isOk) {
|
||||||
val resolvedPlaylistUrl = playlistResponse.url
|
val resolvedPlaylistUrl = playlistResponse.url
|
||||||
val playlistContent = playlistResponse.body?.string()
|
val playlistContent = playlistResponse.body?.string()
|
||||||
@@ -345,6 +369,8 @@ class VideoDownload {
|
|||||||
if(vsource is JSSource) {
|
if(vsource is JSSource) {
|
||||||
this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || vsource.hasRequestExecutor;
|
this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || vsource.hasRequestExecutor;
|
||||||
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (vsource is JSDashManifestRawSource && vsource.hasGenerate);
|
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (vsource is JSDashManifestRawSource && vsource.hasGenerate);
|
||||||
|
if (vsource.hasRequestModifier && preparedVideoRequestModifier == null)
|
||||||
|
preparedVideoRequestModifier = vsource.getRequestModifier()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(vsource == null) {
|
if(vsource == null) {
|
||||||
@@ -366,8 +392,10 @@ class VideoDownload {
|
|||||||
if (video is VideoUnMuxedSourceDescriptor) {
|
if (video is VideoUnMuxedSourceDescriptor) {
|
||||||
for (source in video.audioSources) {
|
for (source in video.audioSources) {
|
||||||
if (source is IHLSManifestAudioSource) {
|
if (source is IHLSManifestAudioSource) {
|
||||||
|
val sourceModifier = if (source is JSSource && source.hasRequestModifier) source.getRequestModifier() else null
|
||||||
|
if (sourceModifier != null) preparedAudioRequestModifier = sourceModifier
|
||||||
try {
|
try {
|
||||||
val playlistResponse = client.get(source.url)
|
val playlistResponse = client.get(source.url, HashMap(), sourceModifier)
|
||||||
if (playlistResponse.isOk) {
|
if (playlistResponse.isOk) {
|
||||||
val resolvedPlaylistUrl = playlistResponse.url
|
val resolvedPlaylistUrl = playlistResponse.url
|
||||||
val playlistContent = playlistResponse.body?.string()
|
val playlistContent = playlistResponse.body?.string()
|
||||||
@@ -402,6 +430,8 @@ class VideoDownload {
|
|||||||
if(asource is JSSource) {
|
if(asource is JSSource) {
|
||||||
this.hasAudioRequestExecutor = this.hasAudioRequestExecutor || asource.hasRequestExecutor;
|
this.hasAudioRequestExecutor = this.hasAudioRequestExecutor || asource.hasRequestExecutor;
|
||||||
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
||||||
|
if (asource.hasRequestModifier && preparedAudioRequestModifier == null)
|
||||||
|
preparedAudioRequestModifier = asource.getRequestModifier()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(asource == null) {
|
if(asource == null) {
|
||||||
@@ -498,10 +528,16 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val videoModifier = preparedVideoRequestModifier
|
||||||
if(actualVideoSource is IVideoUrlSource)
|
if(actualVideoSource is IVideoUrlSource)
|
||||||
videoFileSize = when (videoSource!!.container) {
|
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)
|
"application/vnd.apple.mpegurl" -> {
|
||||||
else -> downloadFileSource("Video", client, if (actualVideoSource is JSSource) actualVideoSource else null, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
// 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) {
|
else if(actualVideoSource is JSDashManifestRawSource) {
|
||||||
if(actualAudioSource == null)
|
if(actualAudioSource == null)
|
||||||
@@ -542,10 +578,16 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val audioModifier = preparedAudioRequestModifier
|
||||||
if(actualAudioSource is IAudioUrlSource)
|
if(actualAudioSource is IAudioUrlSource)
|
||||||
audioFileSize = when (audioSource!!.container) {
|
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)
|
"application/vnd.apple.mpegurl" -> {
|
||||||
else -> downloadFileSource("Audio", client, if (actualAudioSource is JSSource) actualAudioSource else null, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
// 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) {
|
else if(actualAudioSource is JSDashManifestRawAudioSource) {
|
||||||
audioFileSize = downloadDashFileSource("Audio", client, actualAudioSource, File(downloadDir, audioFileName!!), progressCallback, 2);
|
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())
|
if (targetFile.exists())
|
||||||
targetFile.delete()
|
targetFile.delete()
|
||||||
|
|
||||||
var downloadedTotalLength = 0L
|
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 {
|
fun downloadBytes(url: String, rangeStart: Long? = null, rangeLength: Long? = null): ByteArray {
|
||||||
val headers = mutableMapOf<String, String>()
|
val headers = mutableMapOf<String, String>()
|
||||||
@@ -681,17 +719,13 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val modified = modifier?.modifyRequest(url, headers)
|
val resp = client.get(url, headers, modifier)
|
||||||
val finalUrl = modified?.url ?: url
|
|
||||||
val finalHeaders = modified?.headers?.toMutableMap() ?: headers
|
|
||||||
|
|
||||||
val resp = client.get(finalUrl, finalHeaders)
|
|
||||||
if (!resp.isOk) {
|
if (!resp.isOk) {
|
||||||
resp.body?.close()
|
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()
|
val bytes = body.bytes()
|
||||||
body.close()
|
body.close()
|
||||||
return bytes
|
return bytes
|
||||||
@@ -706,12 +740,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
val segmentFiles = arrayListOf<File>()
|
val segmentFiles = arrayListOf<File>()
|
||||||
try {
|
try {
|
||||||
val playlistHeaders = mutableMapOf<String, String>()
|
val playlistResp = client.get(hlsUrl, mutableMapOf(), modifier)
|
||||||
val modifiedPlaylistReq = modifier?.modifyRequest(hlsUrl, playlistHeaders)
|
|
||||||
val playlistResp = client.get(
|
|
||||||
modifiedPlaylistReq?.url ?: hlsUrl,
|
|
||||||
modifiedPlaylistReq?.headers?.toMutableMap() ?: playlistHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
check(playlistResp.isOk) { "Failed to get variant playlist: ${playlistResp.code}" }
|
check(playlistResp.isOk) { "Failed to get variant playlist: ${playlistResp.code}" }
|
||||||
|
|
||||||
@@ -960,16 +989,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
Logger.i(TAG, "Downloading cue ${indexCounter}")
|
Logger.i(TAG, "Downloading cue ${indexCounter}")
|
||||||
val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString());
|
val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString());
|
||||||
val modified = modifier?.modifyRequest(url, mapOf());
|
val data = executeOrGet(client, executor, modifier, url)
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
fileStream.write(data, 0, data.size);
|
fileStream.write(data, 0, data.size);
|
||||||
speedTracker.addWork(data.size.toLong());
|
speedTracker.addWork(data.size.toLong());
|
||||||
written += data.size;
|
written += data.size;
|
||||||
@@ -989,16 +1009,7 @@ class VideoDownload {
|
|||||||
val t2 = cue2.groupValues[1];
|
val t2 = cue2.groupValues[1];
|
||||||
val d2 = cue2.groupValues[2];
|
val d2 = cue2.groupValues[2];
|
||||||
val url2 = foundTemplateUrl2!!.replace("\$Number\$", (index2).toString());
|
val url2 = foundTemplateUrl2!!.replace("\$Number\$", (index2).toString());
|
||||||
val modified2 = modifier?.modifyRequest(url, mapOf());
|
val data = executeOrGet(client, executor, modifier, url2)
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
fileStream2.write(data, 0, data.size);
|
fileStream2.write(data, 0, data.size);
|
||||||
speedTracker.addWork(data.size.toLong());
|
speedTracker.addWork(data.size.toLong());
|
||||||
written2 += data.size;
|
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())
|
if(targetFile.exists())
|
||||||
targetFile.delete();
|
targetFile.delete();
|
||||||
|
|
||||||
@@ -1076,13 +1087,8 @@ class VideoDownload {
|
|||||||
val sourceLength: Long?;
|
val sourceLength: Long?;
|
||||||
val fileStream = FileOutputStream(targetFile);
|
val fileStream = FileOutputStream(targetFile);
|
||||||
|
|
||||||
val modifier = if (source is JSSource && source.hasRequestModifier)
|
|
||||||
source.getRequestModifier();
|
|
||||||
else
|
|
||||||
null;
|
|
||||||
|
|
||||||
try {
|
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 };
|
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"))
|
if(Settings.instance.downloads.byteRangeDownload && head?.containsKey("accept-ranges") == true && head.containsKey("content-length"))
|
||||||
{
|
{
|
||||||
@@ -1157,12 +1163,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
var lastSpeed: Long = 0;
|
var lastSpeed: Long = 0;
|
||||||
|
|
||||||
val result = if (modifier != null) {
|
val result = client.get(url, HashMap(), modifier)
|
||||||
val modified = modifier.modifyRequest(url, mapOf())
|
|
||||||
client.get(modified.url!!, modified.headers.toMutableMap())
|
|
||||||
} else {
|
|
||||||
client.get(url)
|
|
||||||
}
|
|
||||||
if (!result.isOk) {
|
if (!result.isOk) {
|
||||||
result.body?.close()
|
result.body?.close()
|
||||||
throw IllegalStateException("Failed to download source. Web[${result.code}] Error");
|
throw IllegalStateException("Failed to download source. Web[${result.code}] Error");
|
||||||
@@ -1375,13 +1376,12 @@ class VideoDownload {
|
|||||||
var lastException: Throwable? = null;
|
var lastException: Throwable? = null;
|
||||||
|
|
||||||
val headers = mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}"));
|
val headers = mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}"));
|
||||||
val modified = modifier?.modifyRequest(url, headers);
|
|
||||||
|
|
||||||
while (retryCount <= 3) {
|
while (retryCount <= 3) {
|
||||||
try {
|
try {
|
||||||
val toRead = rangeEnd - rangeStart;
|
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) {
|
if (!req.isOk) {
|
||||||
val bodyString = req.body?.string()
|
val bodyString = req.body?.string()
|
||||||
req.body?.close()
|
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 {
|
companion object {
|
||||||
const val TAG = "VideoDownload";
|
const val TAG = "VideoDownload";
|
||||||
const val GROUP_PLAYLIST = "Playlist";
|
const val GROUP_PLAYLIST = "Playlist";
|
||||||
|
|||||||
@@ -231,6 +231,16 @@ class DownloadService : Service() {
|
|||||||
download.targetBitrate = download.audioSource!!.bitrate.toLong();
|
download.targetBitrate = download.audioSource!!.bitrate.toLong();
|
||||||
download.audioSource = null;
|
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))
|
if(download.videoDetails == null || (!download.isVideoDownloadReady || !download.isAudioDownloadReady))
|
||||||
download.changeState(VideoDownload.State.PREPARING);
|
download.changeState(VideoDownload.State.PREPARING);
|
||||||
notifyDownload(download);
|
notifyDownload(download);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.futo.platformplayer.UIDialogs
|
|||||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
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.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
@@ -341,8 +342,8 @@ class StateDownloads {
|
|||||||
fun download(video: IPlatformVideo, targetPixelcount: Long?, targetBitrate: Long?) {
|
fun download(video: IPlatformVideo, targetPixelcount: Long?, targetBitrate: Long?) {
|
||||||
download(VideoDownload(video, targetPixelcount, targetBitrate));
|
download(VideoDownload(video, targetPixelcount, targetBitrate));
|
||||||
}
|
}
|
||||||
fun download(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
fun download(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?, videoModifier: IRequestModifier? = null, audioModifier: IRequestModifier? = null) {
|
||||||
download(VideoDownload(video, videoSource, audioSource, subtitleSource));
|
download(VideoDownload(video, videoSource, audioSource, subtitleSource, videoModifier, audioModifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun download(videoState: VideoDownload, notify: Boolean = true) {
|
private fun download(videoState: VideoDownload, notify: Boolean = true) {
|
||||||
|
|||||||
Reference in New Issue
Block a user