From ace7ca15511391387cab5976a6489fd93544b5ac Mon Sep 17 00:00:00 2001 From: Koen Date: Mon, 27 Apr 2026 10:14:01 +0000 Subject: [PATCH] Possible busy fix --- .../com/futo/platformplayer/Extensions_V8.kt | 104 +++++---- .../platformplayer/activities/MainActivity.kt | 2 +- .../api/media/platforms/js/JSClient.kt | 87 ++++---- .../media/platforms/js/models/JSArticle.kt | 3 +- .../platforms/js/models/JSArticleDetails.kt | 58 ++++-- .../media/platforms/js/models/JSComment.kt | 46 ++-- .../media/platforms/js/models/JSContent.kt | 4 +- .../api/media/platforms/js/models/JSPager.kt | 6 +- .../platforms/js/models/JSPostDetails.kt | 70 +++++-- .../platforms/js/models/JSRequestExecutor.kt | 32 +-- .../platforms/js/models/JSRequestModifier.kt | 10 +- .../platforms/js/models/JSSubtitleSource.kt | 30 ++- .../platforms/js/models/JSVideoDetails.kt | 85 +++++--- .../js/models/sources/JSAudioUrlSource.kt | 4 +- .../sources/JSAudioUrlWidevineSource.kt | 22 +- .../sources/JSDashManifestRawAudioSource.kt | 8 +- .../models/sources/JSDashManifestRawSource.kt | 8 +- .../sources/JSDashManifestWidevineSource.kt | 24 ++- .../platforms/js/models/sources/JSSource.kt | 27 ++- .../sources/JSVideoUrlWidevineSource.kt | 24 ++- .../platformplayer/downloads/VideoDownload.kt | 10 +- .../futo/platformplayer/engine/V8Plugin.kt | 197 +++++++++++++----- .../engine/packages/PackageBridge.kt | 6 +- .../engine/packages/PackageHttp.kt | 88 ++++---- .../platformplayer/states/StatePlatform.kt | 32 ++- .../states/StateSubscriptions.kt | 3 +- .../SubscriptionsTaskFetchAlgorithm.kt | 12 +- 27 files changed, 662 insertions(+), 340 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 1da4b6fe..3bea13dd 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt @@ -118,14 +118,13 @@ inline fun V8ValueArray.expectV8Variants(config: IV8PluginConfig, co inline fun V8Plugin.ensureIsBusy() { this.let { if (!it.isThreadAlreadyBusy()) { - //throw IllegalStateException("Tried to access V8Plugin without busy"); val stacktrace = Thread.currentThread().stackTrace; - Logger.w("Extensions_V8", - "V8 USE OUTSIDE BUSY: " + stacktrace.drop(3)?.firstOrNull().toString() + - ", " + stacktrace.drop(4)?.firstOrNull().toString() + - ", " + stacktrace.drop(5)?.firstOrNull()?.toString() + - ", " + stacktrace.drop(6)?.firstOrNull()?.toString() - ); + val message = "V8 USE OUTSIDE BUSY: " + stacktrace.drop(3)?.firstOrNull().toString() + + ", " + stacktrace.drop(4)?.firstOrNull().toString() + + ", " + stacktrace.drop(5)?.firstOrNull()?.toString() + + ", " + stacktrace.drop(6)?.firstOrNull()?.toString(); + Logger.w("Extensions_V8", message); + throw IllegalStateException(message); } } } @@ -136,8 +135,7 @@ inline fun V8Value.ensureIsBusy() { } inline fun V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T { - if(false) - ensureIsBusy(); + ensureIsBusy(); return when(T::class) { String::class -> this.expectOrThrow(config, contextName).value as T; Int::class -> { @@ -186,10 +184,14 @@ inline fun V8Value.expectV8Variant(config: IV8PluginConfig, contextN else -> throw NotImplementedError("Type ${T::class.simpleName} not implemented conversion"); } } -fun V8ArrayToStringList(obj: V8ValueArray): List = obj.keys.map { obj.getString(it) }; +fun V8ArrayToStringList(obj: V8ValueArray): List { + obj.ensureIsBusy(); + return obj.keys.map { obj.getString(it) }; +} fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap { if(obj == null) return hashMapOf(); + obj.ensureIsBusy(); val map = hashMapOf(); for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get(it).toString() }) map.put(prop, obj.getString(prop)); @@ -203,21 +205,27 @@ fun V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T { plugin.busy { this.register(object: IV8ValuePromise.IListener { override fun onFulfilled(p0: V8Value?) { - if(p0 is V8ValueError) - promiseException = ScriptExecutionException(plugin.config, p0.message); - else { - if(p0 is V8ValueObject) - p0.setWeak(); - promiseResult = p0 as T; + plugin.busy { + if(p0 is V8ValueError) + promiseException = ScriptExecutionException(plugin.config, p0.message); + else { + if(p0 is V8ValueObject) + p0.setWeak(); + promiseResult = p0 as T; + } } latch.countDown(); } override fun onRejected(p0: V8Value?) { - promiseException = p0?.toException(plugin.config); + plugin.busy { + promiseException = p0?.toException(plugin.config); + } latch.countDown(); } override fun onCatch(p0: V8Value?) { - promiseException = p0?.toException(plugin.config); + plugin.busy { + promiseException = p0?.toException(plugin.config); + } latch.countDown(); } }); @@ -229,20 +237,23 @@ fun V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T { } //Logger.i("V8", "V8ValueBlocking started (Busy) [" + blockCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString()); - - if(!promise.isPending) { - try { - Logger.i("V8", "V8Promise resolved synchronously"); - if(promise.isFulfilled) - promiseResult = promise.getResult(); - else - promiseException = promise.getResult().toException(plugin.config); + val isPending = plugin.busy { + promise.isPending + }; + if(!isPending) { + plugin.busy { + try { + Logger.i("V8", "V8Promise resolved synchronously"); + if(promise.isFulfilled) + promiseResult = promise.getResult(); + else + promiseException = promise.getResult().toException(plugin.config); + } + catch(ex: Throwable) { + promiseException = ex; + } } - catch(ex: Throwable) { - promiseException = ex; - } - } - else { + } else { plugin.unbusy { latch.await(); } @@ -266,15 +277,19 @@ fun V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred plugin.busy { this.register(object: IV8ValuePromise.IListener { override fun onFulfilled(p0: V8Value?) { - plugin.resolvePromise(promise); - underlyingDef.complete(p0 as T); + plugin.busy { + plugin.resolvePromise(promise); + underlyingDef.complete(p0 as T); + } } override fun onRejected(p0: V8Value?) { try { - plugin.resolvePromise(promise); - val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onRejected promise not implemented.."); - Logger.i("V8", "Promise rejected, setting exception"); - underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound)); + plugin.busy { + plugin.resolvePromise(promise); + val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onRejected promise not implemented.."); + Logger.i("V8", "Promise rejected, setting exception"); + underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound)); + } } catch(ex: Throwable) { Logger.e("V8", "Rejection handling failed?" , ex); @@ -282,9 +297,11 @@ fun V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred } override fun onCatch(p0: V8Value?) { try { - plugin.resolvePromise(promise); - val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onCatch promise not implemented.."); - underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound)); + plugin.busy { + plugin.resolvePromise(promise); + val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onCatch promise not implemented.."); + underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound)); + } } catch(ex: Throwable) { Logger.e("V8", "Catching handling failed?" , ex); @@ -300,6 +317,7 @@ fun V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred } fun V8Value.toException(config: IV8PluginConfig): Throwable { + ensureIsBusy(); val p0 = this; if(p0 is V8ValueObject) { return V8Plugin.getExceptionFromPlugin(config, p0, null, null, null, "P:"); @@ -349,6 +367,7 @@ class V8Deferred(val deferred: Deferred, val estDuration: Int = -1): Defer fun V8ValueObject.invokeV8(method: String, vararg obj: Any?): T { + ensureIsBusy(); var result = this.invoke(method, *obj); if(result is V8ValuePromise) { return result.toV8ValueBlocking(this.getSourcePlugin()!!); @@ -356,6 +375,7 @@ fun V8ValueObject.invokeV8(method: String, vararg obj: Any?): T { return result as T; } fun V8ValueObject.invokeV8Async(method: String, vararg obj: Any?): V8Deferred { + ensureIsBusy(); var result = this.invoke(method, *obj); if(result is V8ValuePromise) { return result.toV8ValueAsync(this.getSourcePlugin()!!); @@ -363,6 +383,7 @@ fun V8ValueObject.invokeV8Async(method: String, vararg obj: Any?): return V8Deferred(CompletableDeferred(result as T)); } fun V8ValueObject.invokeV8Void(method: String, vararg obj: Any?): V8Value { + ensureIsBusy(); var result = this.invoke(method, *obj); if(result is V8ValuePromise) { return result.toV8ValueBlocking(this.getSourcePlugin()!!); @@ -370,6 +391,7 @@ fun V8ValueObject.invokeV8Void(method: String, vararg obj: Any?): V8Value { return result; } fun V8ValueObject.invokeV8VoidAsync(method: String, vararg obj: Any?): V8Deferred { + ensureIsBusy(); var result = this.invoke(method, *obj); if(result is V8ValuePromise) { val result = result.toV8ValueAsync(this.getSourcePlugin()!!); @@ -399,4 +421,4 @@ fun IPager.toList(): List { } return list.toList(); -} \ No newline at end of file +} diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index 891d4fd5..e6f2c48c 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -1543,4 +1543,4 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { return sourcesIntent; } } -} \ 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 f2c53733..bb684201 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 @@ -488,13 +488,14 @@ open class JSClient : IPlatformClient { if (_peekChannelTypes != null) { return _peekChannelTypes!!; } - val arr: V8ValueArray = plugin.executeTyped("source.getPeekChannelTypes()"); - - _peekChannelTypes = arr.keys.mapNotNull { - val str = arr.get(it); - return@mapNotNull str.value; - }; - return _peekChannelTypes ?: listOf(); + return busy { + val arr: V8ValueArray = plugin.executeTyped("source.getPeekChannelTypes()"); + _peekChannelTypes = arr.keys.mapNotNull { + val str = arr.get(it); + return@mapNotNull str.value; + }; + return@busy _peekChannelTypes ?: listOf(); + } } catch(ex: Throwable) { announcePluginUnhandledException("getPeekChannelTypes", ex); @@ -523,10 +524,12 @@ open class JSClient : IPlatformClient { if(!capabilities.hasGetChannelUrlByClaim) throw IllegalStateException("This plugin does not support channel url by claim"); - val value = plugin.executeTyped("source.getChannelUrlByClaim(${claimType}, ${Json.encodeToString(claimValues)})"); - if(value !is V8ValueString) - return null; - return value.value; + return busy { + val value = plugin.executeTyped("source.getChannelUrlByClaim(${claimType}, ${Json.encodeToString(claimValues)})"); + if(value !is V8ValueString) + return@busy null; + return@busy value.value; + } } @JSOptional @JSDocs(12, "source.getChannelTemplateByClaimMap()", "Get a map for every supported claimtype mapping field to urls") @@ -536,28 +539,30 @@ open class JSClient : IPlatformClient { if(!capabilities.hasGetChannelTemplateByClaimMap) throw IllegalStateException("This plugin does not support channel template by claim map"); - val value = plugin.executeTyped("source.getChannelTemplateByClaimMap()"); - if(value !is V8ValueObject) - return mapOf(); + return busy { + val value = plugin.executeTyped("source.getChannelTemplateByClaimMap()"); + if(value !is V8ValueObject) + return@busy mapOf(); - val claimTypes = mutableMapOf>(); + val claimTypes = mutableMapOf>(); - val keys = value.ownPropertyNames; - for(key in keys.toArray()) { - if(key is V8ValueInteger) { - val map = value.get(key); - val mapKeys = map.ownPropertyNames; + val keys = value.ownPropertyNames; + for(key in keys.toArray()) { + if(key is V8ValueInteger) { + val map = value.get(key); + val mapKeys = map.ownPropertyNames; - claimTypes[key.value] = mapKeys.toArray().filter { - it is V8ValueInteger - }.associate { - val mapKey = (it as V8ValueInteger).value; - return@associate Pair(mapKey, map.getString(mapKey)); - }; + claimTypes[key.value] = mapKeys.toArray().filter { + it is V8ValueInteger + }.associate { + val mapKey = (it as V8ValueInteger).value; + return@associate Pair(mapKey, map.getString(mapKey)); + }; + } } + channelClaimTemplates = claimTypes.toMap(); + return@busy claimTypes; } - channelClaimTemplates = claimTypes.toMap(); - return claimTypes; } @@ -698,27 +703,33 @@ open class JSClient : IPlatformClient { @JSDocs(22, "source.getUserPlaylists()", "Gets the playlist of the current user") override fun getUserPlaylists(): Array { ensureEnabled(); - return plugin.executeTyped("source.getUserPlaylists()") - .toArray() - .map { (it as V8ValueString).value } - .toTypedArray(); + return busy { + return@busy plugin.executeTyped("source.getUserPlaylists()") + .toArray() + .map { (it as V8ValueString).value } + .toTypedArray(); + } } @JSOptional @JSDocs(23, "source.getUserSubscriptions()", "Gets the subscriptions of the current user") override fun getUserSubscriptions(): Array { ensureEnabled(); - return plugin.executeTyped("source.getUserSubscriptions()") - .toArray() - .map { (it as V8ValueString).value } - .toTypedArray(); + return busy { + return@busy plugin.executeTyped("source.getUserSubscriptions()") + .toArray() + .map { (it as V8ValueString).value } + .toTypedArray(); + } } @JSOptional @JSDocs(23, "source.getUserHistory()", "Gets the history of the current user") override fun getUserHistory(): IPager { ensureEnabled(); - return JSContentPager(config, this, plugin.executeTyped("source.getUserHistory()")); + return isBusyWith("getUserHistory") { + return@isBusyWith JSContentPager(config, this, plugin.executeTyped("source.getUserHistory()")); + } } fun validate() { @@ -894,4 +905,4 @@ open class JSClient : IPlatformClient { return name; } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSArticle.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSArticle.kt index e5b9f353..4de5d29c 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSArticle.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSArticle.kt @@ -21,6 +21,7 @@ import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrowNullableList +import com.futo.platformplayer.getSourcePlugin import com.futo.platformplayer.states.StateDeveloper open class JSArticle( @@ -34,7 +35,7 @@ open class JSArticle( obj.getOrDefault(config, "summary", "PlatformArticle", "") ?: "" override val thumbnails: Thumbnails? = - if (obj.has("thumbnails")) + if (obj.getSourcePlugin()?.busy { obj.has("thumbnails") } ?: obj.has("thumbnails")) Thumbnails.fromV8( config, obj.getOrThrow(config, "thumbnails", "PlatformArticle") diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSArticleDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSArticleDetails.kt index 1d175e5a..35459921 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSArticleDetails.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSArticleDetails.kt @@ -31,18 +31,20 @@ open class JSArticleDetails( final override val contentType: ContentType = ContentType.ARTICLE - private val _hasGetComments: Boolean = _content.has("getComments") - private val _hasGetContentRecommendations: Boolean = _content.has("getContentRecommendations") + private val _hasGetComments: Boolean = client.busy { _content.has("getComments") } + private val _hasGetContentRecommendations: Boolean = client.busy { _content.has("getContentRecommendations") } - override val rating: IRating = + override val rating: IRating = client.busy { obj.getOrDefault(client.config, "rating", "PlatformArticle", null) ?.let { IRating.fromV8(client.config, it, "PlatformArticle") } ?: RatingLikes(0) + } - override val summary: String = + override val summary: String = client.busy { _content.getOrThrow(client.config, "summary", "PlatformArticle") + } - override val thumbnails: Thumbnails? = + override val thumbnails: Thumbnails? = client.busy { if (_content.has("thumbnails")) Thumbnails.fromV8( client.config, @@ -50,14 +52,19 @@ open class JSArticleDetails( ) else null + } - override val segments: List = + override val segments: List = client.busy { obj.getOrThrowNullableList(client.config, "segments", "PlatformArticle") ?.mapNotNull { fromV8Segment(client, it) } ?: emptyList() + } override fun getComments(client: IPlatformClient): IPager? { - if(!_hasGetComments || _content.isClosed) + val canGetComments = this.client.busy { + _hasGetComments && !_content.isClosed + } + if(!canGetComments) return null; if(client is DevJSClient) @@ -73,7 +80,10 @@ open class JSArticleDetails( override fun getPlaybackTracker(): IPlaybackTracker? = null; override fun getContentRecommendations(client: IPlatformClient): IPager? { - if(!_hasGetContentRecommendations || _content.isClosed) + val canGetContentRecommendations = this.client.busy { + _hasGetContentRecommendations && !_content.isClosed + } + if(!canGetContentRecommendations) return null; if(client is DevJSClient) @@ -87,25 +97,31 @@ open class JSArticleDetails( } private fun getContentRecommendationsJS(client: JSClient): JSContentPager { - val contentPager = _content.invokeV8("getContentRecommendations", arrayOf()); - return JSContentPager(_pluginConfig, client, contentPager); + return client.busy { + val contentPager = _content.invokeV8("getContentRecommendations", arrayOf()); + return@busy JSContentPager(_pluginConfig, client, contentPager); + } } private fun getCommentsJS(client: JSClient): JSCommentPager { - val commentPager = _content.invokeV8("getComments", arrayOf()); - return JSCommentPager(_pluginConfig, client, commentPager); + return client.busy { + val commentPager = _content.invokeV8("getComments", arrayOf()); + return@busy JSCommentPager(_pluginConfig, client, commentPager); + } } companion object { fun fromV8Segment(client: JSClient, obj: V8ValueObject): IJSArticleSegment? { - if(!obj.has("type")) - throw IllegalArgumentException("Object missing type field"); - return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) { - SegmentType.TEXT -> JSTextSegment(client, obj); - SegmentType.IMAGES -> JSImagesSegment(client, obj); - SegmentType.HEADER -> JSHeaderSegment(client, obj); - SegmentType.NESTED -> JSNestedSegment(client, obj); - else -> null; + return client.busy { + if(!obj.has("type")) + throw IllegalArgumentException("Object missing type field"); + return@busy when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) { + SegmentType.TEXT -> JSTextSegment(client, obj); + SegmentType.IMAGES -> JSImagesSegment(client, obj); + SegmentType.HEADER -> JSHeaderSegment(client, obj); + SegmentType.NESTED -> JSNestedSegment(client, obj); + else -> null; + } } } } @@ -176,4 +192,4 @@ class JSNestedSegment: IJSArticleSegment { val nestedObj = obj.getOrThrow(client.config, "nested", contextName, false); nested = IJSContent.fromV8(client, nestedObj); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSComment.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSComment.kt index 7767ef78..fa2d98ac 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSComment.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSComment.kt @@ -46,23 +46,45 @@ class JSComment : IPlatformComment { _comment = obj; _plugin = plugin; - val contextName = "Comment"; - contextUrl = _comment!!.getOrThrow(config, "contextUrl", contextName); - author = PlatformAuthorLink.fromV8(_config!!, _comment!!.getOrThrow(config, "author", contextName)); - message = _comment!!.getOrThrow(config, "message", contextName); - rating = IRating.fromV8(config, _comment!!.getOrThrow(config, "rating", contextName)); - date = _comment!!.getOrThrowNullable(config, "date", contextName)?.let { OffsetDateTime.of(LocalDateTime.ofEpochSecond(it.toLong(), 0, ZoneOffset.UTC), ZoneOffset.UTC) } - replyCount = _comment!!.getOrThrowNullable(config, "replyCount", contextName); - context = _comment!!.getOrDefault(config, "context", contextName, hashMapOf()) ?: hashMapOf(); - _hasGetReplies = _comment!!.has("getReplies"); + var parsedContextUrl: String? = null; + var parsedAuthor: PlatformAuthorLink? = null; + var parsedMessage: String? = null; + var parsedRating: IRating? = null; + var parsedDate: OffsetDateTime? = null; + var parsedReplyCount: Int? = null; + var parsedContext: Map? = null; + var parsedHasGetReplies = false; + + plugin.busy { + val contextName = "Comment"; + parsedContextUrl = _comment!!.getOrThrow(config, "contextUrl", contextName); + parsedAuthor = PlatformAuthorLink.fromV8(_config!!, _comment!!.getOrThrow(config, "author", contextName)); + parsedMessage = _comment!!.getOrThrow(config, "message", contextName); + parsedRating = IRating.fromV8(config, _comment!!.getOrThrow(config, "rating", contextName)); + parsedDate = _comment!!.getOrThrowNullable(config, "date", contextName)?.let { OffsetDateTime.of(LocalDateTime.ofEpochSecond(it.toLong(), 0, ZoneOffset.UTC), ZoneOffset.UTC) }; + parsedReplyCount = _comment!!.getOrThrowNullable(config, "replyCount", contextName); + parsedContext = _comment!!.getOrDefault(config, "context", contextName, hashMapOf()) ?: hashMapOf(); + parsedHasGetReplies = _comment!!.has("getReplies"); + } + + contextUrl = parsedContextUrl ?: ""; + author = parsedAuthor ?: PlatformAuthorLink.UNKNOWN; + message = parsedMessage ?: ""; + rating = parsedRating ?: throw IllegalStateException("Missing comment rating"); + date = parsedDate; + replyCount = parsedReplyCount; + context = parsedContext ?: hashMapOf(); + _hasGetReplies = parsedHasGetReplies; } override fun getReplies(client: IPlatformClient): IPager? { if(!_hasGetReplies) return null; - val obj = _comment!!.invokeV8("getReplies", arrayOf()); val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient"); - return JSCommentPager(_config!!, plugin, obj); + return plugin.busy { + val obj = _comment!!.invokeV8("getReplies", arrayOf()); + return@busy JSCommentPager(_config!!, plugin, obj); + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContent.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContent.kt index a3b5c34b..7743154b 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContent.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSContent.kt @@ -11,6 +11,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.decodeUnicode import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow +import com.futo.platformplayer.getSourcePlugin import com.futo.platformplayer.logging.Logger import java.time.LocalDateTime import java.time.OffsetDateTime @@ -23,7 +24,8 @@ open class JSContent( override val contentType: ContentType = ContentType.UNKNOWN - protected val _hasGetDetails: Boolean = _content.has("getDetails") + protected val _hasGetDetails: Boolean = + _content.getSourcePlugin()?.busy { _content.has("getDetails") } ?: false override val id: PlatformID = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(_pluginConfig, "id", CTX)) 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 d4aecc16..b757c8bb 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 @@ -41,7 +41,9 @@ abstract class JSPager : IPager { } override fun hasMorePages(): Boolean { - return _hasMorePages && !pager.isClosed; + return plugin.getUnderlyingPlugin().busy { + _hasMorePages && !pager.isClosed; + } } override fun nextPage() { @@ -91,4 +93,4 @@ abstract class JSPager : IPager { } abstract fun convertResult(obj: V8ValueObject): T; -} \ No newline at end of file +} diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt index 4d48a354..48ec583c 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPostDetails.kt @@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.getOrDefault +import com.futo.platformplayer.getSourcePlugin import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.states.StateDeveloper @@ -30,52 +31,79 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails { constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) { - val contextName = "PlatformPostDetails"; + var parsedRating: IRating? = null; + var parsedTextType: TextType? = null; + var parsedContent: String? = null; + var parsedHasGetComments = false; + var parsedHasGetContentRecommendations = false; - rating = obj.getOrDefault(config, "rating", contextName, null)?.let { IRating.fromV8(config, it, contextName) } ?: RatingLikes(0); - textType = TextType.fromInt((obj.getOrDefault(config, "textType", contextName, null) ?: 0)); - content = obj.getOrDefault(config, "content", contextName, "") ?: ""; + val parse = { + val contextName = "PlatformPostDetails"; - _hasGetComments = _content.has("getComments"); - _hasGetContentRecommendations = _content.has("getContentRecommendations"); + parsedRating = obj.getOrDefault(config, "rating", contextName, null)?.let { IRating.fromV8(config, it, contextName) } ?: RatingLikes(0); + parsedTextType = TextType.fromInt((obj.getOrDefault(config, "textType", contextName, null) ?: 0)); + parsedContent = obj.getOrDefault(config, "content", contextName, "") ?: ""; + + parsedHasGetComments = _content.has("getComments"); + parsedHasGetContentRecommendations = _content.has("getContentRecommendations"); + }; + obj.getSourcePlugin()?.busy { + parse(); + } ?: parse() + + rating = parsedRating ?: RatingLikes(0); + textType = parsedTextType ?: TextType.RAW; + content = parsedContent ?: ""; + _hasGetComments = parsedHasGetComments; + _hasGetContentRecommendations = parsedHasGetContentRecommendations; } override fun getComments(client: IPlatformClient): IPager? { - if(!_hasGetComments || _content.isClosed) + val jsClient = client as? JSClient; + if(jsClient == null) + return null; + val canGetComments = jsClient.busy { + _hasGetComments && !_content.isClosed + } + if(!canGetComments) return null; if(client is DevJSClient) return StateDeveloper.instance.handleDevCall(client.devID, "videoDetail.getComments()") { return@handleDevCall getCommentsJS(client); } - else if(client is JSClient) - return getCommentsJS(client); - - return null; + return getCommentsJS(jsClient); } override fun getPlaybackTracker(): IPlaybackTracker? = null; override fun getContentRecommendations(client: IPlatformClient): IPager? { - if(!_hasGetContentRecommendations || _content.isClosed) + val jsClient = client as? JSClient; + if(jsClient == null) + return null; + val canGetContentRecommendations = jsClient.busy { + _hasGetContentRecommendations && !_content.isClosed + } + if(!canGetContentRecommendations) return null; if(client is DevJSClient) return StateDeveloper.instance.handleDevCall(client.devID, "postDetail.getContentRecommendations()") { return@handleDevCall getContentRecommendationsJS(client); } - else if(client is JSClient) - return getContentRecommendationsJS(client); - - return null; + return getContentRecommendationsJS(jsClient); } private fun getContentRecommendationsJS(client: JSClient): JSContentPager { - val contentPager = _content.invokeV8("getContentRecommendations", arrayOf()); - return JSContentPager(_pluginConfig, client, contentPager); + return client.busy { + val contentPager = _content.invokeV8("getContentRecommendations", arrayOf()); + return@busy JSContentPager(_pluginConfig, client, contentPager); + } } private fun getCommentsJS(client: JSClient): JSCommentPager { - val commentPager = _content.invokeV8("getComments", arrayOf()); - return JSCommentPager(_pluginConfig, client, commentPager); + return client.busy { + val commentPager = _content.invokeV8("getComments", arrayOf()); + return@busy JSCommentPager(_pluginConfig, client, commentPager); + } } -} \ No newline at end of file +} 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 5a41d0a2..6be3b438 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 @@ -41,20 +41,26 @@ class JSRequestExecutor: AutoCloseable { this._config = plugin.config; val config = plugin.config; - urlPrefix = executor.getOrDefault(config, "urlPrefix", "RequestExecutor", null); + var parsedUrlPrefix: String? = null; + var parsedHasCleanup = false; + plugin.busy { + parsedUrlPrefix = executor.getOrDefault(config, "urlPrefix", "RequestExecutor", null); - if(!executor.has("executeRequest")) - throw ScriptImplementationException(config, "RequestExecutor is missing executeRequest", null); - hasCleanup = executor.has("cleanup"); + if(!executor.has("executeRequest")) + throw ScriptImplementationException(config, "RequestExecutor is missing executeRequest", null); + parsedHasCleanup = executor.has("cleanup"); + } + + urlPrefix = parsedUrlPrefix; + hasCleanup = parsedHasCleanup; } //TODO: Executor properties? @Throws(ScriptException::class) open fun executeRequest(method: String, url: String, body: ByteArray?, headers: Map): ByteArray { - if (_executor.isClosed) - throw IllegalStateException("Executor object is closed"); - return _plugin.getUnderlyingPlugin().busy { + if (_executor.isClosed) + throw IllegalStateException("Executor object is closed"); val result = if(_plugin is DevJSClient) StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") { @@ -108,10 +114,12 @@ class JSRequestExecutor: AutoCloseable { open fun cleanup() { - synchronized(_cleanLock) { - if (!hasCleanup || _executor.isClosed || _cleaned) - return; - _cleaned = true; + _plugin.busy { + synchronized(_cleanLock) { + if (!hasCleanup || _executor.isClosed || _cleaned) + return@busy; + _cleaned = true; + } } Logger.i("JSRequestExecutor", "JSRequestExecutor cleanup requested"); _plugin.busy { @@ -163,4 +171,4 @@ class ExecutorParameters { var rangeEnd: Int = -1; var segment: Int = -1; -} \ No newline at end of file +} 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 af03d070..2cb20217 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 @@ -36,11 +36,11 @@ class JSRequestModifier: IRequestModifier { } override fun modifyRequest(url: String, headers: Map): IRequest { - if (_modifier.isClosed) { - return Request(url, headers); - } - return _plugin.busy { + if (_modifier.isClosed) { + return@busy Request(url, headers); + } + val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") { _modifier.invokeV8("modifyRequest", url, headers); } as V8ValueObject; @@ -53,4 +53,4 @@ class JSRequestModifier: IRequestModifier { data class Request(override val url: String, override val headers: Map) : IRequest; -} \ No newline at end of file +} diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSSubtitleSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSSubtitleSource.kt index 70cf197c..e58f7683 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSSubtitleSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSSubtitleSource.kt @@ -29,12 +29,28 @@ class JSSubtitleSource : ISubtitleSource { constructor(config: SourcePluginConfig, v8Value: V8ValueObject) { _obj = v8Value; - val context = "JSSubtitles"; - name = v8Value.getOrThrow(config, "name", context, false); - language = v8Value.getOrDefault(config, "language", context, null); - url = v8Value.getOrThrow(config, "url", context, true); - format = v8Value.getOrThrow(config, "format", context, true); - hasFetch = v8Value.has("getSubtitles"); + var parsedName: String? = null; + var parsedLanguage: String? = null; + var parsedUrl: String? = null; + var parsedFormat: String? = null; + var parsedHasFetch = false; + val parse = { + val context = "JSSubtitles"; + parsedName = v8Value.getOrThrow(config, "name", context, false); + parsedLanguage = v8Value.getOrDefault(config, "language", context, null); + parsedUrl = v8Value.getOrThrow(config, "url", context, true); + parsedFormat = v8Value.getOrThrow(config, "format", context, true); + parsedHasFetch = v8Value.has("getSubtitles"); + }; + v8Value.getSourcePlugin()?.busy { + parse(); + } ?: parse() + + name = parsedName ?: ""; + language = parsedLanguage; + url = parsedUrl; + format = parsedFormat; + hasFetch = parsedHasFetch; } override fun getSubtitles(): String { @@ -69,4 +85,4 @@ class JSSubtitleSource : ISubtitleSource { return JSSubtitleSource(config, value); } } -} \ No newline at end of file +} 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 4aca63aa..1b8a5100 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 @@ -52,34 +52,63 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { override val subtitles: List; 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)); - dash = JSSource.fromV8DashNullable(plugin, _content.getOrThrowNullable(config, "dash", contextName)); - hls = JSSource.fromV8HLSNullable(plugin, _content.getOrThrowNullable(config, "hls", contextName)); - live = JSSource.fromV8VideoNullable(plugin, _content.getOrThrowNullable(config, "live", contextName)); - rating = IRating.fromV8OrDefault(config, _content.getOrDefault(config, "rating", contextName, null), RatingLikes(0)); + var parsedDescription: String? = null; + var parsedVideo: IVideoSourceDescriptor? = null; + var parsedDash: IDashManifestSource? = null; + var parsedHls: IHLSManifestSource? = null; + var parsedLive: IVideoSource? = null; + var parsedRating: IRating? = null; + var parsedSubtitles: List? = null; + var parsedHasGetComments = false; + var parsedHasGetPlaybackTracker = false; + var parsedHasGetContentRecommendations = false; + var parsedHasGetVODEvents = false; - if(!_content.has("subtitles")) - subtitles = listOf(); - else { - val subArrs = _content.getOrThrowNullable(config, "subtitles", contextName); - if(subArrs != null) - subtitles = subArrs.keys.map { JSSubtitleSource.fromV8(config, subArrs.get(it)) }; - else - subtitles = listOf(); + plugin.busy { + val contextName = "VideoDetails"; + val config = plugin.config; + parsedDescription = _content.getOrThrow(config, "description", contextName); + parsedVideo = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName)); + parsedDash = JSSource.fromV8DashNullable(plugin, _content.getOrThrowNullable(config, "dash", contextName)); + parsedHls = JSSource.fromV8HLSNullable(plugin, _content.getOrThrowNullable(config, "hls", contextName)); + parsedLive = JSSource.fromV8VideoNullable(plugin, _content.getOrThrowNullable(config, "live", contextName)); + parsedRating = IRating.fromV8OrDefault(config, _content.getOrDefault(config, "rating", contextName, null), RatingLikes(0)); + + if(!_content.has("subtitles")) + parsedSubtitles = listOf(); + else { + val subArrs = _content.getOrThrowNullable(config, "subtitles", contextName); + if(subArrs != null) + parsedSubtitles = subArrs.keys.map { JSSubtitleSource.fromV8(config, subArrs.get(it)) }; + else + parsedSubtitles = listOf(); + } + + parsedHasGetComments = _content.has("getComments"); + parsedHasGetPlaybackTracker = _content.has("getPlaybackTracker"); + parsedHasGetContentRecommendations = _content.has("getContentRecommendations"); + parsedHasGetVODEvents = _content.has("getVODEvents"); } - _hasGetComments = _content.has("getComments"); - _hasGetPlaybackTracker = _content.has("getPlaybackTracker"); - _hasGetContentRecommendations = _content.has("getContentRecommendations"); - _hasGetVODEvents = _content.has("getVODEvents"); + description = parsedDescription ?: ""; + video = parsedVideo ?: throw IllegalStateException("Missing video source descriptor"); + dash = parsedDash; + hls = parsedHls; + live = parsedLive; + rating = parsedRating ?: RatingLikes(0); + subtitles = parsedSubtitles ?: listOf(); + _hasGetComments = parsedHasGetComments; + _hasGetPlaybackTracker = parsedHasGetPlaybackTracker; + _hasGetContentRecommendations = parsedHasGetContentRecommendations; + _hasGetVODEvents = parsedHasGetVODEvents; } override fun getPlaybackTracker(): IPlaybackTracker? { - if(!_hasGetPlaybackTracker || _content.isClosed) + val canGetPlaybackTracker = _plugin.busy { + _hasGetPlaybackTracker && !_content.isClosed + } + if(!canGetPlaybackTracker) return null; if(_pluginConfig.id == StateDeveloper.DEV_ID) return StateDeveloper.instance.handleDevCall(_pluginConfig.id, "videoDetail.getComments()") { @@ -102,7 +131,10 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { } override fun getContentRecommendations(client: IPlatformClient): IPager? { - if(!_hasGetContentRecommendations || _content.isClosed) + val canGetContentRecommendations = _plugin.busy { + _hasGetContentRecommendations && !_content.isClosed + } + if(!canGetContentRecommendations) return null; if(client is DevJSClient) @@ -122,7 +154,12 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { } override fun getComments(client: IPlatformClient): IPager? { - if(client !is JSClient || !_hasGetComments || _content.isClosed) + if(client !is JSClient) + return null; + val canGetComments = _plugin.busy { + _hasGetComments && !_content.isClosed + } + if(!canGetComments) return null; if(client is DevJSClient) @@ -153,4 +190,4 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { return@busy JSVODEventPager(_plugin.config, _plugin, _content.invokeV8("getVODEvents", arrayOf())); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlSource.kt index 416afbbb..ebdc9568 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlSource.kt @@ -39,10 +39,10 @@ open class JSAudioUrlSource( ?: "$container $bitrate" override var priority: Boolean = - if (_obj.has("priority")) _obj.getOrThrow(cfg, "priority", ctx) else false + plugin.busy { if (_obj.has("priority")) _obj.getOrThrow(cfg, "priority", ctx) else false } override var original: Boolean = - if (_obj.has("original")) _obj.getOrThrow(cfg, "original", ctx) else false + plugin.busy { if (_obj.has("original")) _obj.getOrThrow(cfg, "original", ctx) else false } override fun getAudioUrl(): String = url diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlWidevineSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlWidevineSource.kt index 7df120d5..914e2fcc 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlWidevineSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSAudioUrlWidevineSource.kt @@ -19,21 +19,23 @@ class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource { val config = plugin.config licenseUri = _obj.getOrThrow(config, "licenseUri", contextName) - hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor") + hasLicenseRequestExecutor = plugin.busy { obj.has("getLicenseRequestExecutor") } } override fun getLicenseRequestExecutor(): JSRequestExecutor? { - if (!hasLicenseRequestExecutor || _obj.isClosed) - return null + return _plugin.busy { + if (!hasLicenseRequestExecutor || _obj.isClosed) + return@busy null - val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") { - _obj.invokeV8("getLicenseRequestExecutor", arrayOf()) + val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") { + _obj.invokeV8("getLicenseRequestExecutor", arrayOf()) + } + + if (result !is V8ValueObject) + return@busy null + + return@busy JSRequestExecutor(_plugin, result) } - - if (result !is V8ValueObject) - return null - - return JSRequestExecutor(_plugin, result) } override fun toString(): String { 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 04632a06..b61acde0 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 @@ -55,7 +55,7 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false; language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN; original = obj.getOrNull(config, "original", contextName) ?: false; - hasGenerate = _obj.has("generate"); + hasGenerate = plugin.busy { _obj.has("generate") }; } private var _pregenerate: V8Deferred? = null; @@ -67,7 +67,7 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS override fun generateAsync(scope: CoroutineScope): V8Deferred { if(!hasGenerate) return V8Deferred(CompletableDeferred(manifest)); - if(_obj.isClosed) + if(_plugin.busy { _obj.isClosed }) throw IllegalStateException("Source object already closed"); val pregenerated = _pregenerate; @@ -111,7 +111,7 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS override fun generate(): String? { if(!hasGenerate) return manifest; - if(_obj.isClosed) + if(_plugin.busy { _obj.isClosed }) throw IllegalStateException("Source object already closed"); val plugin = _plugin.getUnderlyingPlugin(); @@ -145,4 +145,4 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS } return result; } -} \ No newline at end of file +} 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 463a43f9..0e6eaff1 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 @@ -73,7 +73,7 @@ open class JSDashManifestRawSource( override var manifest: String? = _obj.getOrDefault(cfg, "manifest", ctx, null) - override val hasGenerate: Boolean = _obj.has("generate") + override val hasGenerate: Boolean = plugin.busy { _obj.has("generate") } val canMerge: Boolean = _obj.getOrDefault(cfg, "canMerge", ctx, false) ?: false @@ -89,7 +89,7 @@ open class JSDashManifestRawSource( override fun generateAsync(scope: CoroutineScope): V8Deferred { if(!hasGenerate) return V8Deferred(CompletableDeferred(manifest)); - if(_obj.isClosed) + if(_plugin.busy { _obj.isClosed }) throw IllegalStateException("Source object already closed"); val pregenerated = _pregenerate; if(pregenerated != null) { @@ -133,7 +133,7 @@ open class JSDashManifestRawSource( override open fun generate(): String? { if(!hasGenerate) return manifest; - if(_obj.isClosed) + if(_plugin.busy { _obj.isClosed }) throw IllegalStateException("Source object already closed"); var result: String? = null; @@ -241,4 +241,4 @@ class JSDashManifestMergingRawSource( companion object { private val adaptationSetRegex = Regex(".*?<\\/AdaptationSet>", RegexOption.DOT_MATCHES_ALL); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestWidevineSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestWidevineSource.kt index f6ce30f3..46481201 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestWidevineSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestWidevineSource.kt @@ -42,27 +42,29 @@ class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource, priority = obj.getOrNull(config, "priority", contextName) ?: false licenseUri = _obj.getOrThrow(config, "licenseUri", contextName) - hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor") + hasLicenseRequestExecutor = plugin.busy { obj.has("getLicenseRequestExecutor") } language = _obj.getOrNull(config, "language", contextName); original = _obj.getOrNull(config, "original", contextName); } override fun getLicenseRequestExecutor(): JSRequestExecutor? { - if (!hasLicenseRequestExecutor || _obj.isClosed) - return null + return _plugin.busy { + if (!hasLicenseRequestExecutor || _obj.isClosed) + return@busy null - val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") { - _obj.invokeV8("getLicenseRequestExecutor", arrayOf()) + val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") { + _obj.invokeV8("getLicenseRequestExecutor", arrayOf()) + } + + if (result !is V8ValueObject) + return@busy null + + return@busy JSRequestExecutor(_plugin, result) } - - if (result !is V8ValueObject) - return null - - return JSRequestExecutor(_plugin, result) } override fun getVideoUrl(): String { return url } -} \ No newline at end of file +} 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 4fe4307f..7235e265 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 @@ -44,15 +44,26 @@ abstract class JSSource { this._obj = obj; this.type = type; - _requestModifier = obj.getOrDefault(_config, "requestModifier", "JSSource.requestModifier", null)?.let { - JSRequest(plugin, it, null, null, true); - } - hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier"); + var parsedRequestModifier: JSRequest? = null; + var parsedHasRequestModifier = false; + var parsedRequestExecutor: JSRequest? = null; + var parsedHasRequestExecutor = false; + plugin.busy { + parsedRequestModifier = obj.getOrDefault(_config, "requestModifier", "JSSource.requestModifier", null)?.let { + JSRequest(plugin, it, null, null, true); + }; + parsedHasRequestModifier = parsedRequestModifier != null || obj.has("getRequestModifier"); - _requestExecutor = obj.getOrDefault(_config, "requestExecutor", "JSSource.requestExecutor", null)?.let { - JSRequest(plugin, it, null, null, true); + parsedRequestExecutor = obj.getOrDefault(_config, "requestExecutor", "JSSource.requestExecutor", null)?.let { + JSRequest(plugin, it, null, null, true); + }; + parsedHasRequestExecutor = parsedRequestExecutor != null || obj.has("getRequestExecutor"); } - hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor"); + + _requestModifier = parsedRequestModifier; + hasRequestModifier = parsedHasRequestModifier; + _requestExecutor = parsedRequestExecutor; + hasRequestExecutor = parsedHasRequestExecutor; } fun getRequestModifier(): IRequestModifier? = _plugin.isBusyWith("getRequestModifier") { @@ -166,4 +177,4 @@ abstract class JSSource { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlWidevineSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlWidevineSource.kt index aff22c33..79ef8fdb 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlWidevineSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoUrlWidevineSource.kt @@ -18,25 +18,27 @@ class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource { val config = plugin.config licenseUri = _obj.getOrThrow(config, "licenseUri", contextName) - hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor") + hasLicenseRequestExecutor = plugin.busy { obj.has("getLicenseRequestExecutor") } } override fun getLicenseRequestExecutor(): JSRequestExecutor? { - if (!hasLicenseRequestExecutor || _obj.isClosed) - return null + return _plugin.busy { + if (!hasLicenseRequestExecutor || _obj.isClosed) + return@busy null - val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") { - _obj.invokeV8("getLicenseRequestExecutor", arrayOf()) + val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") { + _obj.invokeV8("getLicenseRequestExecutor", arrayOf()) + } + + if (result !is V8ValueObject) + return@busy null + + return@busy JSRequestExecutor(_plugin, result) } - - if (result !is V8ValueObject) - return null - - return JSRequestExecutor(_plugin, result) } override fun toString(): String { val url = getVideoUrl() return "(width=$width, height=$height, container=$container, codec=$codec, name=$name, bitrate=$bitrate, duration=$duration, url=$url, hasLicenseRequestExecutor=$hasLicenseRequestExecutor, licenseUri=$licenseUri)" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index 0c974052..5ddf10e9 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -139,13 +139,17 @@ class VideoDownload { @Contextual @kotlinx.serialization.Transient var videoSourceLive: JSSource? = null; - val isLiveVideoSourceValid get() = videoSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false; + val isLiveVideoSourceValid get() = videoSourceLive?.getUnderlyingPlugin()?.busy { + videoSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false; + } ?: false; var requiresLiveAudioSource: Boolean = false; @Contextual @kotlinx.serialization.Transient var audioSourceLive: JSSource? = null; - val isLiveAudioSourceValid get() = audioSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false; + val isLiveAudioSourceValid get() = audioSourceLive?.getUnderlyingPlugin()?.busy { + audioSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false; + } ?: false; var hasVideoRequestExecutor: Boolean = false; var hasAudioRequestExecutor: Boolean = false; @@ -1603,4 +1607,4 @@ class VideoDownload { } } -} \ No newline at end of file +} 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 ffe9e99a..13f58e35 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -58,6 +58,7 @@ import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.cancel import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -95,6 +96,9 @@ class V8Plugin { private val _busyLock = ReentrantLock() val isBusy get() = _busyLock.isLocked; + @Volatile + private var _busyHolder: Thread? = null; + var allowDevSubmit: Boolean = false private set(value) { field = value; @@ -161,51 +165,53 @@ class V8Plugin { fun start() { val script = _script ?: throw IllegalStateException("Attempted to start V8 without script"); - synchronized(_runtimeLock) { - if (_runtime != null) - return; - runtimeId = runtimeId + 1; - //V8RuntimeOptions.V8_FLAGS.setUseStrict(true); - val host = V8Host.getV8Instance(); - val options = host.jsRuntimeType.getRuntimeOptions(); + tryBusy(BUSY_STARTUP_MS) { + synchronized(_runtimeLock) { + if (_runtime != null) + return@tryBusy; + runtimeId = runtimeId + 1; + //V8RuntimeOptions.V8_FLAGS.setUseStrict(true); + val host = V8Host.getV8Instance(); + val options = host.jsRuntimeType.getRuntimeOptions(); - _runtime = host.createV8Runtime(options); - if (!host.isIsolateCreated) - throw IllegalStateException("Isolate not created"); + _runtime = host.createV8Runtime(options); + if (!host.isIsolateCreated) + throw IllegalStateException("Isolate not created"); - _runtimeMap.put(_runtime!!, this); + _runtimeMap.put(_runtime!!, this); - //Setup bridge - _runtime?.let { - it.converter = V8Converter(); + //Setup bridge + _runtime?.let { + it.converter = V8Converter(); - for (pack in _depsPackages) { - if (pack.variableName != null) - it.createV8ValueObject().use { v8valueObject -> - it.globalObject.set(pack.variableName, v8valueObject); - v8valueObject.bind(pack); - }; - catchScriptErrors("Package Dep[${pack.name}]") { - for (packScript in pack.getScripts()) - it.getExecutor(packScript).executeVoid(); + for (pack in _depsPackages) { + if (pack.variableName != null) + it.createV8ValueObject().use { v8valueObject -> + it.globalObject.set(pack.variableName, v8valueObject); + v8valueObject.bind(pack); + }; + catchScriptErrors("Package Dep[${pack.name}]") { + for (packScript in pack.getScripts()) + it.getExecutor(packScript).executeVoid(); + } } - } - //Load deps - for (dep in _deps) - catchScriptErrors("Dep[${dep.key}]") { - it.getExecutor(dep.value).executeVoid() + //Load deps + for (dep in _deps) + catchScriptErrors("Dep[${dep.key}]") { + it.getExecutor(dep.value).executeVoid() + }; + + + if (config.allowEval) + it.allowEval(true); + + //Load plugin + catchScriptErrors("Plugin[${config.name}]") { + it.getExecutor(script).executeVoid() }; - - - if (config.allowEval) - it.allowEval(true); - - //Load plugin - catchScriptErrors("Plugin[${config.name}]") { - it.getExecutor(script).executeVoid() - }; - isStopped = false; + isStopped = false; + } } } } @@ -254,27 +260,30 @@ class V8Plugin { fun isThreadAlreadyBusy(): Boolean { return _busyLock.isHeldByCurrentThread; } - fun busy(handle: ()->T): T { - _busyLock.lock(); - //Logger.i(TAG, "Busy Enter [" + _busyLock.holdCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString()) + fun busy(handle: ()->T): T = busyInternal(BUSY_FATAL_MS, true, "busy(enter)", handle) + + fun tryBusy(maxWaitMs: Long, handle: ()->T): T = busyInternal(maxWaitMs, false, "tryBusy(enter)", handle) + + private fun busyInternal(maxWaitMs: Long, allowUnwedge: Boolean, context: String, handle: ()->T): T { + acquireBusyOrThrow(context, maxWaitMs, allowUnwedge); + _busyHolder = Thread.currentThread(); try { return handle(); } finally { - //Logger.i(TAG, "Busy Leave [" + _busyLock.holdCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString()) - _busyLock.unlock(); + if (_busyLock.isHeldByCurrentThread) { + if (_busyLock.holdCount == 1) + _busyHolder = null; + _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; + _busyHolder = null; for(i in 1..lockCount) _busyLock.unlock(); try { @@ -283,9 +292,90 @@ class V8Plugin { } finally { Logger.w(TAG, "Relocking V8 thread for [${config.name}] for a blocking resolve of a promise") + var acquired = 0; + try { + for (i in 1..lockCount) { + acquireBusyOrThrow("unbusy(relock)"); + acquired++; + } + _busyHolder = Thread.currentThread(); + } + catch (timeout: Throwable) { + for (j in 1..acquired) + _busyLock.unlock(); + throw timeout; + } + } + } - for(i in 1..lockCount) - _busyLock.lock(); + private fun acquireBusyOrThrow(context: String, maxWaitMs: Long = BUSY_FATAL_MS, allowUnwedge: Boolean = true) { + val warnAt = Math.min(BUSY_WARN_MS, maxWaitMs); + if (_busyLock.tryLock(warnAt, TimeUnit.MILLISECONDS)) + return; + logBusyContention(context); + val remaining = maxWaitMs - warnAt; + if (remaining > 0 && _busyLock.tryLock(remaining, TimeUnit.MILLISECONDS)) + return; + if (!allowUnwedge) + throw IllegalStateException("V8 busy lock for [${config.name}] timed out after ${maxWaitMs}ms in $context (fast-fail)"); + unwedgeBusyHolder(context); + if (_busyLock.tryLock(BUSY_RECOVERY_MS, TimeUnit.MILLISECONDS)) + return; + throw IllegalStateException("V8 busy lock for [${config.name}] timed out after ${maxWaitMs + BUSY_RECOVERY_MS}ms in $context; holder did not release after recovery"); + } + + private fun unwedgeBusyHolder(context: String) { + val holder = _busyHolder; + Logger.w(TAG, "V8 busy lock for [${config.name}] still held in $context after ${BUSY_FATAL_MS}ms; attempting to unwedge holder ${holder?.name ?: "unknown"}"); + try { + val rt = _runtime; + if (rt != null && !rt.isClosed && !rt.isDead) { + Logger.w(TAG, "Calling terminateExecution() on [${config.name}] runtime"); + rt.terminateExecution(); + } + } + catch (ex: Throwable) { + Logger.e(TAG, "terminateExecution() failed for [${config.name}]", ex); + } + try { + holder?.interrupt(); + } + catch (ex: Throwable) { + Logger.e(TAG, "Interrupting holder thread for [${config.name}] failed", ex); + } + } + + private fun logBusyContention(context: String) { + try { + val holder = _busyHolder; + val sb = StringBuilder(); + sb.append("V8 BUSY CONTENTION [${config.name}] in $context: queueLength=${_busyLock.queueLength}, holdCount=${_busyLock.holdCount}, waited>${BUSY_WARN_MS}ms\n"); + if (holder != null) { + sb.append("Lock holder: ${holder.name} (id=${holder.id}, state=${holder.state})\n"); + for (frame in holder.stackTrace.take(40)) + sb.append(" at ").append(frame.toString()).append("\n"); + } else { + sb.append("Lock holder unknown (likely already released or never set)\n"); + } + sb.append("Suspect waiting/blocked threads:\n"); + val cur = Thread.currentThread(); + for ((thread, stack) in Thread.getAllStackTraces()) { + if (thread == cur || thread == holder) continue; + if (thread.state != Thread.State.WAITING && thread.state != Thread.State.BLOCKED && thread.state != Thread.State.TIMED_WAITING) continue; + if (stack.none { + val cn = it.className; + cn.contains("V8Plugin") || cn.contains("JSClient") || cn.contains("Extensions_V8") + || cn.contains("Subscription") || cn.contains("PackageHttp") || cn.contains("JSPager") + || cn.contains("JSContent") + }) continue; + sb.append(" ${thread.name} (state=${thread.state}):\n"); + for (frame in stack.take(20)) + sb.append(" at ").append(frame.toString()).append("\n"); + } + Logger.w(TAG, sb.toString()); + } + catch (ex: Throwable) { + Logger.e(TAG, "Failed to log busy contention", ex); } } fun execute(js: String) : V8Value { @@ -430,6 +520,11 @@ class V8Plugin { val TAG = "V8Plugin"; + private const val BUSY_WARN_MS = 10_000L; + private const val BUSY_FATAL_MS = 60_000L; + private const val BUSY_RECOVERY_MS = 5_000L; + const val BUSY_STARTUP_MS = 5_000L; + fun getPluginFromRuntime(runtime: V8Runtime): V8Plugin? { return _runtimeMap.getOrDefault(runtime, null); } @@ -567,4 +662,4 @@ class V8Plugin { return StateAssets.readAsset(context, path) ?: throw java.lang.IllegalStateException("script ${path} not found"); } } -} \ No newline at end of file +} 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 ca2399f3..08285278 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 @@ -128,7 +128,9 @@ class PackageBridge : V8Package { @V8Function fun dispose(value: V8Value) { Logger.e(TAG, "Manual dispose: " + value.javaClass.name); - value.close(); + _plugin.busy { + value.close(); + } } var timeoutCounter = 0; @@ -294,4 +296,4 @@ class PackageBridge : V8Package { } -} \ No newline at end of file +} 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 a6068a8c..287a1486 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 @@ -651,14 +651,17 @@ class PackageHttp: V8Package { @V8Function fun connect(socketObj: V8ValueObject) { - val hasOpen = socketObj.has("open"); - val hasMessage = socketObj.has("message"); - val hasClosing = socketObj.has("closing"); - val hasClosed = socketObj.has("closed"); - val hasFailure = socketObj.has("failure"); + val (hasOpen, hasMessage, hasClosing, hasClosed, hasFailure) = _package._plugin.busy { + val open = socketObj.has("open"); + val message = socketObj.has("message"); + val closing = socketObj.has("closing"); + val closed = socketObj.has("closed"); + val failure = socketObj.has("failure"); - socketObj.setWeak(); //We have to manage this lifecycle - _listeners = socketObj; + socketObj.setWeak(); //We have to manage this lifecycle + _listeners = socketObj; + Quintuple(open, message, closing, closed, failure); + }; _socket = _packageClient.logExceptions { val client = _client; @@ -666,51 +669,50 @@ class PackageHttp: V8Package { override fun open() { Logger.i(TAG, "Websocket opened: " + _url); _isOpen = true; - if(hasOpen && _listeners?.isClosed != true) { - try { - _package._plugin.busy { + try { + _package._plugin.busy { + if(hasOpen && _listeners?.isClosed != true) { _listeners?.invokeV8Void("open", arrayOf()); } } - catch(ex: Throwable){ - Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex); - } + } + catch(ex: Throwable){ + Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex); } } override fun message(msg: String) { - if(hasMessage && _listeners?.isClosed != true) { - try { - _package._plugin.busy { + try { + _package._plugin.busy { + if(hasMessage && _listeners?.isClosed != true) { _listeners?.invokeV8Void("message", msg); } } - catch(ex: Throwable) {} } + catch(ex: Throwable) {} } override fun closing(code: Int, reason: String) { - if(hasClosing && _listeners?.isClosed != true) - { - try { - _package._plugin.busy { + try { + _package._plugin.busy { + if(hasClosing && _listeners?.isClosed != true) { _listeners?.invokeV8Void("closing", code, reason); } } - catch(ex: Throwable){ - Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex); - } + } + catch(ex: Throwable){ + Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex); } } override fun closed(code: Int, reason: String) { _isOpen = false; - if(hasClosed && _listeners?.isClosed != true) { - try { - _package._plugin.busy { + try { + _package._plugin.busy { + if(hasClosed && _listeners?.isClosed != true) { _listeners?.invokeV8Void("closed", code, reason); } } - catch(ex: Throwable){ - Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); - } + } + catch(ex: Throwable){ + Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); } Logger.w(TAG, "PackageHttp Socket removed"); synchronized(_package.aliveSockets) { @@ -720,15 +722,15 @@ class PackageHttp: V8Package { override fun failure(exception: Throwable) { _isOpen = false; Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception); - if(hasFailure && _listeners?.isClosed != true) { - try { - _package._plugin.busy { + try { + _package._plugin.busy { + if(hasFailure && _listeners?.isClosed != true) { _listeners?.invokeV8Void("failure", exception.message); } } - catch(ex: Throwable){ - Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); - } + } + catch(ex: Throwable){ + Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); } } }); @@ -747,10 +749,20 @@ class PackageHttp: V8Package { @V8Function fun close(code: Int?, reason: String?) { _socket?.close(code ?: 1000, reason ?: ""); - _listeners?.close() + _package._plugin.busy { + _listeners?.close() + } } } + private data class Quintuple( + val first: A, + val second: B, + val third: C, + val fourth: D, + val fifth: E + ) + data class RequestDescriptor( val method: String, val url: String, @@ -780,4 +792,4 @@ class PackageHttp: V8Package { private const val TAG = "PackageHttp"; private val WHITELISTED_RESPONSE_HEADERS = listOf("content-type", "date", "content-length", "last-modified", "etag", "cache-control", "content-encoding", "content-disposition", "connection") } -} \ No newline at end of file +} 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 eb6ddc2c..0e52a9b9 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt @@ -28,6 +28,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.platforms.js.DevJSClient import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig +import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor import com.futo.platformplayer.api.media.platforms.local.LocalClient import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.IPager @@ -181,11 +182,14 @@ class StatePlatform { } withContext(Dispatchers.IO) { - var toDisables = mutableListOf(); + val toDispose = mutableListOf(); var enabled: Array; synchronized(_clientsLock) { - for(e in _enabledClients) { - toDisables.add(e); + val previousAvailable = _availableClients.toList(); + val reusableByDescriptor = HashMap(); + for (prev in previousAvailable) { + if (prev is JSClient) + reusableByDescriptor[prev.descriptor] = prev; } _enabledClients.clear(); @@ -200,9 +204,16 @@ class StatePlatform { for (plugin in StatePlugins.instance.getPlugins()) { try { - val client = JSClient(context, plugin); - client.onCaptchaException.subscribe { c, ex -> - StateApp.instance.handleCaptchaException(c, ex); + val reused = reusableByDescriptor[plugin]; + val isReused = reused != null && reused.descriptor === plugin; + val client: JSClient = if (isReused) { + reused!!; + } else { + JSClient(context, plugin).also { fresh -> + fresh.onCaptchaException.subscribe { c, ex -> + StateApp.instance.handleCaptchaException(c, ex); + } + } } _icons[plugin.config.id] = StatePlugins.instance.getPluginIconOrNull(plugin.config.id) ?: @@ -210,6 +221,9 @@ class StatePlatform { _iconsByName[plugin.config.name.lowercase()] = StatePlugins.instance.getPluginIconOrNull(plugin.config.id) ?: ImageVariable(plugin.config.absoluteIconUrl, null); _availableClients.add(client); + + if (isReused) + reusableByDescriptor.remove(plugin); } catch(ex: Throwable) { Logger.e(TAG, "Failed to initialize plugin [${plugin.config.name}]", ex); @@ -219,6 +233,8 @@ class StatePlatform { } } + toDispose.addAll(reusableByDescriptor.values); + if(_availableClients.distinctBy { it.id }.count() < _availableClients.size) { val dups = _availableClients.filter { x-> _availableClients.count { it.id == x.id } > 1 }; val overrideClients = _availableClients.distinctBy { it.id } @@ -244,7 +260,7 @@ class StatePlatform { } selectClients(*enabled); - for(toDisable in toDisables) { + for(toDisable in toDispose) { launch(Dispatchers.IO) { try { toDisable.disable(); @@ -1144,4 +1160,4 @@ class StatePlatform { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt index 60026ea6..093f80e3 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt @@ -44,6 +44,7 @@ import kotlin.streams.asSequence * Used to maintain subscriptions */ class StateSubscriptions { + private val _subscriptions = FragmentedStorage.storeJson("subscriptions") .withUnique { it.channel.url } .withRestore(object: ReconstructStore(){ @@ -489,4 +490,4 @@ class StateSubscriptions { } } } -} \ No newline at end of file +} 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 2740ca8b..f101d7db 100644 --- a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt +++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt @@ -40,6 +40,8 @@ import java.time.OffsetDateTime import java.util.concurrent.ExecutionException import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinTask +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException import kotlin.system.measureTimeMillis abstract class SubscriptionsTaskFetchAlgorithm( @@ -125,7 +127,7 @@ abstract class SubscriptionsTaskFetchAlgorithm( val timeTotal = measureTimeMillis { for(task in forkTasks) { try { - val result = task.get(); + val result = task.get(TASK_TIMEOUT_S, TimeUnit.SECONDS); if(result != null) { if(result.pager != null) { taskResults.add(result); @@ -148,6 +150,10 @@ abstract class SubscriptionsTaskFetchAlgorithm( } else { throw ex.cause ?: ex; } + } catch (ex: TimeoutException) { + Logger.w(TAG, "Subscription task timed out after ${TASK_TIMEOUT_S}s, abandoning"); + task.cancel(true); + exs.add(ex); }; } @@ -382,4 +388,8 @@ abstract class SubscriptionsTaskFetchAlgorithm( val pager: IPager?, val exception: Throwable? ) + + companion object { + private const val TASK_TIMEOUT_S = 90L; + } } \ No newline at end of file