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
parent 55781e2b34
commit 2941546ae4
17 changed files with 657 additions and 107 deletions

View file

@ -406,6 +406,39 @@ class DashSource {
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 {
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)
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)

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");
}
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.primitive.V8ValueString
import com.caoccao.javet.values.primitive.V8ValueUndefined
import com.caoccao.javet.values.reference.V8ValueObject
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.engine.IV8PluginConfig
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.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.serialization.Serializable
import java.util.Base64
@ -16,6 +23,9 @@ class JSRequestExecutor {
private val _plugin: JSClient;
private val _config: IV8PluginConfig;
private var _executor: V8ValueObject;
val urlPrefix: String?;
private val hasCleanup: Boolean;
constructor(plugin: JSClient, executor: V8ValueObject) {
this._plugin = plugin;
@ -23,40 +33,95 @@ class JSRequestExecutor {
this._config = plugin.config;
val config = plugin.config;
urlPrefix = executor.getOrDefault(config, "urlPrefix", "RequestExecutor", null);
if(!executor.has("executeRequest"))
throw ScriptImplementationException(config, "RequestExecutor is missing executeRequest", null);
hasCleanup = executor.has("cleanup");
}
//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)
throw IllegalStateException("Executor object is closed");
val result = V8Plugin.catchScriptErrors<Any>(
_config,
"[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invoke("executeRequest", url, headers);
} as V8Value;
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.name}] JSRequestExecutor",
"builder.modifyRequest()"
) {
_executor.invoke("executeRequest", url, headers);
} as V8Value;
try {
if(result is V8ValueString)
return Base64.getDecoder().decode(result.value);
if(result is V8ValueTypedArray)
return result.toBytes();
if(result is V8ValueString) {
val base64Result = Base64.getDecoder().decode(result.value);
return base64Result;
}
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")) {
val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier");
when(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 {
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..?

View file

@ -1,23 +1,64 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
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.IVideoUrlSource
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.others.Language
import com.futo.platformplayer.states.StateDeveloper
class JSDashManifestRawAudioSource : JSSource {
val container : String = "application/dash+xml";
val name : String;
val manifest: String;
class JSDashManifestRawAudioSource : JSSource, IAudioSource {
override val container : String = "application/dash+xml";
override val name : 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) {
val contextName = "DashSource";
val contextName = "DashRawSource";
val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName);
url = _obj.getOrThrow(config, "url", 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
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueString
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.IVideoSource
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.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.states.StateDeveloper
class JSDashManifestRawSource : JSSource {
val container : String = "application/dash+xml";
val name : String;
val manifest: String;
open class JSDashManifestRawSource: JSSource, IVideoSource {
override val container : String = "application/dash+xml";
override val name : 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) {
val contextName = "DashSource";
val contextName = "DashRawSource";
val config = plugin.config;
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.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.orNull
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
@ -70,12 +71,12 @@ abstract class JSSource {
return JSRequestModifier(_plugin, result)
}
fun getRequestExecutor(): JSRequestExecutor? {
open fun getRequestExecutor(): JSRequestExecutor? {
if (!hasRequestExecutor || _obj.isClosed)
return null;
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)
@ -84,40 +85,58 @@ abstract class JSSource {
return JSRequestExecutor(_plugin, result)
}
fun getUnderlyingPlugin(): JSClient? {
return _plugin;
}
fun getUnderlyingObject(): V8ValueObject? {
return _obj;
}
companion object {
const val TYPE_AUDIOURL = "AudioUrlSource";
const val TYPE_VIDEOURL = "VideoUrlSource";
const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource";
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
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_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
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");
return when(type) {
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
TYPE_HLS -> fromV8HLS(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 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 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");
return when(type) {
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
TYPE_DASH_RAW_AUDIO -> fromV8DashRawAudio(plugin, obj);
TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(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.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
.filterNotNull()
.toTypedArray();
this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray()
.map { JSSource.fromV8Audio(plugin, it as V8ValueObject) }
.filterNotNull()
.toTypedArray();
}
}

View file

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

View file

@ -25,10 +25,8 @@ import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.states.StatePlatform
import com.google.gson.ExclusionStrategy
import com.google.gson.FieldAttributes
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonParser
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@ -573,7 +571,7 @@ class DeveloperEndpoints(private val context: Context) {
val resp = _client.get(body.url!!, body.headers);
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"));
}
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.V8Property
import com.caoccao.javet.values.V8Value
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
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.internal.JSHttpClient
import com.futo.platformplayer.engine.IV8PluginConfig
@ -49,6 +51,16 @@ class PackageBridge : V8Package {
fun buildFlavor(): String {
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
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.interop.V8Runtime
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.V8ValueSharedArrayBuffer
import com.caoccao.javet.values.reference.V8ValueTypedArray
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
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.V8BindObject
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 kotlin.streams.asSequence
@ -64,33 +71,42 @@ class PackageHttp: V8Package {
}
@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)
_packageClientAuth.request(method, url, headers)
_packageClientAuth.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
else
_packageClient.request(method, url, headers);
_packageClient.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
}
@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)
_packageClientAuth.requestWithBody(method, url, body, headers)
_packageClientAuth.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
else
_packageClient.requestWithBody(method, url, body, headers);
_packageClient.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
}
@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)
_packageClientAuth.GET(url, headers)
_packageClientAuth.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING)
else
_packageClient.GET(url, headers);
_packageClient.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
}
@V8Function
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
return if(useAuth)
_packageClientAuth.POST(url, body, headers)
fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
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
_packageClient.POST(url, body, headers);
throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST");
}
@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
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;
override fun toV8(runtime: V8Runtime): V8Value? {
@ -125,6 +152,37 @@ class PackageHttp: V8Package {
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.
@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
= 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
@V8Function
@ -169,12 +233,14 @@ class PackageHttp: V8Package {
//Finalizer
@V8Function
fun execute(): List<BridgeHttpResponse> {
fun execute(): List<IBridgeHttpResponse?> {
return _reqs.parallelStream().map {
if(it.second.method == "DUMMY")
return@map 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
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()
.toList();
@ -232,63 +298,108 @@ class PackageHttp: V8Package {
}
@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);
return logExceptions {
return@logExceptions catchHttp {
val client = _client;
//logRequest(method, url, headers, null);
val resp = client.requestMethod(method, url, headers);
val responseBody = resp.body?.string();
//logResponse(method, url, resp.code, resp.headers, responseBody);
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
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");
}
}
};
}
@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);
return logExceptions {
catchHttp {
val client = _client;
//logRequest(method, url, headers, body);
val resp = client.requestMethod(method, url, body, headers);
val responseBody = resp.body?.string();
//logResponse(method, url, resp.code, resp.headers, responseBody);
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
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");
}
}
};
}
@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);
return logExceptions {
catchHttp {
val client = _client;
//logRequest("GET", url, headers, null);
val resp = client.get(url, headers);
val responseBody = resp.body?.string();
//val responseBody = resp.body?.string();
//logResponse("GET", url, resp.code, resp.headers, responseBody);
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
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");
}
}
};
}
@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);
return logExceptions {
catchHttp {
val client = _client;
//logRequest("POST", url, headers, body);
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);
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
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");
}
}
};
}
@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{
return handle();
}
//Forward timeouts
catch(ex: SocketTimeoutException) {
return BridgeHttpResponse("", 408, null);
return BridgeHttpStringResponse("", 408, null);
}
}
}
@ -514,20 +625,25 @@ class PackageHttp: V8Package {
val url: String,
val headers: MutableMap<String, String>,
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{
return handle();
}
//Forward timeouts
catch(ex: SocketTimeoutException) {
return BridgeHttpResponse("", 408, null);
return BridgeHttpStringResponse("", 408, null);
}
}
enum class ReturnType(val value: Int) {
STRING(0),
BYTES(1);
}
companion object {
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.platforms.js.SourcePluginConfig
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.casting.CastConnectionState
import com.futo.platformplayer.casting.StateCasting
@ -693,6 +694,7 @@ class VideoDetailView : ConstraintLayout {
_lastAudioSource = null;
_lastSubtitleSource = null;
video = null;
_player.clear();
cleanupPlaybackTracker();
Logger.i(TAG, "Keep screen on unset onClose")
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.net.Uri
import android.util.AttributeSet
import android.util.Xml
import android.widget.RelativeLayout
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.Encoding
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
@ -17,6 +22,8 @@ import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
import androidx.media3.exoplayer.ExoPlayer
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.hls.HlsMediaSource
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.video.IPlatformVideoDetails
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.JSSource
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.states.StateApp
import com.futo.platformplayer.video.PlayerManager
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
import com.google.gson.Gson
import getHttpDataSourceFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
import java.io.File
import kotlin.math.abs
@ -319,18 +331,31 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
swapSources(videoSource, audioSource,false, play, keepSubtitles);
}
fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean {
swapSourceInternal(videoSource);
swapSourceInternal(audioSource);
var videoSourceUsed = videoSource;
var audioSourceUsed = audioSource;
if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){
videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource);
audioSourceUsed = null;
}
swapSourceInternal(videoSourceUsed);
swapSourceInternal(audioSourceUsed);
if(!keepSubtitles)
_lastSubtitleMediaSource = null;
return loadSelectedSources(play, resume);
}
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);
}
fun swapSource(audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true): Boolean {
swapSourceInternal(audioSource);
if(audioSource is JSDashManifestRawAudioSource && lastVideoSource is JSDashManifestMergingRawSource)
swapSourceInternal(JSDashManifestMergingRawSource((lastVideoSource as JSDashManifestMergingRawSource).video, audioSource));
else
swapSourceInternal(audioSource);
return loadSelectedSources(play, resume);
}
@ -387,6 +412,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
is LocalVideoSource -> swapVideoSourceLocal(videoSource);
is JSVideoUrlRangeSource -> swapVideoSourceUrlRange(videoSource);
is IDashManifestSource -> swapVideoSourceDash(videoSource);
is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource);
is IHLSManifestSource -> swapVideoSourceHLS(videoSource);
is IVideoUrlSource -> swapVideoSourceUrl(videoSource);
null -> _lastVideoMediaSource = null;
@ -399,6 +425,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
is LocalAudioSource -> swapAudioSourceLocal(audioSource);
is JSAudioUrlRangeSource -> swapAudioSourceUrlRange(audioSource);
is JSHLSManifestAudioSource -> swapAudioSourceHLS(audioSource);
is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource);
is IAudioUrlWidevineSource -> swapAudioSourceUrlWidevine(audioSource)
is IAudioUrlSource -> swapAudioSourceUrl(audioSource);
null -> _lastAudioMediaSource = null;
@ -459,6 +486,54 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
.createMediaSource(MediaItem.fromUri(videoSource.url))
}
@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) {
Logger.i(TAG, "Loading VideoSource [HLS]");
val dataSource = if(videoSource is JSSource && videoSource.requiresCustomDatasource)
@ -521,6 +596,31 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
.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)
private fun swapAudioSourceUrlWidevine(audioSource: IAudioUrlWidevineSource) {
Logger.i(TAG, "Loading AudioSource [UrlWidevine]")
@ -574,28 +674,37 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
val sourceAudio = _lastAudioMediaSource;
val sourceSubs = _lastSubtitleMediaSource;
val sources = listOf(sourceVideo, sourceAudio, sourceSubs).filter { it != null }.map { it!! }.toTypedArray()
beforeSourceChanged();
_mediaSource = if(sources.size == 1) {
Logger.i(TAG, "Using single source mode")
(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();
val source = mergeMediaSources(sourceVideo, sourceAudio, sourceSubs);
if(source == null)
return false;
}
_mediaSource = source;
reloadMediaSource(play, resume);
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)
private fun reloadMediaSource(play: Boolean = false, resume: Boolean = true) {
val player = exoPlayer ?: return
@ -619,6 +728,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
fun clear() {
exoPlayer?.player?.stop();
exoPlayer?.player?.clearMediaItems();
_lastVideoMediaSource = null;
_lastAudioMediaSource = null;
_lastSubtitleMediaSource = null;
_mediaSource = null;
}
fun stop(){
@ -697,8 +810,15 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
companion object {
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_AUDIO_CONTAINERS = arrayOf("audio/mp3", "audio/mp4", "audio/webm", "audio/opus");
val PREFERED_VIDEO_CONTAINERS_MP4Pref = arrayOf("video/mp4", "video/webm", "video/3gpp");
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");
}

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.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.JSRequestModifier;
import androidx.media3.common.C;
@ -27,6 +28,8 @@ import androidx.media3.datasource.HttpUtil;
import androidx.media3.datasource.TransferListener;
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.google.common.base.Predicate;
import com.google.common.collect.ForwardingMap;
@ -70,7 +73,9 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
private boolean allowCrossProtocolRedirects;
private boolean keepPostFor302Redirects;
@Nullable private IRequestModifier requestModifier = null;
@Nullable private JSRequestExecutor requestExecutor = null;
@Nullable public JSRequestExecutor requestExecutor = null;
@Nullable public JSRequestExecutor requestExecutor2 = null;
/** Creates an instance. */
public Factory() {
@ -109,6 +114,18 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
this.requestExecutor = requestExecutor;
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.
@ -216,7 +233,8 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
contentTypePredicate,
keepPostFor302Redirects,
requestModifier,
requestExecutor);
requestExecutor,
requestExecutor2);
if (transferListener != null) {
dataSource.addTransferListener(transferListener);
}
@ -252,7 +270,10 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
private long bytesToRead;
private long bytesRead;
@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(
@Nullable String userAgent,
@ -263,7 +284,8 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
@Nullable Predicate<String> contentTypePredicate,
boolean keepPostFor302Redirects,
@Nullable IRequestModifier requestModifier,
@Nullable JSRequestExecutor requestExecutor) {
@Nullable JSRequestExecutor requestExecutor,
@Nullable JSRequestExecutor requestExecutor2) {
super(/* isNetwork= */ true);
this.userAgent = userAgent;
this.connectTimeoutMillis = connectTimeoutMillis;
@ -275,12 +297,13 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
this.keepPostFor302Redirects = keepPostFor302Redirects;
this.requestModifier = requestModifier;
this.requestExecutor = requestExecutor;
this.requestExecutor2 = requestExecutor2;
}
@Override
@Nullable
public Uri getUri() {
return connection == null ? null : Uri.parse(connection.getURL().toString());
return connection == null ? fallbackUri : Uri.parse(connection.getURL().toString());
}
@Override
@ -330,18 +353,29 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
bytesToRead = 0;
transferInitializing(dataSpec);
if(requestExecutor != null) {
byte[] data = requestExecutor.executeRequest(dataSpec.uri.toString(), dataSpec.httpRequestHeaders);
if(data == null)
throw new HttpDataSourceException(
"No response",
dataSpec,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_OPEN);
inputStream = new ByteArrayInputStream(data);
//Use executor 2 if it matches the urlPrefix
JSRequestExecutor executor = (requestExecutor2 != null && requestExecutor2.getUrlPrefix() != null && dataSpec.uri.toString().startsWith(requestExecutor2.getUrlPrefix())) ?
requestExecutor2 : requestExecutor;
transferStarted(dataSpec);
return data.length;
if(executor != null) {
try {
byte[] data = executor.executeRequest(dataSpec.uri.toString(), dataSpec.httpRequestHeaders);
if (data == null)
throw new HttpDataSourceException(
"No response",
dataSpec,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_OPEN);
inputStream = new ByteArrayInputStream(data);
fallbackUri = dataSpec.uri;
bytesToRead = data.length;
transferStarted(dataSpec);
return data.length;
}
catch(PluginException ex) {
throw HttpDataSourceException.createForIOException(new IOException("Executor failed: " + ex.getMessage(), ex), dataSpec, HttpDataSourceException.TYPE_OPEN);
}
}
else {
String responseMessage;

View file

@ -373,6 +373,10 @@
<string name="system_volume_descr">Gesture controls adjust system volume</string>
<string name="live_chat_webview">Live Chat Webview</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="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>