DashManifestRaw support, RequestExecutor support, http binary body and response support, spec version support, ignore unsupported sources, webm container preference in settings

This commit is contained in:
Kelvin 2024-08-22 21:00:06 +02:00
commit 2941546ae4
17 changed files with 657 additions and 107 deletions

View file

@ -406,6 +406,39 @@ class DashSource {
this.requestModifier = obj.requestModifier; this.requestModifier = obj.requestModifier;
} }
} }
class DashManifestRawSource {
constructor(obj) {
obj = obj ?? {};
this.plugin_type = "DashRawSource";
this.name = obj.name ?? "";
this.bitrate = obj.bitrate ?? 0;
this.container = obj.container ?? "";
this.codec = obj.codec ?? "";
this.duration = obj.duration ?? 0;
this.url = obj.url;
this.language = obj.language ?? Language.UNKNOWN;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
}
}
class DashManifestRawAudioSource {
constructor(obj) {
obj = obj ?? {};
this.plugin_type = "DashRawAudioSource";
this.name = obj.name ?? "";
this.bitrate = obj.bitrate ?? 0;
this.container = obj.container ?? "";
this.codec = obj.codec ?? "";
this.duration = obj.duration ?? 0;
this.url = obj.url;
this.language = obj.language ?? Language.UNKNOWN;
this.manifest = obj.manifest ?? null;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
}
}
class RequestModifier { class RequestModifier {
constructor(obj) { constructor(obj) {

View file

@ -454,6 +454,12 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13) @FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
var fullscreenPortrait: Boolean = false; var fullscreenPortrait: Boolean = false;
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 14)
var preferWebmVideo: Boolean = false;
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 15)
var preferWebmAudio: Boolean = false;
} }
@FormField(R.string.comments, "group", R.string.comments_description, 6) @FormField(R.string.comments, "group", R.string.comments_description, 6)

View file

@ -0,0 +1,7 @@
package com.futo.platformplayer.api.media.platforms.js
class JSClientConstants {
companion object {
val PLUGIN_SPEC_VERSION = 2;
}
}

View file

@ -54,4 +54,8 @@ open class JSContent : IPlatformContent, IPluginSourced {
_hasGetDetails = _content.has("getDetails"); _hasGetDetails = _content.has("getDetails");
} }
fun getUnderlyingObject(): V8ValueObject? {
return _content;
}
} }

View file

@ -2,13 +2,20 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.primitive.V8ValueUndefined
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValueTypedArray import com.caoccao.javet.values.reference.V8ValueTypedArray
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.Base64 import java.util.Base64
@ -16,6 +23,9 @@ class JSRequestExecutor {
private val _plugin: JSClient; private val _plugin: JSClient;
private val _config: IV8PluginConfig; private val _config: IV8PluginConfig;
private var _executor: V8ValueObject; private var _executor: V8ValueObject;
val urlPrefix: String?;
private val hasCleanup: Boolean;
constructor(plugin: JSClient, executor: V8ValueObject) { constructor(plugin: JSClient, executor: V8ValueObject) {
this._plugin = plugin; this._plugin = plugin;
@ -23,16 +33,30 @@ class JSRequestExecutor {
this._config = plugin.config; this._config = plugin.config;
val config = plugin.config; val config = plugin.config;
urlPrefix = executor.getOrDefault(config, "urlPrefix", "RequestExecutor", null);
if(!executor.has("executeRequest")) if(!executor.has("executeRequest"))
throw ScriptImplementationException(config, "RequestExecutor is missing executeRequest", null); throw ScriptImplementationException(config, "RequestExecutor is missing executeRequest", null);
hasCleanup = executor.has("cleanup");
} }
//TODO: Executor properties? //TODO: Executor properties?
fun executeRequest(url: String, headers: Map<String, String>): ByteArray { @Throws(ScriptException::class)
open fun executeRequest(url: String, headers: Map<String, String>): ByteArray {
if (_executor.isClosed) if (_executor.isClosed)
throw IllegalStateException("Executor object is closed"); throw IllegalStateException("Executor object is closed");
val result = V8Plugin.catchScriptErrors<Any>( val result = if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invoke("executeRequest", url, headers);
} as V8Value;
}
else V8Plugin.catchScriptErrors<Any>(
_config, _config,
"[${_config.name}] JSRequestExecutor", "[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()" "builder.modifyRequest()"
@ -41,22 +65,63 @@ class JSRequestExecutor {
} as V8Value; } as V8Value;
try { try {
if(result is V8ValueString) if(result is V8ValueString) {
return Base64.getDecoder().decode(result.value); val base64Result = Base64.getDecoder().decode(result.value);
if(result is V8ValueTypedArray) return base64Result;
return result.toBytes(); }
if(result is V8ValueTypedArray) {
val buffer = result.buffer;
val byteBuffer = buffer.byteBuffer;
val bytesResult = ByteArray(result.byteLength);
byteBuffer.get(bytesResult, 0, result.byteLength);
buffer.close();
return bytesResult;
}
if(result is V8ValueObject && result.has("type")) { if(result is V8ValueObject && result.has("type")) {
val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier"); val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier");
when(type) { when(type) {
//TODO: Buffer type? //TODO: Buffer type?
} }
} }
throw NotImplementedError("Executor result type not implemented?"); if(result is V8ValueUndefined) {
if(_plugin is DevJSClient)
StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined");
throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null);
}
throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name);
} }
finally { finally {
result.close(); result.close();
} }
} }
open fun cleanup() {
if (!hasCleanup || _executor.isClosed)
return;
if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invokeVoid("cleanup", null);
};
}
else V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invokeVoid("cleanup", null);
};
}
protected fun finalize() {
cleanup();
}
} }
//TODO: are these available..? //TODO: are these available..?

View file

@ -1,23 +1,64 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.others.Language
import com.futo.platformplayer.states.StateDeveloper
class JSDashManifestRawAudioSource : JSSource { class JSDashManifestRawAudioSource : JSSource, IAudioSource {
val container : String = "application/dash+xml"; override val container : String = "application/dash+xml";
val name : String; override val name : String;
val manifest: String; override val codec: String;
override val bitrate: Int;
override val duration: Long;
override val priority: Boolean;
override val language: String;
val url: String;
var manifest: String?;
val hasGenerate: Boolean;
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
val contextName = "DashSource"; val contextName = "DashRawSource";
val config = plugin.config; val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName); name = _obj.getOrThrow(config, "name", contextName);
url = _obj.getOrThrow(config, "url", contextName);
manifest = _obj.getOrThrow(config, "manifest", contextName); manifest = _obj.getOrThrow(config, "manifest", contextName);
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
hasGenerate = _obj.has("generate");
}
fun generate(): String? {
if(!hasGenerate)
return manifest;
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
val plugin = _plugin.getUnderlyingPlugin();
if(_plugin is DevJSClient)
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_obj.invokeString("generate");
}
}
else
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_obj.invokeString("generate");
}
} }
} }

View file

@ -1,23 +1,109 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources package com.futo.platformplayer.api.media.platforms.js.models.sources
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.states.StateDeveloper
class JSDashManifestRawSource : JSSource { open class JSDashManifestRawSource: JSSource, IVideoSource {
val container : String = "application/dash+xml"; override val container : String = "application/dash+xml";
val name : String; override val name : String;
val manifest: String; override val width: Int;
override val height: Int;
override val codec: String;
override val bitrate: Int?;
override val duration: Long;
override val priority: Boolean;
var url: String?;
var manifest: String?;
val hasGenerate: Boolean;
val canMerge: Boolean;
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
val contextName = "DashSource"; val contextName = "DashRawSource";
val config = plugin.config; val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName); name = _obj.getOrThrow(config, "name", contextName);
manifest = _obj.getOrThrow(config, "manifest", contextName); url = _obj.getOrThrow(config, "url", contextName);
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
canMerge = _obj.getOrDefault(config, "canMerge", contextName, false) ?: false;
hasGenerate = _obj.has("generate");
}
open fun generate(): String? {
if(!hasGenerate)
return manifest;
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
if(_plugin is DevJSClient) {
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_obj.invokeString("generate");
});
}
}
else
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_obj.invokeString("generate");
});
}
}
class JSDashManifestMergingRawSource(
val video: JSDashManifestRawSource,
val audio: JSDashManifestRawAudioSource): JSDashManifestRawSource(video.getUnderlyingPlugin()!!, video.getUnderlyingObject()!!), IVideoSource {
override val name: String
get() = video.name;
override val bitrate: Int
get() = (video.bitrate ?: 0) + audio.bitrate;
override val codec: String
get() = video.codec
override val container: String
get() = video.container
override val duration: Long
get() = video.duration;
override val height: Int
get() = video.height;
override val width: Int
get() = video.width;
override val priority: Boolean
get() = video.priority;
override fun generate(): String? {
val videoDash = video.generate();
val audioDash = audio.generate();
if(videoDash != null && audioDash == null) return videoDash;
if(audioDash != null && videoDash == null) return audioDash;
if(videoDash == null) return null;
//TODO: Temporary simple solution..make more reliable version
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
if(audioAdaptationSet != null) {
return videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
}
else
return videoDash;
}
companion object {
private val adaptationSetRegex = Regex("<AdaptationSet.*?>.*?<\\/AdaptationSet>", RegexOption.DOT_MATCHES_ALL);
} }
} }

View file

@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.orNull import com.futo.platformplayer.orNull
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
@ -70,12 +71,12 @@ abstract class JSSource {
return JSRequestModifier(_plugin, result) return JSRequestModifier(_plugin, result)
} }
fun getRequestExecutor(): JSRequestExecutor? { open fun getRequestExecutor(): JSRequestExecutor? {
if (!hasRequestExecutor || _obj.isClosed) if (!hasRequestExecutor || _obj.isClosed)
return null; return null;
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") { val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") {
_obj.invoke("getRequestModifier", arrayOf<Any>()); _obj.invoke("getRequestExecutor", arrayOf<Any>());
}; };
if (result !is V8ValueObject) if (result !is V8ValueObject)
@ -84,40 +85,58 @@ abstract class JSSource {
return JSRequestExecutor(_plugin, result) return JSRequestExecutor(_plugin, result)
} }
fun getUnderlyingPlugin(): JSClient? {
return _plugin;
}
fun getUnderlyingObject(): V8ValueObject? {
return _obj;
}
companion object { companion object {
const val TYPE_AUDIOURL = "AudioUrlSource"; const val TYPE_AUDIOURL = "AudioUrlSource";
const val TYPE_VIDEOURL = "VideoUrlSource"; const val TYPE_VIDEOURL = "VideoUrlSource";
const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource"; const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource";
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource"; const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
const val TYPE_DASH = "DashSource"; const val TYPE_DASH = "DashSource";
const val TYPE_DASH_RAW = "DashSourceRaw"; const val TYPE_DASH_RAW = "DashRawSource";
const val TYPE_DASH_RAW_AUDIO = "DashRawAudioSource";
const val TYPE_HLS = "HLSSource"; const val TYPE_HLS = "HLSSource";
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource" const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) }; fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource { fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? {
val type = obj.getString("plugin_type"); val type = obj.getString("plugin_type");
return when(type) { return when(type) {
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj); TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj); TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
TYPE_HLS -> fromV8HLS(plugin, obj); TYPE_HLS -> fromV8HLS(plugin, obj);
TYPE_DASH -> fromV8Dash(plugin, obj); TYPE_DASH -> fromV8Dash(plugin, obj);
else -> throw NotImplementedError("Unknown type ${type}"); TYPE_DASH_RAW -> fromV8DashRaw(plugin, obj);
else -> {
Logger.w("JSSource", "Unknown video type ${type}");
null;
};
} }
} }
fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) }; fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) };
fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj); fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj);
fun fromV8DashRaw(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawSource = JSDashManifestRawSource(plugin, obj);
fun fromV8DashRawAudio(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawAudioSource = JSDashManifestRawAudioSource(plugin, obj);
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) }; fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj); fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj);
fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource { fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource? {
val type = obj.getString("plugin_type"); val type = obj.getString("plugin_type");
return when(type) { return when(type) {
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj); TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj); TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
TYPE_DASH_RAW_AUDIO -> fromV8DashRawAudio(plugin, obj);
TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj); TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj);
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj); TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
else -> throw NotImplementedError("Unknown type ${type}"); else -> {
Logger.w("JSSource", "Unknown audio type ${type}");
null;
};
} }
} }
} }

View file

@ -23,9 +23,11 @@ class JSUnMuxVideoSourceDescriptor: VideoUnMuxedSourceDescriptor {
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName); this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray() this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) } .map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
.filterNotNull()
.toTypedArray(); .toTypedArray();
this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray() this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray()
.map { JSSource.fromV8Audio(plugin, it as V8ValueObject) } .map { JSSource.fromV8Audio(plugin, it as V8ValueObject) }
.filterNotNull()
.toTypedArray(); .toTypedArray();
} }
} }

View file

@ -21,6 +21,7 @@ class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName); this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray() this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) } .map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
.filterNotNull()
.toTypedArray(); .toTypedArray();
} }

View file

@ -25,10 +25,8 @@ import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.google.gson.ExclusionStrategy import com.google.gson.ExclusionStrategy
import com.google.gson.FieldAttributes import com.google.gson.FieldAttributes
import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonParser import com.google.gson.JsonParser
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -573,7 +571,7 @@ class DeveloperEndpoints(private val context: Context) {
val resp = _client.get(body.url!!, body.headers); val resp = _client.get(body.url!!, body.headers);
context.respondCode(200, context.respondCode(200,
Json.encodeToString(PackageHttp.BridgeHttpResponse(resp.url, resp.code, resp.body?.string())), Json.encodeToString(PackageHttp.BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string())),
context.query.getOrDefault("CT", "text/plain")); context.query.getOrDefault("CT", "text/plain"));
} }
catch(ex: Exception) { catch(ex: Exception) {

View file

@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.packages
import com.caoccao.javet.annotations.V8Function import com.caoccao.javet.annotations.V8Function
import com.caoccao.javet.annotations.V8Property import com.caoccao.javet.annotations.V8Property
import com.caoccao.javet.values.V8Value
import com.futo.platformplayer.BuildConfig import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
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.JSClientConstants
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
@ -49,6 +51,16 @@ class PackageBridge : V8Package {
fun buildFlavor(): String { fun buildFlavor(): String {
return BuildConfig.FLAVOR; return BuildConfig.FLAVOR;
} }
@V8Property
fun buildSpecVersion(): Int {
return JSClientConstants.PLUGIN_SPEC_VERSION;
}
@V8Function
fun dispose(value: V8Value) {
Logger.e(TAG, "Manual dispose: " + value.javaClass.name);
value.close();
}
@V8Function @V8Function
fun toast(str: String) { fun toast(str: String) {

View file

@ -7,7 +7,11 @@ import com.caoccao.javet.enums.V8ConversionMode
import com.caoccao.javet.enums.V8ProxyMode import com.caoccao.javet.enums.V8ProxyMode
import com.caoccao.javet.interop.V8Runtime import com.caoccao.javet.interop.V8Runtime
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueArrayBuffer
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValueSharedArrayBuffer
import com.caoccao.javet.values.reference.V8ValueTypedArray
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
@ -16,6 +20,9 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.internal.IV8Convertable import com.futo.platformplayer.engine.internal.IV8Convertable
import com.futo.platformplayer.engine.internal.V8BindObject import com.futo.platformplayer.engine.internal.V8BindObject
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import kotlin.streams.asSequence import kotlin.streams.asSequence
@ -64,33 +71,42 @@ class PackageHttp: V8Package {
} }
@V8Function @V8Function
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse { fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
return if(useAuth) return if(useAuth)
_packageClientAuth.request(method, url, headers) _packageClientAuth.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
else else
_packageClient.request(method, url, headers); _packageClient.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
} }
@V8Function @V8Function
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse { fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
return if(useAuth) return if(useAuth)
_packageClientAuth.requestWithBody(method, url, body, headers) _packageClientAuth.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
else else
_packageClient.requestWithBody(method, url, body, headers); _packageClient.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
} }
@V8Function @V8Function
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse { fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
return if(useAuth) return if(useAuth)
_packageClientAuth.GET(url, headers) _packageClientAuth.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING)
else else
_packageClient.GET(url, headers); _packageClient.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
} }
@V8Function @V8Function
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse { fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
return if(useAuth)
_packageClientAuth.POST(url, body, headers) val client = if(useAuth) _packageClientAuth else _packageClient;
if(body is V8ValueString)
return client.POST(url, body.value, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else if(body is V8ValueTypedArray)
return client.POST(url, body.toBytes(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else if(body is ByteArray)
return client.POST(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else if(body is ArrayList<*>) //Avoid this case, used purely for testing
return client.POST(url, body.map { (it as Double).toInt().toByte() }.toByteArray(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
else else
_packageClient.POST(url, body, headers); throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST");
} }
@V8Function @V8Function
@ -111,8 +127,19 @@ class PackageHttp: V8Package {
} }
} }
interface IBridgeHttpResponse {
val url: String;
val code: Int;
val headers: Map<String, List<String>>?;
}
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
class BridgeHttpResponse(val url: String, val code: Int, val body: String?, val headers: Map<String, List<String>>? = null) : IV8Convertable { class BridgeHttpStringResponse(
override val url: String,
override val code: Int, val
body: String?,
override val headers: Map<String, List<String>>? = null) : IV8Convertable, IBridgeHttpResponse {
val isOk = code >= 200 && code < 300; val isOk = code >= 200 && code < 300;
override fun toV8(runtime: V8Runtime): V8Value? { override fun toV8(runtime: V8Runtime): V8Value? {
@ -125,6 +152,37 @@ class PackageHttp: V8Package {
return obj; return obj;
} }
} }
@kotlinx.serialization.Serializable
class BridgeHttpBytesResponse: IV8Convertable, IBridgeHttpResponse {
override val url: String;
override val code: Int;
val body: ByteArray?;
override val headers: Map<String, List<String>>?;
val isOk: Boolean;
constructor(url: String, code: Int, body: ByteArray? = null, headers: Map<String, List<String>>? = null) {
this.url = url;
this.code = code;
this.body = body;
this.headers = headers;
this.isOk = code >= 200 && code < 300;
}
override fun toV8(runtime: V8Runtime): V8Value? {
val obj = runtime.createV8ValueObject();
obj.set("url", url);
obj.set("code", code);
if(body != null) {
val buffer = runtime.createV8ValueArrayBuffer(body.size);
buffer.fromBytes(body);
obj.set("body", body);
}
obj.set("headers", headers);
obj.set("isOk", isOk);
return obj;
}
}
//TODO: This object is currently re-wrapped each modification, this is due to an issue passing the same object back and forth, should be fixed in future. //TODO: This object is currently re-wrapped each modification, this is due to an issue passing the same object back and forth, should be fixed in future.
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class) @V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
@ -147,6 +205,12 @@ class PackageHttp: V8Package {
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
= clientPOST(_package.getDefaultClient(useAuth), url, body, headers); = clientPOST(_package.getDefaultClient(useAuth), url, body, headers);
@V8Function
fun DUMMY(): BatchBuilder {
_reqs.add(Pair(_package.getDefaultClient(false), RequestDescriptor("DUMMY", "", mutableMapOf())));
return BatchBuilder(_package, _reqs);
}
//Client-specific //Client-specific
@V8Function @V8Function
@ -169,12 +233,14 @@ class PackageHttp: V8Package {
//Finalizer //Finalizer
@V8Function @V8Function
fun execute(): List<BridgeHttpResponse> { fun execute(): List<IBridgeHttpResponse?> {
return _reqs.parallelStream().map { return _reqs.parallelStream().map {
if(it.second.method == "DUMMY")
return@map null;
if(it.second.body != null) if(it.second.body != null)
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers); return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers, it.second.respType);
else else
return@map it.first.request(it.second.method, it.second.url, it.second.headers); return@map it.first.request(it.second.method, it.second.url, it.second.headers, it.second.respType);
} }
.asSequence() .asSequence()
.toList(); .toList();
@ -232,63 +298,108 @@ class PackageHttp: V8Package {
} }
@V8Function @V8Function
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse { fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
applyDefaultHeaders(headers); applyDefaultHeaders(headers);
return logExceptions { return logExceptions {
return@logExceptions catchHttp { return@logExceptions catchHttp {
val client = _client; val client = _client;
//logRequest(method, url, headers, null); //logRequest(method, url, headers, null);
val resp = client.requestMethod(method, url, headers); val resp = client.requestMethod(method, url, headers);
val responseBody = resp.body?.string();
//logResponse(method, url, resp.code, resp.headers, responseBody); //logResponse(method, url, resp.code, resp.headers, responseBody);
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers, return@catchHttp when(returnType) {
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess)); _client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
}
} }
}; };
} }
@V8Function @V8Function
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse { fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
applyDefaultHeaders(headers); applyDefaultHeaders(headers);
return logExceptions { return logExceptions {
catchHttp { catchHttp {
val client = _client; val client = _client;
//logRequest(method, url, headers, body); //logRequest(method, url, headers, body);
val resp = client.requestMethod(method, url, body, headers); val resp = client.requestMethod(method, url, body, headers);
val responseBody = resp.body?.string();
//logResponse(method, url, resp.code, resp.headers, responseBody); //logResponse(method, url, resp.code, resp.headers, responseBody);
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
return@catchHttp when(returnType) {
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess)); _client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
}
} }
}; };
} }
@V8Function @V8Function
fun GET(url: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse { fun GET(url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
applyDefaultHeaders(headers); applyDefaultHeaders(headers);
return logExceptions { return logExceptions {
catchHttp { catchHttp {
val client = _client; val client = _client;
//logRequest("GET", url, headers, null); //logRequest("GET", url, headers, null);
val resp = client.get(url, headers); val resp = client.get(url, headers);
val responseBody = resp.body?.string(); //val responseBody = resp.body?.string();
//logResponse("GET", url, resp.code, resp.headers, responseBody); //logResponse("GET", url, resp.code, resp.headers, responseBody);
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
return@catchHttp when(returnType) {
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess)); _client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
}
} }
}; };
} }
@V8Function @V8Function
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse { fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
applyDefaultHeaders(headers); applyDefaultHeaders(headers);
return logExceptions { return logExceptions {
catchHttp { catchHttp {
val client = _client; val client = _client;
//logRequest("POST", url, headers, body); //logRequest("POST", url, headers, body);
val resp = client.post(url, body, headers); val resp = client.post(url, body, headers);
val responseBody = resp.body?.string(); //val responseBody = resp.body?.string();
//logResponse("POST", url, resp.code, resp.headers, responseBody); //logResponse("POST", url, resp.code, resp.headers, responseBody);
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
return@catchHttp when(returnType) {
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess)); _client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
}
}
};
}
@V8Function
fun POST(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
applyDefaultHeaders(headers);
return logExceptions {
catchHttp {
val client = _client;
//logRequest("POST", url, headers, body);
val resp = client.post(url, body, headers);
//val responseBody = resp.body?.string();
//logResponse("POST", url, resp.code, resp.headers, responseBody);
return@catchHttp when(returnType) {
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
}
} }
}; };
} }
@ -388,13 +499,13 @@ class PackageHttp: V8Package {
} }
} }
private fun catchHttp(handle: ()->BridgeHttpResponse): BridgeHttpResponse { private fun catchHttp(handle: ()->IBridgeHttpResponse): IBridgeHttpResponse {
try{ try{
return handle(); return handle();
} }
//Forward timeouts //Forward timeouts
catch(ex: SocketTimeoutException) { catch(ex: SocketTimeoutException) {
return BridgeHttpResponse("", 408, null); return BridgeHttpStringResponse("", 408, null);
} }
} }
} }
@ -514,20 +625,25 @@ class PackageHttp: V8Package {
val url: String, val url: String,
val headers: MutableMap<String, String>, val headers: MutableMap<String, String>,
val body: String? = null, val body: String? = null,
val contentType: String? = null val contentType: String? = null,
val respType: ReturnType = ReturnType.STRING
) )
private fun catchHttp(handle: ()->BridgeHttpResponse): BridgeHttpResponse { private fun catchHttp(handle: ()->BridgeHttpStringResponse): BridgeHttpStringResponse {
try{ try{
return handle(); return handle();
} }
//Forward timeouts //Forward timeouts
catch(ex: SocketTimeoutException) { catch(ex: SocketTimeoutException) {
return BridgeHttpResponse("", 408, null); return BridgeHttpStringResponse("", 408, null);
} }
} }
enum class ReturnType(val value: Int) {
STRING(0),
BYTES(1);
}
companion object { companion object {
private const val TAG = "PackageHttp"; private const val TAG = "PackageHttp";

View file

@ -72,6 +72,7 @@ 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.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.casting.CastConnectionState import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.casting.StateCasting
@ -693,6 +694,7 @@ class VideoDetailView : ConstraintLayout {
_lastAudioSource = null; _lastAudioSource = null;
_lastSubtitleSource = null; _lastSubtitleSource = null;
video = null; video = null;
_player.clear();
cleanupPlaybackTracker(); cleanupPlaybackTracker();
Logger.i(TAG, "Keep screen on unset onClose") Logger.i(TAG, "Keep screen on unset onClose")
fragment.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); fragment.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

View file

@ -3,9 +3,14 @@ package com.futo.platformplayer.views.video
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Xml
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.fragment.app.findFragment
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.C.Encoding
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
@ -17,6 +22,8 @@ import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource import androidx.media3.datasource.HttpDataSource
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.dash.DashMediaSource import androidx.media3.exoplayer.dash.DashMediaSource
import androidx.media3.exoplayer.dash.manifest.DashManifest
import androidx.media3.exoplayer.dash.manifest.DashManifestParser
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider
import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.hls.HlsMediaSource
import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.MediaSource
@ -42,6 +49,9 @@ import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestMergingRawSource
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.JSHLSManifestAudioSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
@ -52,12 +62,14 @@ import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.video.PlayerManager import com.futo.platformplayer.video.PlayerManager
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
import com.google.gson.Gson import com.google.gson.Gson
import getHttpDataSourceFactory import getHttpDataSourceFactory
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
import java.io.File import java.io.File
import kotlin.math.abs import kotlin.math.abs
@ -319,17 +331,30 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
swapSources(videoSource, audioSource,false, play, keepSubtitles); swapSources(videoSource, audioSource,false, play, keepSubtitles);
} }
fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean { fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean {
swapSourceInternal(videoSource); var videoSourceUsed = videoSource;
swapSourceInternal(audioSource); var audioSourceUsed = audioSource;
if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){
videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource);
audioSourceUsed = null;
}
swapSourceInternal(videoSourceUsed);
swapSourceInternal(audioSourceUsed);
if(!keepSubtitles) if(!keepSubtitles)
_lastSubtitleMediaSource = null; _lastSubtitleMediaSource = null;
return loadSelectedSources(play, resume); return loadSelectedSources(play, resume);
} }
fun swapSource(videoSource: IVideoSource?, resume: Boolean = true, play: Boolean = true): Boolean { fun swapSource(videoSource: IVideoSource?, resume: Boolean = true, play: Boolean = true): Boolean {
swapSourceInternal(videoSource); var videoSourceUsed = videoSource;
if(videoSource is JSDashManifestRawSource && lastVideoSource is JSDashManifestMergingRawSource)
videoSourceUsed = JSDashManifestMergingRawSource(videoSource, (lastVideoSource as JSDashManifestMergingRawSource).audio);
swapSourceInternal(videoSourceUsed);
return loadSelectedSources(play, resume); return loadSelectedSources(play, resume);
} }
fun swapSource(audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true): Boolean { fun swapSource(audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true): Boolean {
if(audioSource is JSDashManifestRawAudioSource && lastVideoSource is JSDashManifestMergingRawSource)
swapSourceInternal(JSDashManifestMergingRawSource((lastVideoSource as JSDashManifestMergingRawSource).video, audioSource));
else
swapSourceInternal(audioSource); swapSourceInternal(audioSource);
return loadSelectedSources(play, resume); return loadSelectedSources(play, resume);
} }
@ -387,6 +412,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
is LocalVideoSource -> swapVideoSourceLocal(videoSource); is LocalVideoSource -> swapVideoSourceLocal(videoSource);
is JSVideoUrlRangeSource -> swapVideoSourceUrlRange(videoSource); is JSVideoUrlRangeSource -> swapVideoSourceUrlRange(videoSource);
is IDashManifestSource -> swapVideoSourceDash(videoSource); is IDashManifestSource -> swapVideoSourceDash(videoSource);
is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource);
is IHLSManifestSource -> swapVideoSourceHLS(videoSource); is IHLSManifestSource -> swapVideoSourceHLS(videoSource);
is IVideoUrlSource -> swapVideoSourceUrl(videoSource); is IVideoUrlSource -> swapVideoSourceUrl(videoSource);
null -> _lastVideoMediaSource = null; null -> _lastVideoMediaSource = null;
@ -399,6 +425,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
is LocalAudioSource -> swapAudioSourceLocal(audioSource); is LocalAudioSource -> swapAudioSourceLocal(audioSource);
is JSAudioUrlRangeSource -> swapAudioSourceUrlRange(audioSource); is JSAudioUrlRangeSource -> swapAudioSourceUrlRange(audioSource);
is JSHLSManifestAudioSource -> swapAudioSourceHLS(audioSource); is JSHLSManifestAudioSource -> swapAudioSourceHLS(audioSource);
is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource);
is IAudioUrlWidevineSource -> swapAudioSourceUrlWidevine(audioSource) is IAudioUrlWidevineSource -> swapAudioSourceUrlWidevine(audioSource)
is IAudioUrlSource -> swapAudioSourceUrl(audioSource); is IAudioUrlSource -> swapAudioSourceUrl(audioSource);
null -> _lastAudioMediaSource = null; null -> _lastAudioMediaSource = null;
@ -459,6 +486,54 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
.createMediaSource(MediaItem.fromUri(videoSource.url)) .createMediaSource(MediaItem.fromUri(videoSource.url))
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun swapVideoSourceDashRaw(videoSource: JSDashManifestRawSource) {
Logger.i(TAG, "Loading VideoSource [Dash]");
if(videoSource.hasGenerate) {
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
try {
val generated = videoSource.generate();
if (generated != null) {
withContext(Dispatchers.Main) {
val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource))
videoSource.getHttpDataSourceFactory()
else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
if(dataSource is JSHttpDataSource.Factory && videoSource is JSDashManifestMergingRawSource)
dataSource.setRequestExecutor2(videoSource.audio.getRequestExecutor());
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
.createMediaSource(
DashManifestParser().parse(
Uri.parse(videoSource.url),
ByteArrayInputStream(
generated?.toByteArray() ?: ByteArray(0)
)
)
);
loadSelectedSources(true, false);
}
}
}
catch(ex: Throwable) {
Logger.e(TAG, "DashRaw generator failed", ex);
}
}
}
else {
val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource))
videoSource.getHttpDataSourceFactory()
else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
if(dataSource is JSHttpDataSource.Factory && videoSource is JSDashManifestMergingRawSource)
dataSource.setRequestExecutor2(videoSource.audio.getRequestExecutor());
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
.createMediaSource(DashManifestParser().parse(Uri.parse(videoSource.url),
ByteArrayInputStream(videoSource.manifest?.toByteArray() ?: ByteArray(0))));
}
}
@OptIn(UnstableApi::class)
private fun swapVideoSourceHLS(videoSource: IHLSManifestSource) { private fun swapVideoSourceHLS(videoSource: IHLSManifestSource) {
Logger.i(TAG, "Loading VideoSource [HLS]"); Logger.i(TAG, "Loading VideoSource [HLS]");
val dataSource = if(videoSource is JSSource && videoSource.requiresCustomDatasource) val dataSource = if(videoSource is JSSource && videoSource.requiresCustomDatasource)
@ -521,6 +596,31 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
.createMediaSource(MediaItem.fromUri(audioSource.url)); .createMediaSource(MediaItem.fromUri(audioSource.url));
} }
@OptIn(UnstableApi::class)
private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource) {
Logger.i(TAG, "Loading AudioSource [DashRaw]");
val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource))
audioSource.getHttpDataSourceFactory()
else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
if(audioSource.hasGenerate) {
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
val generated = audioSource.generate();
if(generated != null) {
withContext(Dispatchers.Main) {
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
.createMediaSource(DashManifestParser().parse(Uri.parse(audioSource.url),
ByteArrayInputStream(generated?.toByteArray() ?: ByteArray(0))));
loadSelectedSources(true, false);
}
}
}
}
else
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
.createMediaSource(DashManifestParser().parse(Uri.parse(audioSource.url),
ByteArrayInputStream(audioSource.manifest?.toByteArray() ?: ByteArray(0))));
}
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun swapAudioSourceUrlWidevine(audioSource: IAudioUrlWidevineSource) { private fun swapAudioSourceUrlWidevine(audioSource: IAudioUrlWidevineSource) {
Logger.i(TAG, "Loading AudioSource [UrlWidevine]") Logger.i(TAG, "Loading AudioSource [UrlWidevine]")
@ -574,28 +674,37 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
val sourceAudio = _lastAudioMediaSource; val sourceAudio = _lastAudioMediaSource;
val sourceSubs = _lastSubtitleMediaSource; val sourceSubs = _lastSubtitleMediaSource;
val sources = listOf(sourceVideo, sourceAudio, sourceSubs).filter { it != null }.map { it!! }.toTypedArray()
beforeSourceChanged(); beforeSourceChanged();
_mediaSource = if(sources.size == 1) { val source = mergeMediaSources(sourceVideo, sourceAudio, sourceSubs);
Logger.i(TAG, "Using single source mode") if(source == null)
(sourceVideo ?: sourceAudio);
}
else if(sources.size > 1) {
Logger.i(TAG, "Using multi source mode ${sources.size}")
MergingMediaSource(true, *sources);
}
else {
Logger.i(TAG, "Using no sources loaded");
stop();
return false; return false;
} _mediaSource = source;
reloadMediaSource(play, resume); reloadMediaSource(play, resume);
return true; return true;
} }
@OptIn(UnstableApi::class)
fun mergeMediaSources(sourceVideo: MediaSource?, sourceAudio: MediaSource?, sourceSubs: MediaSource?): MediaSource? {
val sources = listOf(sourceVideo, sourceAudio, sourceSubs).filter { it != null }.map { it!! }.toTypedArray()
if(sources.size == 1) {
Logger.i(TAG, "Using single source mode")
return (sourceVideo ?: sourceAudio);
}
else if(sources.size > 1) {
Logger.i(TAG, "Using multi source mode ${sources.size}")
return MergingMediaSource(true, *sources);
}
else {
Logger.i(TAG, "Using no sources loaded");
stop();
return null;
}
}
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun reloadMediaSource(play: Boolean = false, resume: Boolean = true) { private fun reloadMediaSource(play: Boolean = false, resume: Boolean = true) {
val player = exoPlayer ?: return val player = exoPlayer ?: return
@ -619,6 +728,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
fun clear() { fun clear() {
exoPlayer?.player?.stop(); exoPlayer?.player?.stop();
exoPlayer?.player?.clearMediaItems(); exoPlayer?.player?.clearMediaItems();
_lastVideoMediaSource = null;
_lastAudioMediaSource = null;
_lastSubtitleMediaSource = null;
_mediaSource = null;
} }
fun stop(){ fun stop(){
@ -697,8 +810,15 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
companion object { companion object {
val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"; val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
val PREFERED_VIDEO_CONTAINERS = arrayOf("video/mp4", "video/webm", "video/3gpp"); val PREFERED_VIDEO_CONTAINERS_MP4Pref = arrayOf("video/mp4", "video/webm", "video/3gpp");
val PREFERED_AUDIO_CONTAINERS = arrayOf("audio/mp3", "audio/mp4", "audio/webm", "audio/opus"); val PREFERED_VIDEO_CONTAINERS_WEBMPref = arrayOf("video/webm", "video/mp4", "video/3gpp");
val PREFERED_VIDEO_CONTAINERS: Array<String> get() { return if(Settings.instance.playback.preferWebmVideo)
PREFERED_VIDEO_CONTAINERS_WEBMPref else PREFERED_VIDEO_CONTAINERS_MP4Pref }
val PREFERED_AUDIO_CONTAINERS_MP4Pref = arrayOf("audio/mp3", "audio/mp4", "audio/webm", "audio/opus");
val PREFERED_AUDIO_CONTAINERS_WEBMPref = arrayOf("audio/webm", "audio/opus", "audio/mp3", "audio/mp4");
val PREFERED_AUDIO_CONTAINERS: Array<String> get() { return if(Settings.instance.playback.preferWebmAudio)
PREFERED_AUDIO_CONTAINERS_WEBMPref else PREFERED_AUDIO_CONTAINERS_MP4Pref }
val SUPPORTED_SUBTITLES = hashSetOf("text/vtt", "application/x-subrip"); val SUPPORTED_SUBTITLES = hashSetOf("text/vtt", "application/x-subrip");
} }

View file

@ -13,6 +13,7 @@ import androidx.annotation.VisibleForTesting;
import com.futo.platformplayer.api.media.models.modifier.IRequest; import com.futo.platformplayer.api.media.models.modifier.IRequest;
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier; import com.futo.platformplayer.api.media.models.modifier.IRequestModifier;
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest;
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor; import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor;
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier; import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier;
import androidx.media3.common.C; import androidx.media3.common.C;
@ -27,6 +28,8 @@ import androidx.media3.datasource.HttpUtil;
import androidx.media3.datasource.TransferListener; import androidx.media3.datasource.TransferListener;
import com.futo.platformplayer.engine.dev.V8RemoteObject; import com.futo.platformplayer.engine.dev.V8RemoteObject;
import com.futo.platformplayer.engine.exceptions.PluginException;
import com.futo.platformplayer.engine.exceptions.ScriptException;
import com.futo.platformplayer.logging.Logger; import com.futo.platformplayer.logging.Logger;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.ForwardingMap; import com.google.common.collect.ForwardingMap;
@ -70,7 +73,9 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
private boolean allowCrossProtocolRedirects; private boolean allowCrossProtocolRedirects;
private boolean keepPostFor302Redirects; private boolean keepPostFor302Redirects;
@Nullable private IRequestModifier requestModifier = null; @Nullable private IRequestModifier requestModifier = null;
@Nullable private JSRequestExecutor requestExecutor = null; @Nullable public JSRequestExecutor requestExecutor = null;
@Nullable public JSRequestExecutor requestExecutor2 = null;
/** Creates an instance. */ /** Creates an instance. */
public Factory() { public Factory() {
@ -109,6 +114,18 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
this.requestExecutor = requestExecutor; this.requestExecutor = requestExecutor;
return this; return this;
} }
/**
* Sets the secondary request executor that will be used.
*
* <p>The default is {@code null}, which results in no request modification
*
* @param requestExecutor The request modifier that will be used, or {@code null} to use no request modifier
* @return This factory.
*/
public Factory setRequestExecutor2(@Nullable JSRequestExecutor requestExecutor) {
this.requestExecutor2 = requestExecutor;
return this;
}
/** /**
* Sets the user agent that will be used. * Sets the user agent that will be used.
@ -216,7 +233,8 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
contentTypePredicate, contentTypePredicate,
keepPostFor302Redirects, keepPostFor302Redirects,
requestModifier, requestModifier,
requestExecutor); requestExecutor,
requestExecutor2);
if (transferListener != null) { if (transferListener != null) {
dataSource.addTransferListener(transferListener); dataSource.addTransferListener(transferListener);
} }
@ -252,7 +270,10 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
private long bytesToRead; private long bytesToRead;
private long bytesRead; private long bytesRead;
@Nullable private IRequestModifier requestModifier; @Nullable private IRequestModifier requestModifier;
@Nullable private JSRequestExecutor requestExecutor; @Nullable public JSRequestExecutor requestExecutor;
@Nullable public JSRequestExecutor requestExecutor2; //Not ideal, but required for now to have 2 executors under 1 datasource
private Uri fallbackUri = null;
private JSHttpDataSource( private JSHttpDataSource(
@Nullable String userAgent, @Nullable String userAgent,
@ -263,7 +284,8 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
@Nullable Predicate<String> contentTypePredicate, @Nullable Predicate<String> contentTypePredicate,
boolean keepPostFor302Redirects, boolean keepPostFor302Redirects,
@Nullable IRequestModifier requestModifier, @Nullable IRequestModifier requestModifier,
@Nullable JSRequestExecutor requestExecutor) { @Nullable JSRequestExecutor requestExecutor,
@Nullable JSRequestExecutor requestExecutor2) {
super(/* isNetwork= */ true); super(/* isNetwork= */ true);
this.userAgent = userAgent; this.userAgent = userAgent;
this.connectTimeoutMillis = connectTimeoutMillis; this.connectTimeoutMillis = connectTimeoutMillis;
@ -275,12 +297,13 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
this.keepPostFor302Redirects = keepPostFor302Redirects; this.keepPostFor302Redirects = keepPostFor302Redirects;
this.requestModifier = requestModifier; this.requestModifier = requestModifier;
this.requestExecutor = requestExecutor; this.requestExecutor = requestExecutor;
this.requestExecutor2 = requestExecutor2;
} }
@Override @Override
@Nullable @Nullable
public Uri getUri() { public Uri getUri() {
return connection == null ? null : Uri.parse(connection.getURL().toString()); return connection == null ? fallbackUri : Uri.parse(connection.getURL().toString());
} }
@Override @Override
@ -330,19 +353,30 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
bytesToRead = 0; bytesToRead = 0;
transferInitializing(dataSpec); transferInitializing(dataSpec);
if(requestExecutor != null) { //Use executor 2 if it matches the urlPrefix
byte[] data = requestExecutor.executeRequest(dataSpec.uri.toString(), dataSpec.httpRequestHeaders); JSRequestExecutor executor = (requestExecutor2 != null && requestExecutor2.getUrlPrefix() != null && dataSpec.uri.toString().startsWith(requestExecutor2.getUrlPrefix())) ?
if(data == null) requestExecutor2 : requestExecutor;
if(executor != null) {
try {
byte[] data = executor.executeRequest(dataSpec.uri.toString(), dataSpec.httpRequestHeaders);
if (data == null)
throw new HttpDataSourceException( throw new HttpDataSourceException(
"No response", "No response",
dataSpec, dataSpec,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED, PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_OPEN); HttpDataSourceException.TYPE_OPEN);
inputStream = new ByteArrayInputStream(data); inputStream = new ByteArrayInputStream(data);
fallbackUri = dataSpec.uri;
bytesToRead = data.length;
transferStarted(dataSpec); transferStarted(dataSpec);
return data.length; return data.length;
} }
catch(PluginException ex) {
throw HttpDataSourceException.createForIOException(new IOException("Executor failed: " + ex.getMessage(), ex), dataSpec, HttpDataSourceException.TYPE_OPEN);
}
}
else { else {
String responseMessage; String responseMessage;
HttpURLConnection connection; HttpURLConnection connection;

View file

@ -373,6 +373,10 @@
<string name="system_volume_descr">Gesture controls adjust system volume</string> <string name="system_volume_descr">Gesture controls adjust system volume</string>
<string name="live_chat_webview">Live Chat Webview</string> <string name="live_chat_webview">Live Chat Webview</string>
<string name="full_screen_portrait">Fullscreen portrait</string> <string name="full_screen_portrait">Fullscreen portrait</string>
<string name="prefer_webm">Prefer Webm Video Codecs</string>
<string name="prefer_webm_description">If player should prefer Webm codecs (vp9/opus) over mp4 codecs (h264/AAC), may result in worse compatibility.</string>
<string name="prefer_webm_audio">Prefer Webm Audio Codecs</string>
<string name="prefer_webm_audio_description">If player should prefer Webm codecs (opus) over mp4 codecs (AAC), may result in worse compatibility.</string>
<string name="allow_full_screen_portrait">Allow fullscreen portrait</string> <string name="allow_full_screen_portrait">Allow fullscreen portrait</string>
<string name="background_switch_audio">Switch to Audio in Background</string> <string name="background_switch_audio">Switch to Audio in Background</string>
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string> <string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>