WIP rewrite generate to async

This commit is contained in:
Kelvin 2025-07-03 19:05:13 +02:00
commit 8fb0826d69
5 changed files with 189 additions and 19 deletions

View file

@ -13,10 +13,18 @@ import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.selects.SelectClause0
import kotlinx.coroutines.selects.SelectClause1
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType
@ -222,9 +230,15 @@ fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
throw promiseException!!; throw promiseException!!;
return promiseResult!!; return promiseResult!!;
} }
fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): Deferred<T> { fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T> {
val def = CompletableDeferred<T>(); val def = if(this.has("estDuration"))
V8Deferred(CompletableDeferred<T>(),
this.getOrDefault(plugin.config, "estDuration", "toV8ValueAsync", -1) ?: -1);
else
V8Deferred<T>(CompletableDeferred<T>());
val promise = this; val promise = this;
plugin.busy {
this.register(object: IV8ValuePromise.IListener { this.register(object: IV8ValuePromise.IListener {
override fun onFulfilled(p0: V8Value?) { override fun onFulfilled(p0: V8Value?) {
plugin.resolvePromise(promise); plugin.resolvePromise(promise);
@ -239,6 +253,7 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): Deferred<T> {
def.completeExceptionally(NotImplementedError("onCatch promise not implemented..")); def.completeExceptionally(NotImplementedError("onCatch promise not implemented.."));
} }
}); });
}
plugin.registerPromise(promise) { plugin.registerPromise(promise) {
if(def.isActive) if(def.isActive)
def.cancel("Cancelled by system"); def.cancel("Cancelled by system");
@ -246,6 +261,37 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): Deferred<T> {
return def; return def;
} }
class V8Deferred<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Deferred<T> by deferred {
fun <R> convert(conversion: (result: T)->R): V8Deferred<R>{
val newDef = CompletableDeferred<R>()
this.invokeOnCompletion {
if(it != null)
newDef.completeExceptionally(it);
else
newDef.complete(conversion(this@V8Deferred.getCompleted()));
}
return V8Deferred<R>(newDef, estDuration);
}
companion object {
fun <R> merge(scope: CoroutineScope, defs: List<V8Deferred<T>> conversion: (result: List<T>)->R): V8Deferred<R> {
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 <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any): T { fun <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any): T {
var result = this.invoke<V8Value>(method, *obj); var result = this.invoke<V8Value>(method, *obj);
@ -254,3 +300,10 @@ fun <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any): T {
} }
return result as T; return result as T;
} }
fun <T: V8Value> V8ValueObject.invokeV8Async(method: String, vararg obj: Any): V8Deferred<T> {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueAsync(this.getSourcePlugin()!!);
}
return V8Deferred(CompletableDeferred(result as T));
}

View file

@ -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.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueObject 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.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource 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.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.others.Language import com.futo.platformplayer.others.Language
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource { class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
override val container : String; override val container : String;
@ -52,6 +57,44 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
hasGenerate = _obj.has("generate"); hasGenerate = _obj.has("generate");
} }
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest));
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
val plugin = _plugin.getUnderlyingPlugin();
var result: V8Deferred<V8ValueString>? = 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<V8ValueString>("generate");
}
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_plugin.isBusyWith("dashAudio.generate") {
_obj.invokeV8Async<V8ValueString>("generate");
}
}
return plugin.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_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? { override fun generate(): String? {
if(!hasGenerate) if(!hasGenerate)
return manifest; return manifest;

View file

@ -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.V8Value
import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueObject 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.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource 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.getOrNull
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.invokeV8Async
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
interface IJSDashManifestRawSource { interface IJSDashManifestRawSource {
val hasGenerate: Boolean; val hasGenerate: Boolean;
var manifest: String?; var manifest: String?;
fun generateAsync(scope: CoroutineScope): Deferred<String?>;
fun generate(): String?; fun generate(): String?;
} }
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource { open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
@ -58,6 +65,45 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
hasGenerate = _obj.has("generate"); hasGenerate = _obj.has("generate");
} }
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
if(!hasGenerate)
return V8Deferred(CompletableDeferred(manifest));
if(_obj.isClosed)
throw IllegalStateException("Source object already closed");
val plugin = _plugin.getUnderlyingPlugin();
var result: V8Deferred<V8ValueString>? = 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<V8ValueString>("generate");
}
});
}
}
else
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_plugin.isBusyWith("dashVideo.generate") {
_obj.invokeV8Async<V8ValueString>("generate");
}
});
return plugin.busy {
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_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? { override open fun generate(): String? {
if(!hasGenerate) if(!hasGenerate)
return manifest; return manifest;
@ -117,6 +163,32 @@ class JSDashManifestMergingRawSource(
override val priority: Boolean override val priority: Boolean
get() = video.priority; get() = video.priority;
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
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(
"</AdaptationSet>",
"</AdaptationSet>\n" + audioAdaptationSet.value
)
} else
result = videoDash;
return@merge result;
};
}
override fun generate(): String? { override fun generate(): String? {
val videoDash = video.generate(); val videoDash = video.generate();
val audioDash = audio.generate(); val audioDash = audio.generate();

View file

@ -277,7 +277,7 @@ class V8Plugin {
} }
suspend fun <T : V8Value> executeTypedAsync(js: String) : Deferred<T> { suspend fun <T : V8Value> executeTypedAsync(js: String) : Deferred<T> {
warnIfMainThread("V8Plugin.executeTyped"); warnIfMainThread("V8Plugin.executeTypedAsync");
if(isStopped) if(isStopped)
throw PluginEngineStoppedException(config, "Instance is stopped", js); throw PluginEngineStoppedException(config, "Instance is stopped", js);
@ -358,7 +358,7 @@ class V8Plugin {
_promises.remove(promise); _promises.remove(promise);
return@synchronized found; return@synchronized found;
}; };
if(found != null) if(found != null && cancelled)
found(promise); found(promise);
} }
fun cancelAllPromises(){ fun cancelAllPromises(){

View file

@ -567,11 +567,13 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
if(videoSource.hasGenerate) { if(videoSource.hasGenerate) {
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
val scope = this;
var startId = -1; var startId = -1;
try { try {
val plugin = videoSource.getUnderlyingPlugin() ?: return@launch; val plugin = videoSource.getUnderlyingPlugin() ?: return@launch;
startId = plugin.getUnderlyingPlugin()?.runtimeId ?: -1; 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) { if (generated != null) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource)) val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource))