Compare commits

...

9 Commits

Author SHA1 Message Date
Kelvin K 7e83793586 Submods 2025-06-16 18:34:37 +02:00
Kelvin K 6ba9ec8bc2 Clearer name setting 2025-06-16 17:56:04 +02:00
Kelvin 0b02ab0e2d Merge branch 'plugin-fixes' into 'master'
V8 Update, V8 interaction locking, Package fixes, ReloadRequiredException support

See merge request videostreaming/grayjay!125
2025-06-16 15:48:01 +00:00
Kelvin K ff531b5e77 Cleanup, fixes, clearCookies support on httpClients 2025-06-16 17:46:00 +02:00
Kelvin K b3f9de3b83 edgecase fix 2025-06-16 14:23:34 +02:00
Kelvin K 86bd71b89c Fix edgecase 2025-06-16 14:19:23 +02:00
Kelvin K 2fca7e9a01 Locking of most known v8 interactions, fix returning previously returned jvm objects, Related fixes 2025-06-16 14:13:47 +02:00
Kelvin K 58c9aeb1a2 WIP: V8 update, package http fixes, ReloadRequiredException support, other fixes. Currently broken in situations where setTimeout is used 2025-06-14 15:51:31 +02:00
Kelvin K 4702787784 WIP 2025-06-13 17:47:22 +02:00
27 changed files with 528 additions and 255 deletions
+2 -1
View File
@@ -179,7 +179,8 @@ dependencies {
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject) implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
//JS //JS
implementation("com.caoccao.javet:javet-android:3.0.2") //implementation("com.caoccao.javet:javet-android:3.0.2")
implementation 'com.caoccao.javet:javet-v8-android:4.1.4'
//Exoplayer //Exoplayer
implementation 'androidx.media3:media3-exoplayer:1.2.1' implementation 'androidx.media3:media3-exoplayer:1.2.1'
+6
View File
@@ -103,6 +103,12 @@ class UnavailableException extends ScriptException {
super("UnavailableException", msg); super("UnavailableException", msg);
} }
} }
class ReloadRequiredException extends ScriptException {
constructor(msg, reloadData) {
super("ReloadRequiredException", msg);
this.reloadData = reloadData;
}
}
class AgeException extends ScriptException { class AgeException extends ScriptException {
constructor(msg) { constructor(msg) {
super("AgeException", msg); super("AgeException", msg);
@@ -1017,8 +1017,8 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3) @FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
var playlistAllowDups: Boolean = true; var playlistAllowDups: Boolean = true;
@FormField(R.string.add_to_beginning_of_watch_later, FieldForm.TOGGLE, R.string.add_to_beginning_description, 4) @FormField(R.string.watch_later_add_start, FieldForm.TOGGLE, R.string.watch_later_add_start_description, 4)
var addToBeginning: Boolean = true; var watchLaterAddStart: Boolean = true;
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 5) @FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 5)
var polycentricEnabled: Boolean = true; var polycentricEnabled: Boolean = true;
@@ -56,6 +56,7 @@ class DevJSClient : JSClient {
override fun getCopy(privateCopy: Boolean, noSaveState: Boolean): JSClient { override fun getCopy(privateCopy: Boolean, noSaveState: Boolean): JSClient {
val client = DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, if (noSaveState) null else saveState(), devID); val client = DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, if (noSaveState) null else saveState(), devID);
client.setReloadData(getReloadData(true));
if (noSaveState) if (noSaveState)
client.initialize() client.initialize()
return client return client
@@ -62,6 +62,7 @@ import com.futo.platformplayer.states.StatePlugins
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.Random
import kotlin.Exception import kotlin.Exception
import kotlin.reflect.full.findAnnotations import kotlin.reflect.full.findAnnotations
import kotlin.reflect.jvm.kotlinFunction import kotlin.reflect.jvm.kotlinFunction
@@ -83,6 +84,8 @@ open class JSClient : IPlatformClient {
private var _channelCapabilities: ResultCapabilities? = null; private var _channelCapabilities: ResultCapabilities? = null;
private var _peekChannelTypes: List<String>? = null; private var _peekChannelTypes: List<String>? = null;
private var _usedReloadData: String? = null;
protected val _script: String; protected val _script: String;
private var _initialized: Boolean = false; private var _initialized: Boolean = false;
@@ -98,14 +101,14 @@ open class JSClient : IPlatformClient {
override val icon: ImageVariable; override val icon: ImageVariable;
override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities(); override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities();
private val _busyLock = Object();
private var _busyCounter = 0;
private var _busyAction = ""; private var _busyAction = "";
val isBusy: Boolean get() = _busyCounter > 0; val isBusy: Boolean get() = _plugin.isBusy;
val isBusyAction: String get() { val isBusyAction: String get() {
return _busyAction; return _busyAction;
} }
val declareOnEnable = HashMap<String, String>();
val settings: HashMap<String, String?> get() = descriptor.settings; val settings: HashMap<String, String?> get() = descriptor.settings;
val flags: Array<String>; val flags: Array<String>;
@@ -197,6 +200,7 @@ open class JSClient : IPlatformClient {
open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient { open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient {
val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials); val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials);
client.setReloadData(getReloadData(true));
if (noSaveState) if (noSaveState)
client.initialize() client.initialize()
return client return client
@@ -213,14 +217,31 @@ open class JSClient : IPlatformClient {
return plugin.httpClientOthers[id]; return plugin.httpClientOthers[id];
} }
fun setReloadData(data: String?) {
if(data == null) {
if(declareOnEnable.containsKey("__reloadData"))
declareOnEnable.remove("__reloadData");
}
else
declareOnEnable.put("__reloadData", data ?: "");
}
fun getReloadData(orLast: Boolean): String? {
if(declareOnEnable.containsKey("__reloadData"))
return declareOnEnable["__reloadData"];
else if(orLast)
return _usedReloadData;
return null;
}
override fun initialize() { override fun initialize() {
if (_initialized) return if (_initialized) return
Logger.i(TAG, "Plugin [${config.name}] initializing");
plugin.start(); plugin.start();
plugin.execute("plugin.config = ${Json.encodeToString(config)}"); plugin.execute("plugin.config = ${Json.encodeToString(config)}");
plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})"); plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})");
descriptor.appSettings.loadDefaults(descriptor.config); descriptor.appSettings.loadDefaults(descriptor.config);
_initialized = true; _initialized = true;
@@ -263,7 +284,16 @@ open class JSClient : IPlatformClient {
fun enable() { fun enable() {
if(!_initialized) if(!_initialized)
initialize(); initialize();
for(toDeclare in declareOnEnable) {
plugin.execute("var ${toDeclare.key} = " + Json.encodeToString(toDeclare.value));
}
plugin.execute("source.enable(${Json.encodeToString(config)}, parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())}), ${Json.encodeToString(_injectedSaveState)})"); plugin.execute("source.enable(${Json.encodeToString(config)}, parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())}), ${Json.encodeToString(_injectedSaveState)})");
if(declareOnEnable.containsKey("__reloadData")) {
Logger.i(TAG, "Plugin [${config.name}] enabled with reload data: ${declareOnEnable["__reloadData"]}");
_usedReloadData = declareOnEnable["__reloadData"];
declareOnEnable.remove("__reloadData");
}
_enabled = true; _enabled = true;
} }
@JSDocs(1, "source.saveState()", "Provide a string that is passed to enable for quicker startup of multiple instances") @JSDocs(1, "source.saveState()", "Provide a string that is passed to enable for quicker startup of multiple instances")
@@ -552,7 +582,7 @@ open class JSClient : IPlatformClient {
Logger.i(TAG, "JSClient.getPlaybackTracker(${url})"); Logger.i(TAG, "JSClient.getPlaybackTracker(${url})");
val tracker = plugin.executeTyped<V8Value>("source.getPlaybackTracker(${Json.encodeToString(url)})"); val tracker = plugin.executeTyped<V8Value>("source.getPlaybackTracker(${Json.encodeToString(url)})");
if(tracker is V8ValueObject) if(tracker is V8ValueObject)
return@isBusyWith JSPlaybackTracker(config, tracker); return@isBusyWith JSPlaybackTracker(this, tracker);
else else
return@isBusyWith null; return@isBusyWith null;
} }
@@ -734,19 +764,22 @@ open class JSClient : IPlatformClient {
return urls; return urls;
} }
fun <T> busy(handle: ()->T): T {
private fun <T> isBusyWith(actionName: String, handle: ()->T): T { return _plugin.busy {
try { return@busy handle();
synchronized(_busyLock) {
_busyCounter++;
}
_busyAction = actionName;
return handle();
} }
finally { }
_busyAction = "";
synchronized(_busyLock) { fun <T> isBusyWith(actionName: String, handle: ()->T): T {
_busyCounter--; //val busyId = kotlin.random.Random.nextInt(9999);
return busy {
try {
_busyAction = actionName;
return@busy handle();
}
finally {
_busyAction = "";
} }
} }
} }
@@ -67,6 +67,25 @@ class JSHttpClient : ManagedHttpClient {
} }
fun resetAuthCookies() {
_currentCookieMap.clear();
if(!_auth?.cookieMap.isNullOrEmpty()) {
for(domainCookies in _auth!!.cookieMap!!)
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
}
if(!_captcha?.cookieMap.isNullOrEmpty()) {
for(domainCookies in _captcha!!.cookieMap!!) {
if(_currentCookieMap.containsKey(domainCookies.key))
_currentCookieMap[domainCookies.key]?.putAll(domainCookies.value);
else
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
}
}
}
fun clearOtherCookies() {
_otherCookieMap.clear();
}
override fun clone(): ManagedHttpClient { override fun clone(): ManagedHttpClient {
val newClient = JSHttpClient(_jsClient, _auth); val newClient = JSHttpClient(_jsClient, _auth);
newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) }) newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
@@ -29,7 +29,9 @@ abstract class JSPager<T> : IPager<T> {
this.pager = pager; this.pager = pager;
this.config = config; this.config = config;
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; plugin.busy {
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
}
getResults(); getResults();
} }
@@ -44,11 +46,14 @@ abstract class JSPager<T> : IPager<T> {
override fun nextPage() { override fun nextPage() {
warnIfMainThread("JSPager.nextPage"); warnIfMainThread("JSPager.nextPage");
pager = plugin.getUnderlyingPlugin().catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") { val pluginV8 = plugin.getUnderlyingPlugin();
pager.invoke("nextPage", arrayOf<Any>()); pluginV8.busy {
}; pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; pager.invoke("nextPage", arrayOf<Any>());
_resultChanged = true; };
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
_resultChanged = true;
}
/* /*
try { try {
} }
@@ -70,15 +75,18 @@ abstract class JSPager<T> : IPager<T> {
return previousResults; return previousResults;
warnIfMainThread("JSPager.getResults"); warnIfMainThread("JSPager.getResults");
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
if(items.v8Runtime.isDead || items.v8Runtime.isClosed) return plugin.getUnderlyingPlugin().busy {
throw IllegalStateException("Runtime closed"); val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
val newResults = items.toArray() if (items.v8Runtime.isDead || items.v8Runtime.isClosed)
.map { convertResult(it as V8ValueObject) } throw IllegalStateException("Runtime closed");
.toList(); val newResults = items.toArray()
_lastResults = newResults; .map { convertResult(it as V8ValueObject) }
_resultChanged = false; .toList();
return newResults; _lastResults = newResults;
_resultChanged = false;
return@busy newResults;
}
} }
abstract fun convertResult(obj: V8ValueObject): T; abstract fun convertResult(obj: V8ValueObject): T;
@@ -2,37 +2,50 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
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.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.warnIfMainThread import com.futo.platformplayer.warnIfMainThread
class JSPlaybackTracker: IPlaybackTracker { class JSPlaybackTracker: IPlaybackTracker {
private val _config: IV8PluginConfig; private lateinit var _client: JSClient;
private val _obj: V8ValueObject; private lateinit var _config: IV8PluginConfig;
private lateinit var _obj: V8ValueObject;
private var _hasCalledInit: Boolean = false; private var _hasCalledInit: Boolean = false;
private val _hasInit: Boolean; private var _hasInit: Boolean = false;
private var _lastRequest: Long = Long.MIN_VALUE; private var _lastRequest: Long = Long.MIN_VALUE;
private val _hasOnConcluded: Boolean; private var _hasOnConcluded: Boolean = false;
override var nextRequest: Int = 1000 override var nextRequest: Int = 1000
private set; private set;
constructor(config: IV8PluginConfig, obj: V8ValueObject) { constructor(client: JSClient, obj: V8ValueObject) {
warnIfMainThread("JSPlaybackTracker.constructor"); warnIfMainThread("JSPlaybackTracker.constructor");
if(!obj.has("onProgress"))
throw ScriptImplementationException(config, "Missing onProgress on PlaybackTracker");
if(!obj.has("nextRequest"))
throw ScriptImplementationException(config, "Missing nextRequest on PlaybackTracker");
_hasOnConcluded = obj.has("onConcluded");
this._config = config; client.busy {
this._obj = obj; if (!obj.has("onProgress"))
this._hasInit = obj.has("onInit"); throw ScriptImplementationException(
client.config,
"Missing onProgress on PlaybackTracker"
);
if (!obj.has("nextRequest"))
throw ScriptImplementationException(
client.config,
"Missing nextRequest on PlaybackTracker"
);
_hasOnConcluded = obj.has("onConcluded");
this._client = client;
this._config = client.config;
this._obj = obj;
this._hasInit = obj.has("onInit");
}
} }
override fun onInit(seconds: Double) { override fun onInit(seconds: Double) {
@@ -40,12 +53,15 @@ class JSPlaybackTracker: IPlaybackTracker {
synchronized(_obj) { synchronized(_obj) {
if(_hasCalledInit) if(_hasCalledInit)
return; return;
if (_hasInit) {
Logger.i("JSPlaybackTracker", "onInit (${seconds})"); _client.busy {
_obj.invokeVoid("onInit", seconds); if (_hasInit) {
Logger.i("JSPlaybackTracker", "onInit (${seconds})");
_obj.invokeVoid("onInit", seconds);
}
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
_hasCalledInit = true;
} }
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
_hasCalledInit = true;
} }
} }
@@ -55,10 +71,12 @@ class JSPlaybackTracker: IPlaybackTracker {
if(!_hasCalledInit && _hasInit) if(!_hasCalledInit && _hasInit)
onInit(seconds); onInit(seconds);
else { else {
Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})"); _client.busy {
_obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying); Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})");
nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false)); _obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying);
_lastRequest = System.currentTimeMillis(); nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false));
_lastRequest = System.currentTimeMillis();
}
} }
} }
} }
@@ -67,7 +85,9 @@ class JSPlaybackTracker: IPlaybackTracker {
if(_hasOnConcluded) { if(_hasOnConcluded) {
synchronized(_obj) { synchronized(_obj) {
Logger.i("JSPlaybackTracker", "onConcluded"); Logger.i("JSPlaybackTracker", "onConcluded");
_obj.invokeVoid("onConcluded", -1); _client.busy {
_obj.invokeVoid("onConcluded", -1);
}
} }
} }
} }
@@ -46,16 +46,18 @@ class JSRequestExecutor {
if (_executor.isClosed) if (_executor.isClosed)
throw IllegalStateException("Executor object is closed"); throw IllegalStateException("Executor object is closed");
val result = if(_plugin is DevJSClient) return _plugin.getUnderlyingPlugin().busy {
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>( val result = if(_plugin is DevJSClient)
_config, StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
"[${_config.name}] JSRequestExecutor", V8Plugin.catchScriptErrors<Any>(
"builder.modifyRequest()" _config,
) { "[${_config.name}] JSRequestExecutor",
_executor.invoke("executeRequest", url, headers, method, body); "builder.modifyRequest()"
} as V8Value; ) {
} _executor.invoke("executeRequest", url, headers, method, body);
} as V8Value;
}
else V8Plugin.catchScriptErrors<Any>( else V8Plugin.catchScriptErrors<Any>(
_config, _config,
"[${_config.name}] JSRequestExecutor", "[${_config.name}] JSRequestExecutor",
@@ -64,34 +66,35 @@ class JSRequestExecutor {
_executor.invoke("executeRequest", url, headers, method, body); _executor.invoke("executeRequest", url, headers, method, body);
} as V8Value; } as V8Value;
try { try {
if(result is V8ValueString) { if(result is V8ValueString) {
val base64Result = Base64.getDecoder().decode(result.value); val base64Result = Base64.getDecoder().decode(result.value);
return base64Result; return@busy 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?
} }
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@busy bytesResult;
}
if(result is V8ValueObject && result.has("type")) {
val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier");
when(type) {
//TODO: Buffer type?
}
}
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);
} }
if(result is V8ValueUndefined) { finally {
if(_plugin is DevJSClient) result.close();
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();
} }
} }
@@ -99,24 +102,25 @@ class JSRequestExecutor {
open fun cleanup() { open fun cleanup() {
if (!hasCleanup || _executor.isClosed) if (!hasCleanup || _executor.isClosed)
return; return;
_plugin.busy {
if(_plugin is DevJSClient) if(_plugin is DevJSClient)
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") { StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
V8Plugin.catchScriptErrors<Any>( V8Plugin.catchScriptErrors<Any>(
_config, _config,
"[${_config.name}] JSRequestExecutor", "[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()" "builder.modifyRequest()"
) { ) {
_executor.invokeVoid("cleanup", null); _executor.invokeVoid("cleanup", null);
}; };
} }
else V8Plugin.catchScriptErrors<Any>( else V8Plugin.catchScriptErrors<Any>(
_config, _config,
"[${_config.name}] JSRequestExecutor", "[${_config.name}] JSRequestExecutor",
"builder.modifyRequest()" "builder.modifyRequest()"
) { ) {
_executor.invokeVoid("cleanup", null); _executor.invokeVoid("cleanup", null);
}; };
}
} }
protected fun finalize() { protected fun finalize() {
@@ -16,7 +16,7 @@ class JSRequestModifier: IRequestModifier {
private val _plugin: JSClient; private val _plugin: JSClient;
private val _config: IV8PluginConfig; private val _config: IV8PluginConfig;
private var _modifier: V8ValueObject; private var _modifier: V8ValueObject;
override var allowByteSkip: Boolean; override var allowByteSkip: Boolean = false;
constructor(plugin: JSClient, modifier: V8ValueObject) { constructor(plugin: JSClient, modifier: V8ValueObject) {
this._plugin = plugin; this._plugin = plugin;
@@ -24,10 +24,13 @@ class JSRequestModifier: IRequestModifier {
this._config = plugin.config; this._config = plugin.config;
val config = plugin.config; val config = plugin.config;
allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true; plugin.busy {
allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true;
if(!modifier.has("modifyRequest"))
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
}
if(!modifier.has("modifyRequest"))
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
} }
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest { override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
@@ -35,13 +38,15 @@ class JSRequestModifier: IRequestModifier {
return Request(url, headers); return Request(url, headers);
} }
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") { return _plugin.busy {
_modifier.invoke("modifyRequest", url, headers); val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
} as V8ValueObject; _modifier.invoke("modifyRequest", url, headers);
} as V8ValueObject;
val req = JSRequest(_plugin, result, url, headers); val req = JSRequest(_plugin, result, url, headers);
result.close(); result.close();
return req; return@busy req;
}
} }
@@ -27,6 +27,7 @@ import com.futo.platformplayer.getOrThrowNullable
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
class JSVideoDetails : JSVideo, IPlatformVideoDetails { class JSVideoDetails : JSVideo, IPlatformVideoDetails {
private val _plugin: JSClient;
private val _hasGetComments: Boolean; private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean; private val _hasGetContentRecommendations: Boolean;
private val _hasGetPlaybackTracker: Boolean; private val _hasGetPlaybackTracker: Boolean;
@@ -48,6 +49,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) {
val contextName = "VideoDetails"; val contextName = "VideoDetails";
_plugin = plugin;
val config = plugin.config; val config = plugin.config;
description = _content.getOrThrow(config, "description", contextName); description = _content.getOrThrow(config, "description", contextName);
video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName)); video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName));
@@ -86,7 +88,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
val tracker = _content.invoke<V8Value>("getPlaybackTracker", arrayOf<Any>()) val tracker = _content.invoke<V8Value>("getPlaybackTracker", arrayOf<Any>())
?: return@catchScriptErrors null; ?: return@catchScriptErrors null;
if(tracker is V8ValueObject) if(tracker is V8ValueObject)
return@catchScriptErrors JSPlaybackTracker(_pluginConfig, tracker); return@catchScriptErrors JSPlaybackTracker(_plugin, tracker);
else else
return@catchScriptErrors null; return@catchScriptErrors null;
}; };
@@ -62,12 +62,16 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
if(_plugin is DevJSClient) if(_plugin is DevJSClient)
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) { result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_obj.invokeString("generate"); _plugin.isBusyWith("dashAudio.generate") {
_obj.invokeString("generate");
}
} }
} }
else else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_obj.invokeString("generate"); _plugin.isBusyWith("dashAudio.generate") {
_obj.invokeString("generate");
}
} }
if(result != null){ if(result != null){
@@ -67,13 +67,17 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
if(_plugin is DevJSClient) { if(_plugin is DevJSClient) {
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") { result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_obj.invokeString("generate"); _plugin.isBusyWith("dashVideo.generate") {
_obj.invokeString("generate");
}
}); });
} }
} }
else else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_obj.invokeString("generate"); _plugin.isBusyWith("dashVideo.generate") {
_obj.invokeString("generate");
}
}); });
if(result != null){ if(result != null){
@@ -62,9 +62,11 @@ abstract class JSSource {
if (!hasRequestModifier || _obj.isClosed) if (!hasRequestModifier || _obj.isClosed)
return null; return null;
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") { val result = _plugin.isBusyWith("getRequestModifier") {
_obj.invoke("getRequestModifier", arrayOf<Any>()); V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") {
}; _obj.invoke("getRequestModifier", arrayOf<Any>());
};
}
if (result !is V8ValueObject) if (result !is V8ValueObject)
return null; return null;
@@ -75,9 +77,14 @@ abstract class JSSource {
if (!hasRequestExecutor || _obj.isClosed) if (!hasRequestExecutor || _obj.isClosed)
return null; return null;
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") { Logger.v("JSSource", "Request executor for [${type}] requesting");
_obj.invoke("getRequestExecutor", arrayOf<Any>()); val result =_plugin.isBusyWith("getRequestExecutor") {
}; V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") {
_obj.invoke("getRequestExecutor", arrayOf<Any>());
};
}
Logger.v("JSSource", "Request executor for [${type}] received");
if (result !is V8ValueObject) if (result !is V8ValueObject)
return null; return null;
@@ -4,10 +4,9 @@ import android.content.Context
import com.caoccao.javet.exceptions.JavetCompilationException import com.caoccao.javet.exceptions.JavetCompilationException
import com.caoccao.javet.exceptions.JavetException import com.caoccao.javet.exceptions.JavetException
import com.caoccao.javet.exceptions.JavetExecutionException import com.caoccao.javet.exceptions.JavetExecutionException
import com.caoccao.javet.interfaces.IJavetEntityError
import com.caoccao.javet.interop.V8Host import com.caoccao.javet.interop.V8Host
import com.caoccao.javet.interop.V8Runtime import com.caoccao.javet.interop.V8Runtime
import com.caoccao.javet.interop.options.V8Flags
import com.caoccao.javet.interop.options.V8RuntimeOptions
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.V8ValueBoolean import com.caoccao.javet.values.primitive.V8ValueBoolean
import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueInteger
@@ -26,6 +25,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptTimeoutException import com.futo.platformplayer.engine.exceptions.ScriptTimeoutException
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.engine.internal.V8Converter import com.futo.platformplayer.engine.internal.V8Converter
@@ -40,6 +40,8 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateAssets import com.futo.platformplayer.states.StateAssets
import com.futo.platformplayer.warnIfMainThread import com.futo.platformplayer.warnIfMainThread
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
class V8Plugin { class V8Plugin {
val config: IV8PluginConfig; val config: IV8PluginConfig;
@@ -51,6 +53,8 @@ class V8Plugin {
val httpClientAuth: ManagedHttpClient get() = _clientAuth; val httpClientAuth: ManagedHttpClient get() = _clientAuth;
val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers; val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers;
var runtimeId: Int = 0;
fun registerHttpClient(client: JSHttpClient) { fun registerHttpClient(client: JSHttpClient) {
synchronized(_clientOthers) { synchronized(_clientOthers) {
_clientOthers.put(client.clientId, client); _clientOthers.put(client.clientId, client);
@@ -67,10 +71,8 @@ class V8Plugin {
var isStopped = true; var isStopped = true;
val onStopped = Event1<V8Plugin>(); val onStopped = Event1<V8Plugin>();
//TODO: Implement a more universal isBusy system for plugins + JSClient + pooling? TBD if propagation would be beneficial private val _busyLock = ReentrantLock()
private val _busyCounterLock = Object(); val isBusy get() = _busyLock.isLocked;
private var _busyCounter = 0;
val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
var allowDevSubmit: Boolean = false var allowDevSubmit: Boolean = false
private set(value) { private set(value) {
@@ -140,6 +142,7 @@ class V8Plugin {
synchronized(_runtimeLock) { synchronized(_runtimeLock) {
if (_runtime != null) if (_runtime != null)
return; return;
runtimeId = runtimeId + 1;
//V8RuntimeOptions.V8_FLAGS.setUseStrict(true); //V8RuntimeOptions.V8_FLAGS.setUseStrict(true);
val host = V8Host.getV8Instance(); val host = V8Host.getV8Instance();
val options = host.jsRuntimeType.getRuntimeOptions(); val options = host.jsRuntimeType.getRuntimeOptions();
@@ -184,10 +187,13 @@ class V8Plugin {
} }
fun stop(){ fun stop(){
Logger.i(TAG, "Stopping plugin [${config.name}]"); Logger.i(TAG, "Stopping plugin [${config.name}]");
isStopped = true; busy {
whenNotBusy { Logger.i(TAG, "Plugin stopping");
synchronized(_runtimeLock) { synchronized(_runtimeLock) {
if(isStopped)
return@busy;
isStopped = true; isStopped = true;
runtimeId = runtimeId + 1;
//Cleanup http //Cleanup http
for(pack in _depsPackages) { for(pack in _depsPackages) {
@@ -211,10 +217,17 @@ class V8Plugin {
Logger.i(TAG, "Stopped plugin [${config.name}]"); Logger.i(TAG, "Stopped plugin [${config.name}]");
}; };
} }
Logger.i(TAG, "Plugin stopped");
onStopped.emit(this); onStopped.emit(this);
} }
} }
fun <T> busy(handle: ()->T): T {
_busyLock.withLock {
//Logger.i(TAG, "Entered busy: " + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString());
return handle();
}
}
fun execute(js: String) : V8Value { fun execute(js: String) : V8Value {
return executeTyped<V8Value>(js); return executeTyped<V8Value>(js);
} }
@@ -223,49 +236,17 @@ class V8Plugin {
if(isStopped) if(isStopped)
throw PluginEngineStoppedException(config, "Instance is stopped", js); throw PluginEngineStoppedException(config, "Instance is stopped", js);
synchronized(_busyCounterLock) { return busy {
_busyCounter++;
}
val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet"); val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
try { return@busy catchScriptErrors("Plugin[${config.name}]", js) {
return catchScriptErrors("Plugin[${config.name}]", js) {
runtime.getExecutor(js).execute() runtime.getExecutor(js).execute()
}; };
} }
finally {
synchronized(_busyCounterLock) {
//Free busy *after* afterBusy calls are done to prevent calls on dead runtimes
try {
afterBusy.emit(_busyCounter - 1);
}
catch(ex: Throwable) {
Logger.e(TAG, "Unhandled V8Plugin.afterBusy", ex);
}
_busyCounter--;
}
}
}
fun executeBoolean(js: String) : Boolean? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueBoolean>(js).value };
fun executeString(js: String) : String? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueString>(js).value };
fun executeInteger(js: String) : Int? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueInteger>(js).value };
fun whenNotBusy(handler: (V8Plugin)->Unit) {
synchronized(_busyCounterLock) {
if(_busyCounter == 0)
handler(this);
else {
val tag = Object();
afterBusy.subscribe(tag) {
if(it == 0) {
Logger.w(TAG, "V8Plugin afterBusy handled");
afterBusy.remove(tag);
handler(this);
}
}
}
}
} }
fun executeBoolean(js: String) : Boolean? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueBoolean>(js).value } }
fun executeString(js: String) : String? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueString>(js).value } }
fun executeInteger(js: String) : Int? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueInteger>(js).value } }
private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? { private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? {
//TODO: Auto get all package types? //TODO: Auto get all package types?
@@ -327,26 +308,38 @@ class V8Plugin {
throw ScriptCompilationException(config, "Compilation: [${context}]: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped); throw ScriptCompilationException(config, "Compilation: [${context}]: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped);
} }
catch(executeEx: JavetExecutionException) { catch(executeEx: JavetExecutionException) {
if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true) { if(executeEx.scriptingError?.context is IJavetEntityError) {
val pluginType = executeEx.scriptingError.context["plugin_type"].toString(); val obj = executeEx.scriptingError?.context as IJavetEntityError
if(obj.context.containsKey("plugin_type") == true) {
val pluginType = obj.context["plugin_type"].toString();
//Captcha //Captcha
if (pluginType == "CaptchaRequiredException") { if (pluginType == "CaptchaRequiredException") {
throw ScriptCaptchaRequiredException(config, throw ScriptCaptchaRequiredException(config,
executeEx.scriptingError.context["url"]?.toString(), obj.context["url"]?.toString(),
executeEx.scriptingError.context["body"]?.toString(), obj.context["body"]?.toString(),
executeEx, executeEx.scriptingError?.stack, codeStripped); executeEx, executeEx.scriptingError?.stack, codeStripped);
}
//Reload Required
if (pluginType == "ReloadRequiredException") {
throw ScriptReloadRequiredException(config,
obj.context["msg"]?.toString(),
obj.context["reloadData"]?.toString(),
executeEx, executeEx.scriptingError?.stack, codeStripped);
}
//Others
throwExceptionFromV8(
config,
pluginType,
(extractJSExceptionMessage(executeEx) ?: ""),
executeEx,
executeEx.scriptingError?.stack,
codeStripped
);
} }
//Others
throwExceptionFromV8(
config,
pluginType,
(extractJSExceptionMessage(executeEx) ?: ""),
executeEx,
executeEx.scriptingError?.stack,
codeStripped
);
} }
throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped); throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped);
} }
@@ -398,9 +391,4 @@ class V8Plugin {
return StateAssets.readAsset(context, path) ?: throw java.lang.IllegalStateException("script ${path} not found"); return StateAssets.readAsset(context, path) ?: throw java.lang.IllegalStateException("script ${path} not found");
} }
} }
/**
* Methods available for scripts (bridge object)
*/
} }
@@ -0,0 +1,20 @@
package com.futo.platformplayer.engine.exceptions
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8PluginConfig
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
class ScriptReloadRequiredException(config: IV8PluginConfig, val msg: String?, val reloadData: String?, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, msg ?: "ReloadRequired", ex, stack, code) {
companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
val contextName = "ScriptReloadRequiredException";
return ScriptReloadRequiredException(config,
obj.getOrThrow(config, "message", contextName),
obj.getOrDefault<String>(config, "reloadData", contextName, null));
}
}
}
@@ -13,8 +13,8 @@ open class V8BindObject : IV8Convertable {
override fun toV8(runtime: V8Runtime): V8Value? { override fun toV8(runtime: V8Runtime): V8Value? {
synchronized(this) { synchronized(this) {
if(_runtimeObj != null) //if(_runtimeObj != null)
return _runtimeObj; // return _runtimeObj;
val v8Obj = runtime.createV8ValueObject(); val v8Obj = runtime.createV8ValueObject();
v8Obj.bind(this); v8Obj.bind(this);
@@ -4,6 +4,7 @@ import android.media.MediaCodec
import android.media.MediaCodecList import android.media.MediaCodecList
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.interop.callback.JavetCallbackContext
import com.caoccao.javet.utils.JavetResourceUtils import com.caoccao.javet.utils.JavetResourceUtils
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueFunction import com.caoccao.javet.values.reference.V8ValueFunction
@@ -78,6 +79,14 @@ class PackageBridge : V8Package {
return "android"; return "android";
} }
@V8Property
fun supportedFeatures(): Array<String> {
return arrayOf(
"ReloadRequiredException",
"HttpBatchClient"
);
}
@V8Property @V8Property
fun supportedContent(): Array<Int> { fun supportedContent(): Array<Int> {
return arrayOf( return arrayOf(
@@ -105,28 +114,38 @@ class PackageBridge : V8Package {
@V8Function @V8Function
fun setTimeout(func: V8ValueFunction, timeout: Long): Int { fun setTimeout(func: V8ValueFunction, timeout: Long): Int {
val id = timeoutCounter++; val id = timeoutCounter++;
val funcClone = func.toClone<V8ValueFunction>() val funcClone = func.toClone<V8ValueFunction>()
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
delay(timeout); delay(timeout);
if(_plugin.isStopped)
return@launch;
synchronized(timeoutMap) { synchronized(timeoutMap) {
if(!timeoutMap.contains(id)) { if(!timeoutMap.contains(id)) {
JavetResourceUtils.safeClose(funcClone); _plugin.busy {
if(!_plugin.isStopped)
JavetResourceUtils.safeClose(funcClone);
}
return@launch; return@launch;
} }
timeoutMap.remove(id); timeoutMap.remove(id);
} }
try { try {
_plugin.whenNotBusy { _plugin.busy {
funcClone.callVoid(null, arrayOf<Any>()); if(!_plugin.isStopped)
funcClone.callVoid(null, arrayOf<Any>());
} }
} }
catch(ex: Throwable) { catch(ex: Throwable) {
Logger.e(TAG, "Failed timeout callback", ex); Logger.e(TAG, "Failed timeout callback", ex);
} }
finally { finally {
JavetResourceUtils.safeClose(funcClone); _plugin.busy {
if(!_plugin.isStopped)
JavetResourceUtils.safeClose(funcClone);
}
//_plugin.whenNotBusy {
//}
} }
}; };
synchronized(timeoutMap) { synchronized(timeoutMap) {
@@ -141,13 +160,17 @@ class PackageBridge : V8Package {
timeoutMap.remove(id); timeoutMap.remove(id);
} }
} }
@V8Function
fun sleep(length: Int) {
Thread.sleep(length.toLong());
}
@V8Function @V8Function
fun toast(str: String) { fun toast(str: String) {
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}"); Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
try { try {
UIDialogs.toast(str); UIDialogs.appToast(str);
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to show toast.", e); Logger.e(TAG, "Failed to show toast.", e);
} }
@@ -44,6 +44,17 @@ class PackageHttp: V8Package {
private val aliveSockets = mutableListOf<SocketResult>(); private val aliveSockets = mutableListOf<SocketResult>();
private var _cleanedUp = false; private var _cleanedUp = false;
private val _clients = mutableMapOf<String, PackageHttpClient>()
fun getClient(id: String?): PackageHttpClient {
if(id == null)
throw IllegalArgumentException("Http client ${id} doesn't exist");
if(_packageClient.clientId() == id)
return _packageClient;
if(_packageClientAuth.clientId() == id)
return _packageClientAuth;
return _clients.getOrDefault(id, null) ?: throw IllegalArgumentException("Http client ${id} doesn't exist");
}
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) { constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
_config = config; _config = config;
@@ -112,6 +123,8 @@ class PackageHttp: V8Package {
_plugin.registerHttpClient(httpClient); _plugin.registerHttpClient(httpClient);
val client = PackageHttpClient(this, httpClient); val client = PackageHttpClient(this, httpClient);
_clients.put(client.clientId() ?: "", client);
return client; return client;
} }
@V8Function @V8Function
@@ -246,18 +259,18 @@ class PackageHttp: V8Package {
@V8Function @V8Function
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder { fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder {
return clientRequest(_package.getDefaultClient(useAuth), method, url, headers); return clientRequest(_package.getDefaultClient(useAuth).clientId(), method, url, headers);
} }
@V8Function @V8Function
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder { fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder {
return clientRequestWithBody(_package.getDefaultClient(useAuth), method, url, body, headers); return clientRequestWithBody(_package.getDefaultClient(useAuth).clientId(), method, url, body, headers);
} }
@V8Function @V8Function
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
= clientGET(_package.getDefaultClient(useAuth), url, headers); = clientGET(_package.getDefaultClient(useAuth).clientId(), url, headers);
@V8Function @V8Function
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).clientId(), url, body, headers);
@V8Function @V8Function
fun DUMMY(): BatchBuilder { fun DUMMY(): BatchBuilder {
@@ -268,21 +281,21 @@ class PackageHttp: V8Package {
//Client-specific //Client-specific
@V8Function @V8Function
fun clientRequest(client: PackageHttpClient, method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder { fun clientRequest(clientId: String?, method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder {
_reqs.add(Pair(client, RequestDescriptor(method, url, headers))); _reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers)));
return BatchBuilder(_package, _reqs); return BatchBuilder(_package, _reqs);
} }
@V8Function @V8Function
fun clientRequestWithBody(client: PackageHttpClient, method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder { fun clientRequestWithBody(clientId: String?, method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder {
_reqs.add(Pair(client, RequestDescriptor(method, url, headers, body))); _reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers, body)));
return BatchBuilder(_package, _reqs); return BatchBuilder(_package, _reqs);
} }
@V8Function @V8Function
fun clientGET(client: PackageHttpClient, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder fun clientGET(clientId: String?, url: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder
= clientRequest(client, "GET", url, headers); = clientRequest(clientId, "GET", url, headers);
@V8Function @V8Function
fun clientPOST(client: PackageHttpClient, url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder fun clientPOST(clientId: String?, url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BatchBuilder
= clientRequestWithBody(client, "POST", url, body, headers); = clientRequestWithBody(clientId, "POST", url, body, headers);
//Finalizer //Finalizer
@@ -321,6 +334,7 @@ class PackageHttp: V8Package {
@Transient @Transient
private val _clientId: String?; private val _clientId: String?;
@V8Property @V8Property
fun clientId(): String? { fun clientId(): String? {
return _clientId; return _clientId;
@@ -333,6 +347,17 @@ class PackageHttp: V8Package {
_clientId = if(_client is JSHttpClient) _client.clientId else null; _clientId = if(_client is JSHttpClient) _client.clientId else null;
} }
@V8Function
fun resetAuthCookies(){
if(_client is JSHttpClient)
_client.resetAuthCookies();
}
@V8Function
fun clearOtherCookies(){
if(_client is JSHttpClient)
_client.clearOtherCookies();
}
@V8Function @V8Function
fun setDefaultHeaders(defaultHeaders: Map<String, String>) { fun setDefaultHeaders(defaultHeaders: Map<String, String>) {
for(pair in defaultHeaders) for(pair in defaultHeaders)
@@ -429,8 +454,23 @@ class PackageHttp: V8Package {
}; };
} }
@V8Function @V8Function
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse {
= POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING) if(body is V8ValueString)
return POSTInternal(url, body.value, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
else if(body is String)
return POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
else if(body is V8ValueTypedArray)
return POSTInternal(url, body.toBytes(), headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
else if(body is ByteArray)
return POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
else if(body is ArrayList<*>) //Avoid this case, used purely for testing
return POSTInternal(url, body.map { (it as Double).toInt().toByte() }.toByteArray(), headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING);
else
throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST");
}
// = POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
fun POSTInternal(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse { fun POSTInternal(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
applyDefaultHeaders(headers); applyDefaultHeaders(headers);
return logExceptions { return logExceptions {
@@ -452,9 +492,6 @@ class PackageHttp: V8Package {
} }
}; };
} }
@V8Function
fun POST(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse
= POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING)
fun POSTInternal(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse { fun POSTInternal(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
applyDefaultHeaders(headers); applyDefaultHeaders(headers);
return logExceptions { return logExceptions {
@@ -630,7 +667,9 @@ class PackageHttp: V8Package {
_isOpen = true; _isOpen = true;
if(hasOpen && _listeners?.isClosed != true) { if(hasOpen && _listeners?.isClosed != true) {
try { try {
_listeners?.invokeVoid("open", arrayOf<Any>()); _package._plugin.busy {
_listeners?.invokeVoid("open", arrayOf<Any>());
}
} }
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex);
@@ -640,7 +679,9 @@ class PackageHttp: V8Package {
override fun message(msg: String) { override fun message(msg: String) {
if(hasMessage && _listeners?.isClosed != true) { if(hasMessage && _listeners?.isClosed != true) {
try { try {
_listeners?.invokeVoid("message", msg); _package._plugin.busy {
_listeners?.invokeVoid("message", msg);
}
} }
catch(ex: Throwable) {} catch(ex: Throwable) {}
} }
@@ -649,7 +690,9 @@ class PackageHttp: V8Package {
if(hasClosing && _listeners?.isClosed != true) if(hasClosing && _listeners?.isClosed != true)
{ {
try { try {
_listeners?.invokeVoid("closing", code, reason); _package._plugin.busy {
_listeners?.invokeVoid("closing", code, reason);
}
} }
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex);
@@ -660,7 +703,9 @@ class PackageHttp: V8Package {
_isOpen = false; _isOpen = false;
if(hasClosed && _listeners?.isClosed != true) { if(hasClosed && _listeners?.isClosed != true) {
try { try {
_listeners?.invokeVoid("closed", code, reason); _package._plugin.busy {
_listeners?.invokeVoid("closed", code, reason);
}
} }
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
@@ -676,7 +721,9 @@ class PackageHttp: V8Package {
Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception); Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception);
if(hasFailure && _listeners?.isClosed != true) { if(hasFailure && _listeners?.isClosed != true) {
try { try {
_listeners?.invokeVoid("failure", exception.message); _package._plugin.busy {
_listeners?.invokeVoid("failure", exception.message);
}
} }
catch(ex: Throwable){ catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
@@ -93,6 +93,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptAgeException
import com.futo.platformplayer.engine.exceptions.ScriptException 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.engine.exceptions.ScriptLoginRequiredException import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.exceptions.UnsupportedCastException import com.futo.platformplayer.exceptions.UnsupportedCastException
import com.futo.platformplayer.fixHtmlLinks import com.futo.platformplayer.fixHtmlLinks
@@ -608,6 +609,10 @@ class VideoDetailView : ConstraintLayout {
} }
} }
_player.onReloadRequired.subscribe {
fetchVideo();
}
_player.onPlayChanged.subscribe { _player.onPlayChanged.subscribe {
if (StateCasting.instance.activeDevice == null) { if (StateCasting.instance.activeDevice == null) {
handlePlayChanged(it); handlePlayChanged(it);
@@ -2492,7 +2497,9 @@ class VideoDetailView : ConstraintLayout {
val url = _url; val url = _url;
if (!url.isNullOrBlank()) { if (!url.isNullOrBlank()) {
setLoading(true); fragment.lifecycleScope.launch(Dispatchers.Main) {
setLoading(true);
}
_taskLoadVideo.run(url); _taskLoadVideo.run(url);
} }
} }
@@ -3027,6 +3034,11 @@ class VideoDetailView : ConstraintLayout {
return@TaskHandler result; return@TaskHandler result;
}) })
.success { setVideoDetails(it, true) } .success { setVideoDetails(it, true) }
.exception<ScriptReloadRequiredException> {
StatePlatform.instance.handleReloadRequired(it, {
fetchVideo();
});
}
.exception<NoPlatformClientException> { .exception<NoPlatformClientException> {
Logger.w(TAG, "exception<NoPlatformClientException>", it) Logger.w(TAG, "exception<NoPlatformClientException>", it)
@@ -2,6 +2,7 @@ package com.futo.platformplayer.states
import android.content.Context import android.content.Context
import androidx.collection.LruCache import androidx.collection.LruCache
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
@@ -38,6 +39,7 @@ import com.futo.platformplayer.awaitFirstNotNullDeferred
import com.futo.platformplayer.constructs.BatchedTaskHandler import com.futo.platformplayer.constructs.BatchedTaskHandler
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
import com.futo.platformplayer.fromPool import com.futo.platformplayer.fromPool
import com.futo.platformplayer.getNowDiffDays import com.futo.platformplayer.getNowDiffDays
import com.futo.platformplayer.getNowDiffSeconds import com.futo.platformplayer.getNowDiffSeconds
@@ -316,7 +318,18 @@ class StatePlatform {
_platformOrderPersistent.save(); _platformOrderPersistent.save();
} }
suspend fun reloadClient(context: Context, id: String) : JSClient? { fun handleReloadRequired(reloadRequiredException: ScriptReloadRequiredException, afterReload: (() -> Unit)? = null) {
val id = if(reloadRequiredException.config is SourcePluginConfig) reloadRequiredException.config.id else "";
UIDialogs.appToast("Reloading [${reloadRequiredException.config.name}] by plugin request");
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
if(!reloadRequiredException.reloadData.isNullOrEmpty())
reEnableClientWithData(id, reloadRequiredException.reloadData, afterReload);
else
reEnableClient(id, afterReload);
}
}
suspend fun reloadClient(context: Context, id: String, afterReload: (()->Unit)? = null) : JSClient? {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val client = getClient(id); val client = getClient(id);
if (client !is JSClient) if (client !is JSClient)
@@ -347,10 +360,27 @@ class StatePlatform {
_availableClients.removeIf { it.id == id }; _availableClients.removeIf { it.id == id };
_availableClients.add(newClient); _availableClients.add(newClient);
} }
afterReload?.invoke();
return@withContext newClient; return@withContext newClient;
}; };
} }
suspend fun reEnableClientWithData(id: String, data: String? = null, afterReload: (()->Unit)? = null) {
val enabledBefore = getEnabledClients().map { it.id };
if(data != null) {
val client = getClientOrNull(id);
if(client != null && client is JSClient)
client.setReloadData(data);
}
selectClients({
_scope.launch(Dispatchers.IO) {
selectClients({
afterReload?.invoke();
}, *(enabledBefore).distinct().toTypedArray());
}
}, *(enabledBefore.filter { it != id }).distinct().toTypedArray())
}
suspend fun reEnableClient(id: String, afterReload: (()->Unit)? = null) = reEnableClientWithData(id, null, afterReload);
suspend fun enableClient(ids: List<String>) { suspend fun enableClient(ids: List<String>) {
val currentClients = getEnabledClients().map { it.id }; val currentClients = getEnabledClients().map { it.id };
@@ -361,6 +391,9 @@ class StatePlatform {
* If a client is disabled, NO requests are made to said client * If a client is disabled, NO requests are made to said client
*/ */
suspend fun selectClients(vararg ids: String) { suspend fun selectClients(vararg ids: String) {
selectClients(null, *ids);
}
suspend fun selectClients(afterLoad: (() -> Unit)?, vararg ids: String) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
synchronized(_clientsLock) { synchronized(_clientsLock) {
val removed = _enabledClients.toMutableList(); val removed = _enabledClients.toMutableList();
@@ -385,6 +418,7 @@ class StatePlatform {
onSourceDisabled.emit(oldClient); onSourceDisabled.emit(oldClient);
} }
} }
afterLoad?.invoke();
}; };
} }
@@ -3,7 +3,6 @@ package com.futo.platformplayer.states
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
@@ -21,7 +20,6 @@ import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.sToOffsetDateTimeUTC import com.futo.platformplayer.sToOffsetDateTimeUTC
import com.futo.platformplayer.smartMerge import com.futo.platformplayer.smartMerge
import com.futo.platformplayer.states.StateSubscriptionGroups.Companion
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringArrayStorage import com.futo.platformplayer.stores.StringArrayStorage
import com.futo.platformplayer.stores.StringDateMapStorage import com.futo.platformplayer.stores.StringDateMapStorage
@@ -30,15 +28,12 @@ import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.stores.v2.ReconstructStore import com.futo.platformplayer.stores.v2.ReconstructStore
import com.futo.platformplayer.sync.internal.GJSyncOpcodes import com.futo.platformplayer.sync.internal.GJSyncOpcodes
import com.futo.platformplayer.sync.models.SyncPlaylistsPackage import com.futo.platformplayer.sync.models.SyncPlaylistsPackage
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
import com.futo.platformplayer.sync.models.SyncWatchLaterPackage import com.futo.platformplayer.sync.models.SyncWatchLaterPackage
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.time.Instant
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
@@ -185,7 +180,7 @@ class StatePlaylists {
} }
_watchlistStore.saveAsync(video) _watchlistStore.saveAsync(video)
if (Settings.instance.other.addToBeginning) { if (Settings.instance.other.watchLaterAddStart) {
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray()) _watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray())
} else { } else {
_watchlistOrderStore.set(*(_watchlistOrderStore.values + listOf(video.url)).toTypedArray()) _watchlistOrderStore.set(*(_watchlistOrderStore.values + listOf(video.url)).toTypedArray())
@@ -174,7 +174,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
if (resolve != null) { if (resolve != null) {
resolveCount = resolves.size; resolveCount = resolves.size;
UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size}") UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size})")
for(result in resolve){ for(result in resolve){
val task = providedTasks?.find { it.url == result.channelUrl }; val task = providedTasks?.find { it.url == result.channelUrl };
if(task != null) { if(task != null) {
@@ -52,10 +52,13 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManif
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
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
import com.futo.platformplayer.helpers.VideoHelper 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.states.StatePlatform
import com.futo.platformplayer.video.PlayerManager import com.futo.platformplayer.video.PlayerManager
import com.futo.platformplayer.views.video.datasources.PluginMediaDrmCallback import com.futo.platformplayer.views.video.datasources.PluginMediaDrmCallback
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
@@ -108,6 +111,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
val onPositionDiscontinuity = Event1<Long>(); val onPositionDiscontinuity = Event1<Long>();
val onDatasourceError = Event1<Throwable>(); val onDatasourceError = Event1<Throwable>();
val onReloadRequired = Event0();
private var _didCallSourceChange = false; private var _didCallSourceChange = false;
private var _lastState: Int = -1; private var _lastState: Int = -1;
@@ -560,12 +565,14 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
if(videoSource.hasGenerate) { if(videoSource.hasGenerate) {
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
var startId = -1;
try { try {
startId = videoSource?.getUnderlyingPlugin()?.getUnderlyingPlugin()?.runtimeId ?: -1;
val generated = videoSource.generate(); val generated = videoSource.generate();
if (generated != null) { if (generated != null) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource)) val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource))
videoSource.getHttpDataSourceFactory() withContext(Dispatchers.IO) { videoSource.getHttpDataSourceFactory() }
else else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
@@ -585,6 +592,17 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
} }
} }
catch(reloadRequired: ScriptReloadRequiredException) {
Logger.i(TAG, "Reload required detected");
val plugin = videoSource.getUnderlyingPlugin();
if(plugin == null)
return@launch;
if(startId != -1 && plugin.getUnderlyingPlugin()?.runtimeId != startId)
return@launch;
StatePlatform.instance.handleReloadRequired(reloadRequired, {
onReloadRequired.emit();
});
}
catch(ex: Throwable) { catch(ex: Throwable) {
Logger.e(TAG, "DashRaw generator failed", ex); Logger.e(TAG, "DashRaw generator failed", ex);
} }
@@ -671,25 +689,47 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean): Boolean { private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean): Boolean {
Logger.i(TAG, "Loading AudioSource [DashRaw]"); 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) { if(audioSource.hasGenerate) {
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
val generated = audioSource.generate(); var startId = -1;
if(generated != null) { try {
withContext(Dispatchers.Main) { startId = audioSource.getUnderlyingPlugin()?.getUnderlyingPlugin()?.runtimeId ?: -1;
_lastVideoMediaSource = DashMediaSource.Factory(dataSource) val generated = audioSource.generate();
.createMediaSource(DashManifestParser().parse(Uri.parse(audioSource.url), if(generated != null) {
ByteArrayInputStream(generated?.toByteArray() ?: ByteArray(0)))); val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource))
loadSelectedSources(play, resume); audioSource.getHttpDataSourceFactory()
else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
withContext(Dispatchers.Main) {
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
.createMediaSource(DashManifestParser().parse(Uri.parse(audioSource.url),
ByteArrayInputStream(generated?.toByteArray() ?: ByteArray(0))));
loadSelectedSources(play, resume);
}
} }
} }
catch(reloadRequired: ScriptReloadRequiredException) {
Logger.i(TAG, "Reload required detected");
val plugin = audioSource.getUnderlyingPlugin();
if(plugin == null)
return@launch;
if(startId != -1 && plugin.getUnderlyingPlugin()?.runtimeId != startId)
return@launch;
StatePlatform.instance.reEnableClient(plugin.id, {
onReloadRequired.emit();
});
}
catch(ex: Throwable) {
}
} }
return false; return false;
} }
else { else {
val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource))
audioSource.getHttpDataSourceFactory()
else
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
_lastVideoMediaSource = DashMediaSource.Factory(dataSource) _lastVideoMediaSource = DashMediaSource.Factory(dataSource)
.createMediaSource( .createMediaSource(
DashManifestParser().parse( DashManifestParser().parse(
+2 -2
View File
@@ -468,8 +468,8 @@
<string name="playlist_delete_confirmation_description">Show confirmation dialog when deleting media from a playlist</string> <string name="playlist_delete_confirmation_description">Show confirmation dialog when deleting media from a playlist</string>
<string name="playlist_allow_dups">Allow duplicate playlist videos</string> <string name="playlist_allow_dups">Allow duplicate playlist videos</string>
<string name="playlist_allow_dups_description">Allow adding duplicate videos to playlists</string> <string name="playlist_allow_dups_description">Allow adding duplicate videos to playlists</string>
<string name="add_to_beginning_of_watch_later">Add new videos to the beginning of Watch Later</string> <string name="watch_later_add_start">Add new videos to the beginning of Watch Later</string>
<string name="add_to_beginning_description">When adding videos to Watch Later add them to the beginning of the list instead of the end</string> <string name="watch_later_add_start_description">When adding videos to Watch Later add them to the beginning of the list instead of the end</string>
<string name="already_in_watch_later">Already in watch later</string> <string name="already_in_watch_later">Already in watch later</string>
<string name="enable_polycentric">Enable Polycentric</string> <string name="enable_polycentric">Enable Polycentric</string>
<string name="polycentric_local_cache">Enable Polycentric Local Caching</string> <string name="polycentric_local_cache">Enable Polycentric Local Caching</string>