From b69402dfe922f96c92209317e8b5db7d92f336e0 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Thu, 3 Jul 2025 00:44:54 +0200 Subject: [PATCH] WIP Async support for Android --- .../com/futo/platformplayer/Extensions_V8.kt | 79 +++++++++++ .../api/media/platforms/js/JSClient.kt | 1 - .../sources/JSDashManifestRawAudioSource.kt | 6 +- .../models/sources/JSDashManifestRawSource.kt | 5 +- .../futo/platformplayer/engine/V8Plugin.kt | 124 +++++++++++++++++- .../engine/packages/PackageBridge.kt | 6 +- 6 files changed, 212 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt index 240a6cfc..ecf4a276 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt @@ -2,12 +2,22 @@ package com.futo.platformplayer import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.primitive.* +import com.caoccao.javet.values.reference.IV8ValuePromise import com.caoccao.javet.values.reference.V8ValueArray +import com.caoccao.javet.values.reference.V8ValueError import com.caoccao.javet.values.reference.V8ValueObject +import com.caoccao.javet.values.reference.V8ValuePromise import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.V8Plugin +import com.futo.platformplayer.engine.exceptions.ScriptExecutionException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.logging.Logger +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.cancel +import java.util.concurrent.CancellationException +import java.util.concurrent.CountDownLatch +import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType //V8 @@ -174,4 +184,73 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap { for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get(it).toString() }) map.put(prop, obj.getString(prop)); return map; +} + + +fun V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T { + val latch = CountDownLatch(1); + var promiseResult: T? = null; + var promiseException: Throwable? = null; + plugin.busy { + this.register(object: IV8ValuePromise.IListener { + override fun onFulfilled(p0: V8Value?) { + if(p0 is V8ValueError) + promiseException = ScriptExecutionException(plugin.config, p0.message); + else + promiseResult = p0 as T; + latch.countDown(); + } + override fun onRejected(p0: V8Value?) { + promiseException = (NotImplementedError("onRejected promise not implemented..")); + latch.countDown(); + } + override fun onCatch(p0: V8Value?) { + promiseException = (NotImplementedError("onCatch promise not implemented..")); + latch.countDown(); + } + }); + } + + plugin.registerPromise(this) { + promiseException = CancellationException("Cancelled by system"); + latch.countDown(); + } + plugin.unbusy { + latch.await(); + } + if(promiseException != null) + throw promiseException!!; + return promiseResult!!; +} +fun V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): Deferred { + val def = CompletableDeferred(); + val promise = this; + this.register(object: IV8ValuePromise.IListener { + override fun onFulfilled(p0: V8Value?) { + plugin.resolvePromise(promise); + def.complete(p0 as T); + } + override fun onRejected(p0: V8Value?) { + plugin.resolvePromise(promise); + def.completeExceptionally(NotImplementedError("onRejected promise not implemented..")); + } + override fun onCatch(p0: V8Value?) { + plugin.resolvePromise(promise); + def.completeExceptionally(NotImplementedError("onCatch promise not implemented..")); + } + }); + plugin.registerPromise(promise) { + if(def.isActive) + def.cancel("Cancelled by system"); + } + return def; +} + + +fun V8ValueObject.invokeV8(method: String, vararg obj: Any): T { + var result = this.invoke(method, *obj); + if(result is V8ValuePromise) { + return result.toV8ValueBlocking(this.getSourcePlugin()!!); + } + return result as T; } \ No newline at end of file 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 d61ebc0b..8c4097ae 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 @@ -632,7 +632,6 @@ open class JSClient : IPlatformClient { plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})")); } - @JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page") @JSDocsParameter("url", "Url of content") override fun getContentRecommendations(url: String): IPager? = isBusyWith("getContentRecommendations") { 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 f4994e0b..a484527c 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 @@ -1,5 +1,6 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources +import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource @@ -13,6 +14,7 @@ import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrThrow +import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.others.Language import com.futo.platformplayer.states.StateDeveloper @@ -63,14 +65,14 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { _plugin.isBusyWith("dashAudio.generate") { - _obj.invokeString("generate"); + _obj.invokeV8("generate").value; } } } else result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { _plugin.isBusyWith("dashAudio.generate") { - _obj.invokeString("generate"); + _obj.invokeV8("generate").value; } } 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 184b783d..a724a26e 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 @@ -15,6 +15,7 @@ import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrThrow +import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.states.StateDeveloper interface IJSDashManifestRawSource { @@ -68,7 +69,7 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { _plugin.isBusyWith("dashVideo.generate") { - _obj.invokeString("generate"); + _obj.invokeV8("generate").value; } }); } @@ -76,7 +77,7 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo else result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { _plugin.isBusyWith("dashVideo.generate") { - _obj.invokeString("generate"); + _obj.invokeV8("generate").value; } }); 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 0cb2f196..8a3c1c20 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -10,7 +10,9 @@ import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.primitive.V8ValueBoolean import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueString +import com.caoccao.javet.values.reference.IV8ValuePromise import com.caoccao.javet.values.reference.V8ValueObject +import com.caoccao.javet.values.reference.V8ValuePromise import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.constructs.Event1 @@ -37,7 +39,15 @@ import com.futo.platformplayer.engine.packages.V8Package import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateAssets +import com.futo.platformplayer.toList +import com.futo.platformplayer.toV8ValueBlocking +import com.futo.platformplayer.toV8ValueAsync import com.futo.platformplayer.warnIfMainThread +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.cancel +import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -48,6 +58,7 @@ class V8Plugin { private val _clientAuth: ManagedHttpClient; private val _clientOthers: ConcurrentHashMap = ConcurrentHashMap(); + private val _promises = ConcurrentHashMapUnit)?>(); val httpClient: ManagedHttpClient get() = _client; val httpClientAuth: ManagedHttpClient get() = _clientAuth; @@ -223,37 +234,144 @@ class V8Plugin { Logger.i(TAG, "Plugin stopped"); onStopped.emit(this); } + cancelAllPromises(); } fun isThreadAlreadyBusy(): Boolean { return _busyLock.isHeldByCurrentThread; } fun busy(handle: ()->T): T { + _busyLock.lock(); + try { + return handle(); + } + finally { + _busyLock.unlock(); + } + /* _busyLock.withLock { //Logger.i(TAG, "Entered busy: " + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()); return handle(); + }*/ + } + fun unbusy(handle: ()->T): T { + val wasLocked = isThreadAlreadyBusy(); + if(!wasLocked) + return handle(); + val lockCount = _busyLock.holdCount; + for(i in 1..lockCount) + _busyLock.unlock(); + try { + Logger.w(TAG, "Unlocking V8 thread for [${config.name}] for a blocking resolve of a promise") + return handle(); + } + finally { + Logger.w(TAG, "Relocking V8 thread for [${config.name}] for a blocking resolve of a promise") + + for(i in 1..lockCount) + _busyLock.lock(); } } fun execute(js: String) : V8Value { return executeTyped(js); } + + suspend fun executeTypedAsync(js: String) : Deferred { + warnIfMainThread("V8Plugin.executeTyped"); + if(isStopped) + throw PluginEngineStoppedException(config, "Instance is stopped", js); + + return withContext(IO) { + return@withContext busy { + try { + val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet"); + val result = catchScriptErrors("Plugin[${config.name}]", js) { + runtime.getExecutor(js).execute() + }; + + if (result is V8ValuePromise) { + return@busy result.toV8ValueAsync(this@V8Plugin); + } else + return@busy CompletableDeferred(result as T); + } + catch(ex: Throwable) { + val def = CompletableDeferred(); + def.completeExceptionally(ex); + return@busy def; + } + } + } + } fun executeTyped(js: String) : T { warnIfMainThread("V8Plugin.executeTyped"); if(isStopped) throw PluginEngineStoppedException(config, "Instance is stopped", js); - return busy { - + val result = busy { val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet"); - return@busy catchScriptErrors("Plugin[${config.name}]", js) { + return@busy catchScriptErrors("Plugin[${config.name}]", js) { runtime.getExecutor(js).execute() }; + }; + if(result is V8ValuePromise) { + return result.toV8ValueBlocking(this@V8Plugin); } + return result as T; } 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 } } + + fun handlePromise(result: V8ValuePromise): CompletableDeferred { + val def = CompletableDeferred(); + result.register(object: IV8ValuePromise.IListener { + override fun onFulfilled(p0: V8Value?) { + resolvePromise(result); + def.complete(p0 as T); + } + override fun onRejected(p0: V8Value?) { + resolvePromise(result); + def.completeExceptionally(NotImplementedError("onRejected promise not implemented..")); + } + override fun onCatch(p0: V8Value?) { + resolvePromise(result); + def.completeExceptionally(NotImplementedError("onCatch promise not implemented..")); + } + }); + registerPromise(result) { + if(def.isActive) + def.cancel("Cancelled by system"); + } + return def; + } + fun registerPromise(promise: V8ValuePromise, onCancelled: ((V8ValuePromise)->Unit)? = null) { + Logger.v(TAG, "Promise registered for plugin [${config.name}]: ${promise.hashCode()}"); + if (onCancelled != null) { + _promises.put(promise, onCancelled) + }; + } + fun resolvePromise(promise: V8ValuePromise, cancelled: Boolean = false) { + Logger.v(TAG, "Promise resolved for plugin [${config.name}]: ${promise.hashCode()}"); + val found = synchronized(_promises) { + val found = _promises.getOrDefault(promise, null); + _promises.remove(promise); + return@synchronized found; + }; + if(found != null) + found(promise); + } + fun cancelAllPromises(){ + val promises = _promises.keys().toList(); + for(key in promises) { + try { + resolvePromise(key, true); + } + catch(ex: Throwable) {} + } + } + + private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? { //TODO: Auto get all package types? return when(packageName) { 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 72bdf34f..db44c1fc 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 @@ -84,7 +84,8 @@ class PackageBridge : V8Package { fun supportedFeatures(): Array { return arrayOf( "ReloadRequiredException", - "HttpBatchClient" + "HttpBatchClient", + "Async" ); } @@ -130,9 +131,12 @@ class PackageBridge : V8Package { } timeoutMap.remove(id); try { + Logger.w(TAG, "setTimeout before busy (${timeout}): ${_plugin.isBusy}"); _plugin.busy { + Logger.w(TAG, "setTimeout in busy"); if (!_plugin.isStopped) funcClone.callVoid(null, arrayOf()); + Logger.w(TAG, "setTimeout after"); } } catch (ex: Throwable) { Logger.e(TAG, "Failed timeout callback", ex);