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..d8427dc6 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,30 @@ 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.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel +import kotlinx.coroutines.selects.SelectClause0 +import kotlinx.coroutines.selects.SelectClause1 +import java.util.concurrent.CancellationException +import java.util.concurrent.CountDownLatch +import kotlin.coroutines.AbstractCoroutineContextElement +import kotlin.coroutines.CoroutineContext +import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType //V8 @@ -174,4 +192,119 @@ 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): V8Deferred { + val underlyingDef = CompletableDeferred(); + val def = if(this.has("estDuration")) + V8Deferred(underlyingDef, + this.getOrDefault(plugin.config, "estDuration", "toV8ValueAsync", -1) ?: -1); + else + V8Deferred(underlyingDef); + + val promise = this; + plugin.busy { + this.register(object: IV8ValuePromise.IListener { + override fun onFulfilled(p0: V8Value?) { + plugin.resolvePromise(promise); + underlyingDef.complete(p0 as T); + } + override fun onRejected(p0: V8Value?) { + plugin.resolvePromise(promise); + underlyingDef.completeExceptionally(NotImplementedError("onRejected promise not implemented..")); + } + override fun onCatch(p0: V8Value?) { + plugin.resolvePromise(promise); + underlyingDef.completeExceptionally(NotImplementedError("onCatch promise not implemented..")); + } + }); + } + plugin.registerPromise(promise) { + if(def.isActive) + def.cancel("Cancelled by system"); + } + return def; +} + +class V8Deferred(val deferred: Deferred, val estDuration: Int = -1): Deferred by deferred { + + fun convert(conversion: (result: T)->R): V8Deferred{ + val newDef = CompletableDeferred() + this.invokeOnCompletion { + if(it != null) + newDef.completeExceptionally(it); + else + newDef.complete(conversion(this@V8Deferred.getCompleted())); + } + + return V8Deferred(newDef, estDuration); + } + + + companion object { + fun merge(scope: CoroutineScope, defs: List>, conversion: (result: List)->R): V8Deferred { + + var amount = -1; + for(def in defs) + amount = Math.max(amount, def.estDuration); + + val def = scope.async { + val results = defs.map { it.await() }; + return@async conversion(results); + } + return V8Deferred(def, amount); + } + } +} + + +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; +} +fun V8ValueObject.invokeV8Async(method: String, vararg obj: Any): V8Deferred { + var result = this.invoke(method, *obj); + if(result is V8ValuePromise) { + return result.toV8ValueAsync(this.getSourcePlugin()!!); + } + return V8Deferred(CompletableDeferred(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..7b6388cd 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,6 +1,8 @@ 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.V8Deferred import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource @@ -13,8 +15,13 @@ 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.invokeV8Async import com.futo.platformplayer.others.Language import com.futo.platformplayer.states.StateDeveloper +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource { override val container : String; @@ -50,6 +57,44 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS hasGenerate = _obj.has("generate"); } + override fun generateAsync(scope: CoroutineScope): V8Deferred { + if(!hasGenerate) + return V8Deferred(CompletableDeferred(manifest)); + if(_obj.isClosed) + throw IllegalStateException("Source object already closed"); + + val plugin = _plugin.getUnderlyingPlugin(); + + var result: V8Deferred? = null; + if(_plugin is DevJSClient) + result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) { + _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { + _plugin.isBusyWith("dashAudio.generate") { + _obj.invokeV8Async("generate"); + } + } + } + else + result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { + _plugin.isBusyWith("dashAudio.generate") { + _obj.invokeV8Async("generate"); + } + } + + return plugin.busy { + val initStart = _obj.getOrDefault(_config, "initStart", "JSDashManifestRawSource", null) ?: 0; + val initEnd = _obj.getOrDefault(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0; + val indexStart = _obj.getOrDefault(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0; + val indexEnd = _obj.getOrDefault(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0; + if(initEnd > 0 && indexStart > 0 && indexEnd > 0) { + streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd); + } + + return@busy result.convert { + it.value + }; + } + } override fun generate(): String? { if(!hasGenerate) return manifest; @@ -63,14 +108,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..aebaab23 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 @@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.reference.V8ValueObject +import com.futo.platformplayer.V8Deferred import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource @@ -15,11 +16,18 @@ 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.invokeV8Async import com.futo.platformplayer.states.StateDeveloper +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async interface IJSDashManifestRawSource { val hasGenerate: Boolean; var manifest: String?; + fun generateAsync(scope: CoroutineScope): Deferred; fun generate(): String?; } open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource { @@ -57,6 +65,45 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo hasGenerate = _obj.has("generate"); } + override fun generateAsync(scope: CoroutineScope): V8Deferred { + if(!hasGenerate) + return V8Deferred(CompletableDeferred(manifest)); + if(_obj.isClosed) + throw IllegalStateException("Source object already closed"); + + val plugin = _plugin.getUnderlyingPlugin(); + + var result: V8Deferred? = null; + if(_plugin is DevJSClient) { + result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") { + _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { + _plugin.isBusyWith("dashVideo.generate") { + _obj.invokeV8Async("generate"); + } + }); + } + } + else + result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { + _plugin.isBusyWith("dashVideo.generate") { + _obj.invokeV8Async("generate"); + } + }); + + return plugin.busy { + val initStart = _obj.getOrDefault(_config, "initStart", "JSDashManifestRawSource", null) ?: 0; + val initEnd = _obj.getOrDefault(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0; + val indexStart = _obj.getOrDefault(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0; + val indexEnd = _obj.getOrDefault(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0; + if(initEnd > 0 && indexStart > 0 && indexEnd > 0) { + streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd); + } + + return@busy result.convert { + it.value + }; + } + } override open fun generate(): String? { if(!hasGenerate) return manifest; @@ -68,7 +115,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 +123,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; } }); @@ -116,6 +163,32 @@ class JSDashManifestMergingRawSource( override val priority: Boolean get() = video.priority; + override fun generateAsync(scope: CoroutineScope): V8Deferred { + val videoDashDef = video.generateAsync(scope); + val audioDashDef = audio.generateAsync(scope); + + return V8Deferred.merge(scope, listOf(videoDashDef, audioDashDef)) { + val (videoDash: String?, audioDash: String?) = it; + + if (videoDash != null && audioDash == null) return@merge videoDash; + if (audioDash != null && videoDash == null) return@merge audioDash; + if (videoDash == null) return@merge null; + + //TODO: Temporary simple solution..make more reliable version + + var result: String? = null; + val audioAdaptationSet = adaptationSetRegex.find(audioDash!!); + if (audioAdaptationSet != null) { + result = videoDash.replace( + "", + "\n" + audioAdaptationSet.value + ) + } else + result = videoDash; + + return@merge result; + }; + } override fun generate(): String? { val videoDash = video.generate(); val audioDash = audio.generate(); 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..9b888bff 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.executeTypedAsync"); + 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 && cancelled) + 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); 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 43ed541d..09cbeba5 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 @@ -567,11 +567,13 @@ abstract class FutoVideoPlayerBase : RelativeLayout { if(videoSource.hasGenerate) { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { + val scope = this; var startId = -1; try { val plugin = videoSource.getUnderlyingPlugin() ?: return@launch; startId = plugin.getUnderlyingPlugin()?.runtimeId ?: -1; - val generated = plugin.busy { videoSource.generate(); }; + val generatedDef = plugin.busy { videoSource.generateAsync(scope); }; + val generated = generatedDef.await(); if (generated != null) { withContext(Dispatchers.Main) { val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource)) @@ -694,10 +696,13 @@ abstract class FutoVideoPlayerBase : RelativeLayout { Logger.i(TAG, "Loading AudioSource [DashRaw]"); if(audioSource.hasGenerate) { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { + val scope = this; var startId = -1; try { + val plugin = audioSource.getUnderlyingPlugin() ?: return@launch; startId = audioSource.getUnderlyingPlugin()?.getUnderlyingPlugin()?.runtimeId ?: -1; - val generated = audioSource.generate(); + val generatedDef = plugin.busy { audioSource.generateAsync(scope); } + val generated = generatedDef.await(); if(generated != null) { val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource)) audioSource.getHttpDataSourceFactory()