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 ecf4a276..bb127e42 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt @@ -13,10 +13,18 @@ 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 @@ -222,23 +230,30 @@ fun V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T { throw promiseException!!; return promiseResult!!; } -fun V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): Deferred { - val def = CompletableDeferred(); +fun V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred { + val def = if(this.has("estDuration")) + V8Deferred(CompletableDeferred(), + this.getOrDefault(plugin.config, "estDuration", "toV8ValueAsync", -1) ?: -1); + else + V8Deferred(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.busy { + 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"); @@ -246,6 +261,37 @@ fun V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): Deferred { 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); @@ -253,4 +299,11 @@ fun V8ValueObject.invokeV8(method: String, vararg obj: Any): T { 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/models/sources/JSDashManifestRawAudioSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawAudioSource.kt index a484527c..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 @@ -2,6 +2,7 @@ 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 @@ -15,8 +16,12 @@ 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; @@ -52,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; 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 a724a26e..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 @@ -16,11 +17,17 @@ 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 { @@ -58,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; @@ -117,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 8a3c1c20..9b888bff 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -277,7 +277,7 @@ class V8Plugin { } suspend fun executeTypedAsync(js: String) : Deferred { - warnIfMainThread("V8Plugin.executeTyped"); + warnIfMainThread("V8Plugin.executeTypedAsync"); if(isStopped) throw PluginEngineStoppedException(config, "Instance is stopped", js); @@ -358,7 +358,7 @@ class V8Plugin { _promises.remove(promise); return@synchronized found; }; - if(found != null) + if(found != null && cancelled) found(promise); } fun cancelAllPromises(){ 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..6630ccf3 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))