diff --git a/app/build.gradle b/app/build.gradle index fcbd422c..8c30e58d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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) //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 implementation 'androidx.media3:media3-exoplayer:1.2.1' diff --git a/app/src/main/assets/scripts/source.js b/app/src/main/assets/scripts/source.js index 0638f079..9f38404d 100644 --- a/app/src/main/assets/scripts/source.js +++ b/app/src/main/assets/scripts/source.js @@ -103,6 +103,12 @@ class UnavailableException extends ScriptException { super("UnavailableException", msg); } } +class ReloadRequiredException extends ScriptException { + constructor(msg, reloadData) { + super("ReloadRequiredException", msg); + this.reloadData = reloadData; + } +} class AgeException extends ScriptException { constructor(msg) { super("AgeException", msg); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt index b26abe45..1f29bf2a 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt @@ -56,6 +56,7 @@ class DevJSClient : 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); + client.setReloadData(getReloadData(true)); if (noSaveState) client.initialize() return client diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt index 476bad8a..4422dd28 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt @@ -62,6 +62,7 @@ import com.futo.platformplayer.states.StatePlugins import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.time.OffsetDateTime +import java.util.Random import kotlin.Exception import kotlin.reflect.full.findAnnotations import kotlin.reflect.jvm.kotlinFunction @@ -83,6 +84,8 @@ open class JSClient : IPlatformClient { private var _channelCapabilities: ResultCapabilities? = null; private var _peekChannelTypes: List? = null; + private var _usedReloadData: String? = null; + protected val _script: String; private var _initialized: Boolean = false; @@ -98,14 +101,14 @@ open class JSClient : IPlatformClient { override val icon: ImageVariable; override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities(); - private val _busyLock = Object(); - private var _busyCounter = 0; private var _busyAction = ""; - val isBusy: Boolean get() = _busyCounter > 0; + val isBusy: Boolean get() = _plugin.isBusy; val isBusyAction: String get() { return _busyAction; } + val declareOnEnable = HashMap(); + val settings: HashMap get() = descriptor.settings; val flags: Array; @@ -197,6 +200,7 @@ open class JSClient : IPlatformClient { open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient { val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials); + client.setReloadData(getReloadData(true)); if (noSaveState) client.initialize() return client @@ -213,14 +217,31 @@ open class JSClient : IPlatformClient { 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() { if (_initialized) return - Logger.i(TAG, "Plugin [${config.name}] initializing"); plugin.start(); + plugin.execute("plugin.config = ${Json.encodeToString(config)}"); plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})"); + descriptor.appSettings.loadDefaults(descriptor.config); _initialized = true; @@ -263,7 +284,16 @@ open class JSClient : IPlatformClient { fun enable() { if(!_initialized) 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)})"); + + if(declareOnEnable.containsKey("__reloadData")) { + Logger.i(TAG, "Plugin [${config.name}] enabled with reload data: ${declareOnEnable["__reloadData"]}"); + _usedReloadData = declareOnEnable["__reloadData"]; + declareOnEnable.remove("__reloadData"); + } _enabled = true; } @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})"); val tracker = plugin.executeTyped("source.getPlaybackTracker(${Json.encodeToString(url)})"); if(tracker is V8ValueObject) - return@isBusyWith JSPlaybackTracker(config, tracker); + return@isBusyWith JSPlaybackTracker(this, tracker); else return@isBusyWith null; } @@ -734,19 +764,22 @@ open class JSClient : IPlatformClient { return urls; } - - private fun isBusyWith(actionName: String, handle: ()->T): T { - try { - synchronized(_busyLock) { - _busyCounter++; - } - _busyAction = actionName; - return handle(); + fun busy(handle: ()->T): T { + return _plugin.busy { + return@busy handle(); } - finally { - _busyAction = ""; - synchronized(_busyLock) { - _busyCounter--; + } + + fun isBusyWith(actionName: String, handle: ()->T): T { + //val busyId = kotlin.random.Random.nextInt(9999); + return busy { + try { + _busyAction = actionName; + return@busy handle(); + + } + finally { + _busyAction = ""; } } } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt index 6f835304..03c5c2c6 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt @@ -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 { val newClient = JSHttpClient(_jsClient, _auth); newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) }) diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt index 8782b742..e81a288d 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt @@ -29,7 +29,9 @@ abstract class JSPager : IPager { this.pager = pager; this.config = config; - _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; + plugin.busy { + _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; + } getResults(); } @@ -44,11 +46,14 @@ abstract class JSPager : IPager { override fun nextPage() { warnIfMainThread("JSPager.nextPage"); - pager = plugin.getUnderlyingPlugin().catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") { - pager.invoke("nextPage", arrayOf()); - }; - _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; - _resultChanged = true; + val pluginV8 = plugin.getUnderlyingPlugin(); + pluginV8.busy { + pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") { + pager.invoke("nextPage", arrayOf()); + }; + _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; + _resultChanged = true; + } /* try { } @@ -70,15 +75,18 @@ abstract class JSPager : IPager { return previousResults; warnIfMainThread("JSPager.getResults"); - val items = pager.getOrThrow(config, "results", "JSPager"); - if(items.v8Runtime.isDead || items.v8Runtime.isClosed) - throw IllegalStateException("Runtime closed"); - val newResults = items.toArray() - .map { convertResult(it as V8ValueObject) } - .toList(); - _lastResults = newResults; - _resultChanged = false; - return newResults; + + return plugin.getUnderlyingPlugin().busy { + val items = pager.getOrThrow(config, "results", "JSPager"); + if (items.v8Runtime.isDead || items.v8Runtime.isClosed) + throw IllegalStateException("Runtime closed"); + val newResults = items.toArray() + .map { convertResult(it as V8ValueObject) } + .toList(); + _lastResults = newResults; + _resultChanged = false; + return@busy newResults; + } } abstract fun convertResult(obj: V8ValueObject): T; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaybackTracker.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaybackTracker.kt index e5ee7b68..15a7d854 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaybackTracker.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaybackTracker.kt @@ -2,37 +2,50 @@ package com.futo.platformplayer.api.media.platforms.js.models import com.caoccao.javet.values.reference.V8ValueObject 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.exceptions.ScriptImplementationException import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.warnIfMainThread class JSPlaybackTracker: IPlaybackTracker { - private val _config: IV8PluginConfig; - private val _obj: V8ValueObject; + private lateinit var _client: JSClient; + private lateinit var _config: IV8PluginConfig; + private lateinit var _obj: V8ValueObject; private var _hasCalledInit: Boolean = false; - private val _hasInit: Boolean; + private var _hasInit: Boolean = false; private var _lastRequest: Long = Long.MIN_VALUE; - private val _hasOnConcluded: Boolean; + private var _hasOnConcluded: Boolean = false; override var nextRequest: Int = 1000 private set; - constructor(config: IV8PluginConfig, obj: V8ValueObject) { + constructor(client: JSClient, obj: V8ValueObject) { 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; - this._obj = obj; - this._hasInit = obj.has("onInit"); + client.busy { + if (!obj.has("onProgress")) + 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) { @@ -40,12 +53,15 @@ class JSPlaybackTracker: IPlaybackTracker { synchronized(_obj) { if(_hasCalledInit) return; - if (_hasInit) { - Logger.i("JSPlaybackTracker", "onInit (${seconds})"); - _obj.invokeVoid("onInit", seconds); + + _client.busy { + 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) onInit(seconds); else { - Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})"); - _obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying); - nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false)); - _lastRequest = System.currentTimeMillis(); + _client.busy { + Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})"); + _obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying); + nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false)); + _lastRequest = System.currentTimeMillis(); + } } } } @@ -67,7 +85,9 @@ class JSPlaybackTracker: IPlaybackTracker { if(_hasOnConcluded) { synchronized(_obj) { Logger.i("JSPlaybackTracker", "onConcluded"); - _obj.invokeVoid("onConcluded", -1); + _client.busy { + _obj.invokeVoid("onConcluded", -1); + } } } } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt index 70dfecfd..36cfc7db 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestExecutor.kt @@ -46,16 +46,18 @@ class JSRequestExecutor { if (_executor.isClosed) throw IllegalStateException("Executor object is closed"); - val result = if(_plugin is DevJSClient) - StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") { - V8Plugin.catchScriptErrors( - _config, - "[${_config.name}] JSRequestExecutor", - "builder.modifyRequest()" - ) { - _executor.invoke("executeRequest", url, headers, method, body); - } as V8Value; - } + return _plugin.getUnderlyingPlugin().busy { + + val result = if(_plugin is DevJSClient) + StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") { + V8Plugin.catchScriptErrors( + _config, + "[${_config.name}] JSRequestExecutor", + "builder.modifyRequest()" + ) { + _executor.invoke("executeRequest", url, headers, method, body); + } as V8Value; + } else V8Plugin.catchScriptErrors( _config, "[${_config.name}] JSRequestExecutor", @@ -64,34 +66,35 @@ class JSRequestExecutor { _executor.invoke("executeRequest", url, headers, method, body); } as V8Value; - try { - 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(_config, "type", "JSRequestModifier"); - when(type) { - //TODO: Buffer type? + try { + if(result is V8ValueString) { + val base64Result = Base64.getDecoder().decode(result.value); + 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@busy bytesResult; + } + if(result is V8ValueObject && result.has("type")) { + val type = result.getOrThrow(_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) { - if(_plugin is DevJSClient) - StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined"); - throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null); + finally { + result.close(); } - throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name); - } - finally { - result.close(); } } @@ -99,24 +102,25 @@ class JSRequestExecutor { open fun cleanup() { if (!hasCleanup || _executor.isClosed) return; - - if(_plugin is DevJSClient) - StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") { - V8Plugin.catchScriptErrors( - _config, - "[${_config.name}] JSRequestExecutor", - "builder.modifyRequest()" - ) { - _executor.invokeVoid("cleanup", null); - }; - } - else V8Plugin.catchScriptErrors( - _config, - "[${_config.name}] JSRequestExecutor", - "builder.modifyRequest()" - ) { - _executor.invokeVoid("cleanup", null); - }; + _plugin.busy { + if(_plugin is DevJSClient) + StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") { + V8Plugin.catchScriptErrors( + _config, + "[${_config.name}] JSRequestExecutor", + "builder.modifyRequest()" + ) { + _executor.invokeVoid("cleanup", null); + }; + } + else V8Plugin.catchScriptErrors( + _config, + "[${_config.name}] JSRequestExecutor", + "builder.modifyRequest()" + ) { + _executor.invokeVoid("cleanup", null); + }; + } } protected fun finalize() { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt index 150189e7..f7d169af 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSRequestModifier.kt @@ -16,7 +16,7 @@ class JSRequestModifier: IRequestModifier { private val _plugin: JSClient; private val _config: IV8PluginConfig; private var _modifier: V8ValueObject; - override var allowByteSkip: Boolean; + override var allowByteSkip: Boolean = false; constructor(plugin: JSClient, modifier: V8ValueObject) { this._plugin = plugin; @@ -24,10 +24,13 @@ class JSRequestModifier: IRequestModifier { this._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): IRequest { @@ -35,13 +38,15 @@ class JSRequestModifier: IRequestModifier { return Request(url, headers); } - val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") { - _modifier.invoke("modifyRequest", url, headers); - } as V8ValueObject; + return _plugin.busy { + val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") { + _modifier.invoke("modifyRequest", url, headers); + } as V8ValueObject; - val req = JSRequest(_plugin, result, url, headers); - result.close(); - return req; + val req = JSRequest(_plugin, result, url, headers); + result.close(); + return@busy req; + } } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt index da495498..799c737f 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt @@ -27,6 +27,7 @@ import com.futo.platformplayer.getOrThrowNullable import com.futo.platformplayer.states.StateDeveloper class JSVideoDetails : JSVideo, IPlatformVideoDetails { + private val _plugin: JSClient; private val _hasGetComments: Boolean; private val _hasGetContentRecommendations: Boolean; private val _hasGetPlaybackTracker: Boolean; @@ -48,6 +49,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) { val contextName = "VideoDetails"; + _plugin = plugin; val config = plugin.config; description = _content.getOrThrow(config, "description", contextName); video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName)); @@ -86,7 +88,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { val tracker = _content.invoke("getPlaybackTracker", arrayOf()) ?: return@catchScriptErrors null; if(tracker is V8ValueObject) - return@catchScriptErrors JSPlaybackTracker(_pluginConfig, tracker); + return@catchScriptErrors JSPlaybackTracker(_plugin, tracker); else return@catchScriptErrors null; }; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawAudioSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawAudioSource.kt index ae35207b..2c6d4b35 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawAudioSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawAudioSource.kt @@ -62,12 +62,16 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS if(_plugin is DevJSClient) result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { - _obj.invokeString("generate"); + _plugin.isBusyWith("dashAudio.generate") { + _obj.invokeString("generate"); + } } } else result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { - _obj.invokeString("generate"); + _plugin.isBusyWith("dashAudio.generate") { + _obj.invokeString("generate"); + } } if(result != null){ diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt index d6ff7455..a9c070f7 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt @@ -67,13 +67,17 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo if(_plugin is DevJSClient) { result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { - _obj.invokeString("generate"); + _plugin.isBusyWith("dashVideo.generate") { + _obj.invokeString("generate"); + } }); } } else result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { - _obj.invokeString("generate"); + _plugin.isBusyWith("dashVideo.generate") { + _obj.invokeString("generate"); + } }); if(result != null){ diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt index 3c76e23d..00906239 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt @@ -62,9 +62,11 @@ abstract class JSSource { if (!hasRequestModifier || _obj.isClosed) return null; - val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") { - _obj.invoke("getRequestModifier", arrayOf()); - }; + val result = _plugin.isBusyWith("getRequestModifier") { + V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") { + _obj.invoke("getRequestModifier", arrayOf()); + }; + } if (result !is V8ValueObject) return null; @@ -75,9 +77,14 @@ abstract class JSSource { if (!hasRequestExecutor || _obj.isClosed) return null; - val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") { - _obj.invoke("getRequestExecutor", arrayOf()); - }; + Logger.v("JSSource", "Request executor for [${type}] requesting"); + val result =_plugin.isBusyWith("getRequestExecutor") { + V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") { + _obj.invoke("getRequestExecutor", arrayOf()); + }; + } + + Logger.v("JSSource", "Request executor for [${type}] received"); if (result !is V8ValueObject) return null; diff --git a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt index 15412fd9..323aa5e1 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -4,10 +4,9 @@ import android.content.Context import com.caoccao.javet.exceptions.JavetCompilationException import com.caoccao.javet.exceptions.JavetException import com.caoccao.javet.exceptions.JavetExecutionException +import com.caoccao.javet.interfaces.IJavetEntityError import com.caoccao.javet.interop.V8Host 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.primitive.V8ValueBoolean 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.ScriptImplementationException 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.ScriptUnavailableException 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.warnIfMainThread import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock class V8Plugin { val config: IV8PluginConfig; @@ -51,6 +53,8 @@ class V8Plugin { val httpClientAuth: ManagedHttpClient get() = _clientAuth; val httpClientOthers: Map get() = _clientOthers; + var runtimeId: Int = 0; + fun registerHttpClient(client: JSHttpClient) { synchronized(_clientOthers) { _clientOthers.put(client.clientId, client); @@ -67,10 +71,8 @@ class V8Plugin { var isStopped = true; val onStopped = Event1(); - //TODO: Implement a more universal isBusy system for plugins + JSClient + pooling? TBD if propagation would be beneficial - private val _busyCounterLock = Object(); - private var _busyCounter = 0; - val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 }; + private val _busyLock = ReentrantLock() + val isBusy get() = _busyLock.isLocked; var allowDevSubmit: Boolean = false private set(value) { @@ -140,6 +142,7 @@ class V8Plugin { synchronized(_runtimeLock) { if (_runtime != null) return; + runtimeId = runtimeId + 1; //V8RuntimeOptions.V8_FLAGS.setUseStrict(true); val host = V8Host.getV8Instance(); val options = host.jsRuntimeType.getRuntimeOptions(); @@ -184,10 +187,13 @@ class V8Plugin { } fun stop(){ Logger.i(TAG, "Stopping plugin [${config.name}]"); - isStopped = true; - whenNotBusy { + busy { + Logger.i(TAG, "Plugin stopping"); synchronized(_runtimeLock) { + if(isStopped) + return@busy; isStopped = true; + runtimeId = runtimeId + 1; //Cleanup http for(pack in _depsPackages) { @@ -211,10 +217,17 @@ class V8Plugin { Logger.i(TAG, "Stopped plugin [${config.name}]"); }; } + Logger.i(TAG, "Plugin stopped"); onStopped.emit(this); } } + fun 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 { return executeTyped(js); } @@ -223,49 +236,17 @@ class V8Plugin { if(isStopped) throw PluginEngineStoppedException(config, "Instance is stopped", js); - synchronized(_busyCounterLock) { - _busyCounter++; - } + return busy { - val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet"); - try { - return catchScriptErrors("Plugin[${config.name}]", js) { + val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet"); + return@busy catchScriptErrors("Plugin[${config.name}]", js) { 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(js).value }; - fun executeString(js: String) : String? = catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value }; - fun executeInteger(js: String) : Int? = catchScriptErrors("Plugin[${config.name}]") { executeTyped(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(js).value } } + fun executeString(js: String) : String? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value } } + fun executeInteger(js: String) : Int? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value } } private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? { //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); } catch(executeEx: JavetExecutionException) { - if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true) { - val pluginType = executeEx.scriptingError.context["plugin_type"].toString(); + if(executeEx.scriptingError?.context is IJavetEntityError) { + val obj = executeEx.scriptingError?.context as IJavetEntityError + if(obj.context.containsKey("plugin_type") == true) { + val pluginType = obj.context["plugin_type"].toString(); - //Captcha - if (pluginType == "CaptchaRequiredException") { - throw ScriptCaptchaRequiredException(config, - executeEx.scriptingError.context["url"]?.toString(), - executeEx.scriptingError.context["body"]?.toString(), - executeEx, executeEx.scriptingError?.stack, codeStripped); + //Captcha + if (pluginType == "CaptchaRequiredException") { + throw ScriptCaptchaRequiredException(config, + obj.context["url"]?.toString(), + obj.context["body"]?.toString(), + 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); } @@ -398,9 +391,4 @@ class V8Plugin { return StateAssets.readAsset(context, path) ?: throw java.lang.IllegalStateException("script ${path} not found"); } } - - - /** - * Methods available for scripts (bridge object) - */ } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptReloadRequiredException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptReloadRequiredException.kt new file mode 100644 index 00000000..98c0635d --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptReloadRequiredException.kt @@ -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(config, "reloadData", contextName, null)); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/engine/internal/V8BindObject.kt b/app/src/main/java/com/futo/platformplayer/engine/internal/V8BindObject.kt index 4e861b72..fd30af6f 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/internal/V8BindObject.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/internal/V8BindObject.kt @@ -13,8 +13,8 @@ open class V8BindObject : IV8Convertable { override fun toV8(runtime: V8Runtime): V8Value? { synchronized(this) { - if(_runtimeObj != null) - return _runtimeObj; + //if(_runtimeObj != null) + // return _runtimeObj; val v8Obj = runtime.createV8ValueObject(); v8Obj.bind(this); diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt index d2d7cf04..858b020b 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt @@ -4,6 +4,7 @@ import android.media.MediaCodec import android.media.MediaCodecList import com.caoccao.javet.annotations.V8Function import com.caoccao.javet.annotations.V8Property +import com.caoccao.javet.interop.callback.JavetCallbackContext import com.caoccao.javet.utils.JavetResourceUtils import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.reference.V8ValueFunction @@ -78,6 +79,14 @@ class PackageBridge : V8Package { return "android"; } + @V8Property + fun supportedFeatures(): Array { + return arrayOf( + "ReloadRequiredException", + "HttpBatchClient" + ); + } + @V8Property fun supportedContent(): Array { return arrayOf( @@ -105,28 +114,38 @@ class PackageBridge : V8Package { @V8Function fun setTimeout(func: V8ValueFunction, timeout: Long): Int { val id = timeoutCounter++; - val funcClone = func.toClone() StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { delay(timeout); + if(_plugin.isStopped) + return@launch; synchronized(timeoutMap) { if(!timeoutMap.contains(id)) { - JavetResourceUtils.safeClose(funcClone); + _plugin.busy { + if(!_plugin.isStopped) + JavetResourceUtils.safeClose(funcClone); + } return@launch; } timeoutMap.remove(id); } try { - _plugin.whenNotBusy { - funcClone.callVoid(null, arrayOf()); + _plugin.busy { + if(!_plugin.isStopped) + funcClone.callVoid(null, arrayOf()); } } catch(ex: Throwable) { Logger.e(TAG, "Failed timeout callback", ex); } finally { - JavetResourceUtils.safeClose(funcClone); + _plugin.busy { + if(!_plugin.isStopped) + JavetResourceUtils.safeClose(funcClone); + } + //_plugin.whenNotBusy { + //} } }; synchronized(timeoutMap) { @@ -141,13 +160,17 @@ class PackageBridge : V8Package { timeoutMap.remove(id); } } + @V8Function + fun sleep(length: Int) { + Thread.sleep(length.toLong()); + } @V8Function fun toast(str: String) { Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}"); StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { try { - UIDialogs.toast(str); + UIDialogs.appToast(str); } catch (e: Throwable) { Logger.e(TAG, "Failed to show toast.", e); } diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt index 900eb6f0..82edb023 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageHttp.kt @@ -44,6 +44,17 @@ class PackageHttp: V8Package { private val aliveSockets = mutableListOf(); private var _cleanedUp = false; + private val _clients = mutableMapOf() + + 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) { _config = config; @@ -112,6 +123,8 @@ class PackageHttp: V8Package { _plugin.registerHttpClient(httpClient); val client = PackageHttpClient(this, httpClient); + _clients.put(client.clientId() ?: "", client); + return client; } @V8Function @@ -246,18 +259,18 @@ class PackageHttp: V8Package { @V8Function fun request(method: String, url: String, headers: MutableMap = HashMap(), useAuth: Boolean = false) : BatchBuilder { - return clientRequest(_package.getDefaultClient(useAuth), method, url, headers); + return clientRequest(_package.getDefaultClient(useAuth).clientId(), method, url, headers); } @V8Function fun requestWithBody(method: String, url: String, body:String, headers: MutableMap = 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 fun GET(url: String, headers: MutableMap = HashMap(), useAuth: Boolean = false) : BatchBuilder - = clientGET(_package.getDefaultClient(useAuth), url, headers); + = clientGET(_package.getDefaultClient(useAuth).clientId(), url, headers); @V8Function fun POST(url: String, body: String, headers: MutableMap = HashMap(), useAuth: Boolean = false) : BatchBuilder - = clientPOST(_package.getDefaultClient(useAuth), url, body, headers); + = clientPOST(_package.getDefaultClient(useAuth).clientId(), url, body, headers); @V8Function fun DUMMY(): BatchBuilder { @@ -268,21 +281,21 @@ class PackageHttp: V8Package { //Client-specific @V8Function - fun clientRequest(client: PackageHttpClient, method: String, url: String, headers: MutableMap = HashMap()) : BatchBuilder { - _reqs.add(Pair(client, RequestDescriptor(method, url, headers))); + fun clientRequest(clientId: String?, method: String, url: String, headers: MutableMap = HashMap()) : BatchBuilder { + _reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers))); return BatchBuilder(_package, _reqs); } @V8Function - fun clientRequestWithBody(client: PackageHttpClient, method: String, url: String, body:String, headers: MutableMap = HashMap()) : BatchBuilder { - _reqs.add(Pair(client, RequestDescriptor(method, url, headers, body))); + fun clientRequestWithBody(clientId: String?, method: String, url: String, body:String, headers: MutableMap = HashMap()) : BatchBuilder { + _reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers, body))); return BatchBuilder(_package, _reqs); } @V8Function - fun clientGET(client: PackageHttpClient, url: String, headers: MutableMap = HashMap()) : BatchBuilder - = clientRequest(client, "GET", url, headers); + fun clientGET(clientId: String?, url: String, headers: MutableMap = HashMap()) : BatchBuilder + = clientRequest(clientId, "GET", url, headers); @V8Function - fun clientPOST(client: PackageHttpClient, url: String, body: String, headers: MutableMap = HashMap()) : BatchBuilder - = clientRequestWithBody(client, "POST", url, body, headers); + fun clientPOST(clientId: String?, url: String, body: String, headers: MutableMap = HashMap()) : BatchBuilder + = clientRequestWithBody(clientId, "POST", url, body, headers); //Finalizer @@ -321,6 +334,7 @@ class PackageHttp: V8Package { @Transient private val _clientId: String?; + @V8Property fun clientId(): String? { return _clientId; @@ -333,6 +347,17 @@ class PackageHttp: V8Package { _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 fun setDefaultHeaders(defaultHeaders: Map) { for(pair in defaultHeaders) @@ -429,8 +454,23 @@ class PackageHttp: V8Package { }; } @V8Function - fun POST(url: String, body: String, headers: MutableMap = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse - = POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING) + fun POST(url: String, body: Any, headers: MutableMap = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse { + 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 = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse { applyDefaultHeaders(headers); return logExceptions { @@ -452,9 +492,6 @@ class PackageHttp: V8Package { } }; } - @V8Function - fun POST(url: String, body: ByteArray, headers: MutableMap = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse - = POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING) fun POSTInternal(url: String, body: ByteArray, headers: MutableMap = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse { applyDefaultHeaders(headers); return logExceptions { @@ -630,7 +667,9 @@ class PackageHttp: V8Package { _isOpen = true; if(hasOpen && _listeners?.isClosed != true) { try { - _listeners?.invokeVoid("open", arrayOf()); + _package._plugin.busy { + _listeners?.invokeVoid("open", arrayOf()); + } } catch(ex: Throwable){ 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) { if(hasMessage && _listeners?.isClosed != true) { try { - _listeners?.invokeVoid("message", msg); + _package._plugin.busy { + _listeners?.invokeVoid("message", msg); + } } catch(ex: Throwable) {} } @@ -649,7 +690,9 @@ class PackageHttp: V8Package { if(hasClosing && _listeners?.isClosed != true) { try { - _listeners?.invokeVoid("closing", code, reason); + _package._plugin.busy { + _listeners?.invokeVoid("closing", code, reason); + } } catch(ex: Throwable){ Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex); @@ -660,7 +703,9 @@ class PackageHttp: V8Package { _isOpen = false; if(hasClosed && _listeners?.isClosed != true) { try { - _listeners?.invokeVoid("closed", code, reason); + _package._plugin.busy { + _listeners?.invokeVoid("closed", code, reason); + } } catch(ex: Throwable){ 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); if(hasFailure && _listeners?.isClosed != true) { try { - _listeners?.invokeVoid("failure", exception.message); + _package._plugin.busy { + _listeners?.invokeVoid("failure", exception.message); + } } catch(ex: Throwable){ Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 2aec6eaf..e9be9fd6 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -93,6 +93,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptAgeException import com.futo.platformplayer.engine.exceptions.ScriptException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException 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.exceptions.UnsupportedCastException import com.futo.platformplayer.fixHtmlLinks @@ -608,6 +609,10 @@ class VideoDetailView : ConstraintLayout { } } + _player.onReloadRequired.subscribe { + fetchVideo(); + } + _player.onPlayChanged.subscribe { if (StateCasting.instance.activeDevice == null) { handlePlayChanged(it); @@ -2492,7 +2497,9 @@ class VideoDetailView : ConstraintLayout { val url = _url; if (!url.isNullOrBlank()) { - setLoading(true); + fragment.lifecycleScope.launch(Dispatchers.Main) { + setLoading(true); + } _taskLoadVideo.run(url); } } @@ -3027,6 +3034,11 @@ class VideoDetailView : ConstraintLayout { return@TaskHandler result; }) .success { setVideoDetails(it, true) } + .exception { + StatePlatform.instance.handleReloadRequired(it, { + fetchVideo(); + }); + } .exception { Logger.w(TAG, "exception", it) diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt index c843ea9f..8cf8f080 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.states import android.content.Context import androidx.collection.LruCache +import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.R import com.futo.platformplayer.Settings 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.Event0 import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException import com.futo.platformplayer.fromPool import com.futo.platformplayer.getNowDiffDays import com.futo.platformplayer.getNowDiffSeconds @@ -316,7 +318,18 @@ class StatePlatform { _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) { val client = getClient(id); if (client !is JSClient) @@ -347,10 +360,27 @@ class StatePlatform { _availableClients.removeIf { it.id == id }; _availableClients.add(newClient); } + afterReload?.invoke(); 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) { val currentClients = getEnabledClients().map { it.id }; @@ -361,6 +391,9 @@ class StatePlatform { * If a client is disabled, NO requests are made to said client */ suspend fun selectClients(vararg ids: String) { + selectClients(null, *ids); + } + suspend fun selectClients(afterLoad: (() -> Unit)?, vararg ids: String) { withContext(Dispatchers.IO) { synchronized(_clientsLock) { val removed = _enabledClients.toMutableList(); @@ -385,6 +418,7 @@ class StatePlatform { onSourceDisabled.emit(oldClient); } } + afterLoad?.invoke(); }; } diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt index b72e840c..2740ca8b 100644 --- a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt +++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt @@ -174,7 +174,7 @@ abstract class SubscriptionsTaskFetchAlgorithm( if (resolve != null) { 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){ val task = providedTasks?.find { it.url == result.channelUrl }; if(task != null) { diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index 60c5dbf2..7f361105 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt @@ -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.JSSource 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.engine.exceptions.ScriptReloadRequiredException import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.video.PlayerManager import com.futo.platformplayer.views.video.datasources.PluginMediaDrmCallback import com.futo.platformplayer.views.video.datasources.JSHttpDataSource @@ -108,6 +111,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout { val onPositionDiscontinuity = Event1(); val onDatasourceError = Event1(); + val onReloadRequired = Event0(); + private var _didCallSourceChange = false; private var _lastState: Int = -1; @@ -560,12 +565,14 @@ abstract class FutoVideoPlayerBase : RelativeLayout { if(videoSource.hasGenerate) { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { + var startId = -1; try { + startId = videoSource?.getUnderlyingPlugin()?.getUnderlyingPlugin()?.runtimeId ?: -1; val generated = videoSource.generate(); if (generated != null) { withContext(Dispatchers.Main) { val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource)) - videoSource.getHttpDataSourceFactory() + withContext(Dispatchers.IO) { videoSource.getHttpDataSourceFactory() } else 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) { Logger.e(TAG, "DashRaw generator failed", ex); } @@ -671,25 +689,47 @@ abstract class FutoVideoPlayerBase : RelativeLayout { @OptIn(UnstableApi::class) private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean): Boolean { 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(play, resume); + var startId = -1; + try { + startId = audioSource.getUnderlyingPlugin()?.getUnderlyingPlugin()?.runtimeId ?: -1; + val generated = audioSource.generate(); + if(generated != null) { + val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource)) + 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; } else { + val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource)) + audioSource.getHttpDataSourceFactory() + else + DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); _lastVideoMediaSource = DashMediaSource.Factory(dataSource) .createMediaSource( DashManifestParser().parse(