From 861f34a287d5f89d1d001b30ed031d42e38bd9b0 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Fri, 4 Jul 2025 15:02:01 +0200 Subject: [PATCH 01/19] Refs --- app/src/stable/assets/sources/odysee | 2 +- app/src/stable/assets/sources/patreon | 2 +- app/src/stable/assets/sources/spotify | 2 +- app/src/stable/assets/sources/twitch | 2 +- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/odysee | 2 +- app/src/unstable/assets/sources/patreon | 2 +- app/src/unstable/assets/sources/spotify | 2 +- app/src/unstable/assets/sources/twitch | 2 +- app/src/unstable/assets/sources/youtube | 2 +- dep/polycentricandroid | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/stable/assets/sources/odysee b/app/src/stable/assets/sources/odysee index 6ea9fa7e..d37268ab 160000 --- a/app/src/stable/assets/sources/odysee +++ b/app/src/stable/assets/sources/odysee @@ -1 +1 @@ -Subproject commit 6ea9fa7e4c20ba8c89975ac835ccebdbd1184fc4 +Subproject commit d37268abe2b32a4677c583ee252d9e439c56a38e diff --git a/app/src/stable/assets/sources/patreon b/app/src/stable/assets/sources/patreon index b811f8bd..6880b30b 160000 --- a/app/src/stable/assets/sources/patreon +++ b/app/src/stable/assets/sources/patreon @@ -1 +1 @@ -Subproject commit b811f8bdfbbff73cf0d7581c9d7596911cb132b6 +Subproject commit 6880b30b71800f6d22ddcb692f3c1c09e745315b diff --git a/app/src/stable/assets/sources/spotify b/app/src/stable/assets/sources/spotify index 214ac1df..8c0f03f5 160000 --- a/app/src/stable/assets/sources/spotify +++ b/app/src/stable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit 214ac1dfcc985f533d9db7d128a8315bc55fa854 +Subproject commit 8c0f03f5fbc9b4e499437b85c757ec40cb7c0126 diff --git a/app/src/stable/assets/sources/twitch b/app/src/stable/assets/sources/twitch index 08346f91..f526ce1b 160000 --- a/app/src/stable/assets/sources/twitch +++ b/app/src/stable/assets/sources/twitch @@ -1 +1 @@ -Subproject commit 08346f917753694e14bc1caa784aa87066a2ab84 +Subproject commit f526ce1b7690c0e077279edca601c3e1dab14f15 diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 48d98c1f..ab38d126 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 48d98c1f0cd80e9e569280423ae404e56047c883 +Subproject commit ab38d1262ab4178113b1f6ad445e1b4d580fdfc1 diff --git a/app/src/unstable/assets/sources/odysee b/app/src/unstable/assets/sources/odysee index 6ea9fa7e..d37268ab 160000 --- a/app/src/unstable/assets/sources/odysee +++ b/app/src/unstable/assets/sources/odysee @@ -1 +1 @@ -Subproject commit 6ea9fa7e4c20ba8c89975ac835ccebdbd1184fc4 +Subproject commit d37268abe2b32a4677c583ee252d9e439c56a38e diff --git a/app/src/unstable/assets/sources/patreon b/app/src/unstable/assets/sources/patreon index b811f8bd..6880b30b 160000 --- a/app/src/unstable/assets/sources/patreon +++ b/app/src/unstable/assets/sources/patreon @@ -1 +1 @@ -Subproject commit b811f8bdfbbff73cf0d7581c9d7596911cb132b6 +Subproject commit 6880b30b71800f6d22ddcb692f3c1c09e745315b diff --git a/app/src/unstable/assets/sources/spotify b/app/src/unstable/assets/sources/spotify index 214ac1df..8c0f03f5 160000 --- a/app/src/unstable/assets/sources/spotify +++ b/app/src/unstable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit 214ac1dfcc985f533d9db7d128a8315bc55fa854 +Subproject commit 8c0f03f5fbc9b4e499437b85c757ec40cb7c0126 diff --git a/app/src/unstable/assets/sources/twitch b/app/src/unstable/assets/sources/twitch index 08346f91..f526ce1b 160000 --- a/app/src/unstable/assets/sources/twitch +++ b/app/src/unstable/assets/sources/twitch @@ -1 +1 @@ -Subproject commit 08346f917753694e14bc1caa784aa87066a2ab84 +Subproject commit f526ce1b7690c0e077279edca601c3e1dab14f15 diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 48d98c1f..ab38d126 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 48d98c1f0cd80e9e569280423ae404e56047c883 +Subproject commit ab38d1262ab4178113b1f6ad445e1b4d580fdfc1 diff --git a/dep/polycentricandroid b/dep/polycentricandroid index f87f00ab..278e3c2f 160000 --- a/dep/polycentricandroid +++ b/dep/polycentricandroid @@ -1 +1 @@ -Subproject commit f87f00ab9e1262e300246b8963591bdf3a8fada7 +Subproject commit 278e3c2febf853a71f0719f9b0ea98339a0214ac From 2bbe0e6133940e79311c9a6821a5e451a2dea367 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Fri, 4 Jul 2025 15:24:19 +0200 Subject: [PATCH 02/19] Substitute v8 object calls to wrapper function --- .../com/futo/platformplayer/Extensions_V8.kt | 19 +++++++++++++++++-- .../api/media/platforms/js/models/JSPager.kt | 3 ++- .../platforms/js/models/JSPlaybackTracker.kt | 7 ++++--- .../platforms/js/models/JSRequestExecutor.kt | 10 ++++++---- .../platforms/js/models/JSRequestModifier.kt | 4 +++- .../sources/JSAudioUrlWidevineSource.kt | 4 +++- .../sources/JSDashManifestWidevineSource.kt | 4 +++- .../platforms/js/models/sources/JSSource.kt | 5 +++-- .../sources/JSVideoUrlWidevineSource.kt | 3 ++- .../engine/packages/PackageHttp.kt | 11 ++++++----- 10 files changed, 49 insertions(+), 21 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 d8427dc6..000b4004 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt @@ -294,17 +294,32 @@ class V8Deferred(val deferred: Deferred, val estDuration: Int = -1): Defer } -fun V8ValueObject.invokeV8(method: String, vararg obj: Any): T { +fun V8ValueObject.invokeV8(method: String, vararg obj: Any?): T { var result = this.invoke(method, *obj); if(result is V8ValuePromise) { return result.toV8ValueBlocking(this.getSourcePlugin()!!); } return result as T; } -fun V8ValueObject.invokeV8Async(method: String, vararg obj: Any): V8Deferred { +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)); +} +fun V8ValueObject.invokeV8Void(method: String, vararg obj: Any?): V8Value { + var result = this.invoke(method, *obj); + if(result is V8ValuePromise) { + return result.toV8ValueBlocking(this.getSourcePlugin()!!); + } + return result; +} +fun V8ValueObject.invokeV8VoidAsync(method: String, vararg obj: Any?): V8Deferred { + var result = this.invoke(method, *obj); + if(result is V8ValuePromise) { + val result = result.toV8ValueAsync(this.getSourcePlugin()!!); + return result; + } + return V8Deferred(CompletableDeferred(result)); } \ No newline at end of file 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 e81a288d..f363a011 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 @@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow +import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.warnIfMainThread abstract class JSPager : IPager { @@ -49,7 +50,7 @@ abstract class JSPager : IPager { val pluginV8 = plugin.getUnderlyingPlugin(); pluginV8.busy { pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") { - pager.invoke("nextPage", arrayOf()); + pager.invokeV8("nextPage", arrayOf()); }; _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; _resultChanged = true; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaybackTracker.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaybackTracker.kt index 15a7d854..bd0e4400 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaybackTracker.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPlaybackTracker.kt @@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.getOrThrow +import com.futo.platformplayer.invokeV8Void import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.warnIfMainThread @@ -57,7 +58,7 @@ class JSPlaybackTracker: IPlaybackTracker { _client.busy { if (_hasInit) { Logger.i("JSPlaybackTracker", "onInit (${seconds})"); - _obj.invokeVoid("onInit", seconds); + _obj.invokeV8Void("onInit", seconds); } nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false)); _hasCalledInit = true; @@ -73,7 +74,7 @@ class JSPlaybackTracker: IPlaybackTracker { else { _client.busy { Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})"); - _obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying); + _obj.invokeV8Void("onProgress", Math.floor(seconds), isPlaying); nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false)); _lastRequest = System.currentTimeMillis(); } @@ -86,7 +87,7 @@ class JSPlaybackTracker: IPlaybackTracker { synchronized(_obj) { Logger.i("JSPlaybackTracker", "onConcluded"); _client.busy { - _obj.invokeVoid("onConcluded", -1); + _obj.invokeV8Void("onConcluded", -1); } } } 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 36cfc7db..ed428790 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 @@ -14,6 +14,8 @@ import com.futo.platformplayer.engine.exceptions.ScriptException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow +import com.futo.platformplayer.invokeV8 +import com.futo.platformplayer.invokeV8Void import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateDeveloper import kotlinx.serialization.Serializable @@ -55,7 +57,7 @@ class JSRequestExecutor { "[${_config.name}] JSRequestExecutor", "builder.modifyRequest()" ) { - _executor.invoke("executeRequest", url, headers, method, body); + _executor.invokeV8("executeRequest", url, headers, method, body); } as V8Value; } else V8Plugin.catchScriptErrors( @@ -63,7 +65,7 @@ class JSRequestExecutor { "[${_config.name}] JSRequestExecutor", "builder.modifyRequest()" ) { - _executor.invoke("executeRequest", url, headers, method, body); + _executor.invokeV8("executeRequest", url, headers, method, body); } as V8Value; try { @@ -110,7 +112,7 @@ class JSRequestExecutor { "[${_config.name}] JSRequestExecutor", "builder.modifyRequest()" ) { - _executor.invokeVoid("cleanup", null); + _executor.invokeV8("cleanup", null); }; } else V8Plugin.catchScriptErrors( @@ -118,7 +120,7 @@ class JSRequestExecutor { "[${_config.name}] JSRequestExecutor", "builder.modifyRequest()" ) { - _executor.invokeVoid("cleanup", null); + _executor.invokeV8("cleanup", null); }; } } 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 f7d169af..af03d070 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 @@ -11,6 +11,8 @@ import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrThrow +import com.futo.platformplayer.invokeV8 +import com.futo.platformplayer.invokeV8Void class JSRequestModifier: IRequestModifier { private val _plugin: JSClient; @@ -40,7 +42,7 @@ class JSRequestModifier: IRequestModifier { return _plugin.busy { val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") { - _modifier.invoke("modifyRequest", url, headers); + _modifier.invokeV8("modifyRequest", url, headers); } as V8ValueObject; val req = JSRequest(_plugin, result, url, headers); 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 516e07ff..7df120d5 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 @@ -6,6 +6,8 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.getOrThrow +import com.futo.platformplayer.invokeV8 +import com.futo.platformplayer.invokeV8Void class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource { override val licenseUri: String @@ -25,7 +27,7 @@ class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource { return null val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") { - _obj.invoke("getLicenseRequestExecutor", arrayOf()) + _obj.invokeV8("getLicenseRequestExecutor", arrayOf()) } if (result !is V8ValueObject) 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 be72d3a0..7700bd82 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 @@ -9,6 +9,8 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrThrow +import com.futo.platformplayer.invokeV8 +import com.futo.platformplayer.invokeV8Void class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource, IDashManifestWidevineSource, JSSource { @@ -45,7 +47,7 @@ class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource, return null val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") { - _obj.invoke("getLicenseRequestExecutor", arrayOf()) + _obj.invokeV8("getLicenseRequestExecutor", arrayOf()) } if (result !is V8ValueObject) 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 22bf2a60..4fe4307f 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 @@ -16,6 +16,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrDefault +import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.orNull import com.futo.platformplayer.views.video.datasources.JSHttpDataSource @@ -64,7 +65,7 @@ abstract class JSSource { return@isBusyWith null; val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") { - _obj.invoke("getRequestModifier", arrayOf()); + _obj.invokeV8("getRequestModifier", arrayOf()); }; if (result !is V8ValueObject) @@ -78,7 +79,7 @@ abstract class JSSource { Logger.v("JSSource", "Request executor for [${type}] requesting"); val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") { - _obj.invoke("getRequestExecutor", arrayOf()); + _obj.invokeV8("getRequestExecutor", arrayOf()); }; Logger.v("JSSource", "Request executor for [${type}] received"); 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 bcd6607d..aff22c33 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 @@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.getOrThrow +import com.futo.platformplayer.invokeV8 class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource { override val licenseUri: String @@ -25,7 +26,7 @@ class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource { return null val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") { - _obj.invoke("getLicenseRequestExecutor", arrayOf()) + _obj.invokeV8("getLicenseRequestExecutor", arrayOf()) } if (result !is V8ValueObject) 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 82edb023..d7aa89b7 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 @@ -17,6 +17,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.internal.IV8Convertable import com.futo.platformplayer.engine.internal.V8BindObject +import com.futo.platformplayer.invokeV8Void import com.futo.platformplayer.logging.Logger import java.net.SocketTimeoutException import java.util.concurrent.ForkJoinPool @@ -668,7 +669,7 @@ class PackageHttp: V8Package { if(hasOpen && _listeners?.isClosed != true) { try { _package._plugin.busy { - _listeners?.invokeVoid("open", arrayOf()); + _listeners?.invokeV8Void("open", arrayOf()); } } catch(ex: Throwable){ @@ -680,7 +681,7 @@ class PackageHttp: V8Package { if(hasMessage && _listeners?.isClosed != true) { try { _package._plugin.busy { - _listeners?.invokeVoid("message", msg); + _listeners?.invokeV8Void("message", msg); } } catch(ex: Throwable) {} @@ -691,7 +692,7 @@ class PackageHttp: V8Package { { try { _package._plugin.busy { - _listeners?.invokeVoid("closing", code, reason); + _listeners?.invokeV8Void("closing", code, reason); } } catch(ex: Throwable){ @@ -704,7 +705,7 @@ class PackageHttp: V8Package { if(hasClosed && _listeners?.isClosed != true) { try { _package._plugin.busy { - _listeners?.invokeVoid("closed", code, reason); + _listeners?.invokeV8Void("closed", code, reason); } } catch(ex: Throwable){ @@ -722,7 +723,7 @@ class PackageHttp: V8Package { if(hasFailure && _listeners?.isClosed != true) { try { _package._plugin.busy { - _listeners?.invokeVoid("failure", exception.message); + _listeners?.invokeV8Void("failure", exception.message); } } catch(ex: Throwable){ From 940bf163da3c3e62ac58ad8be39e82a202ab7888 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Fri, 4 Jul 2025 15:58:46 +0200 Subject: [PATCH 03/19] Progress bar color, refs --- app/src/main/java/com/futo/platformplayer/Extensions_V8.kt | 3 +++ .../com/futo/platformplayer/views/video/FutoVideoPlayer.kt | 2 +- app/src/main/res/layout/video_view.xml | 1 + app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 5 files changed, 7 insertions(+), 3 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 000b4004..fc1f5cf3 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt @@ -238,6 +238,9 @@ fun V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred else V8Deferred(underlyingDef); + if(def.estDuration > 0) + Logger.i("V8", "Promise with duration: [${def.estDuration}]"); + val promise = this; plugin.busy { this.register(object: IV8ValuePromise.IListener { diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt index 193b564e..a497ab72 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt @@ -911,6 +911,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase { loaderDeterminate.progress = anim.animatedValue as Int } start() - } + }; } } \ No newline at end of file diff --git a/app/src/main/res/layout/video_view.xml b/app/src/main/res/layout/video_view.xml index 63eabf9f..bc9a8142 100644 --- a/app/src/main/res/layout/video_view.xml +++ b/app/src/main/res/layout/video_view.xml @@ -87,6 +87,7 @@ Date: Fri, 4 Jul 2025 16:13:11 +0200 Subject: [PATCH 04/19] Add missing file --- app/src/main/res/drawable/progress_bar.xml | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 app/src/main/res/drawable/progress_bar.xml diff --git a/app/src/main/res/drawable/progress_bar.xml b/app/src/main/res/drawable/progress_bar.xml new file mode 100644 index 00000000..c6641d2c --- /dev/null +++ b/app/src/main/res/drawable/progress_bar.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 00e28b9ce0094ea3682f15394266afda5648e726 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Fri, 4 Jul 2025 17:27:35 +0200 Subject: [PATCH 05/19] Better raid messaging, loader autochange to indeterminate, refs --- .../platformplayer/views/overlays/LiveChatOverlay.kt | 11 ++++++++++- .../platformplayer/views/video/FutoVideoPlayer.kt | 4 ++++ app/src/main/res/drawable/progress_bar.xml | 8 ++++---- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt index a3a87946..a2ed19ef 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt @@ -64,6 +64,7 @@ class LiveChatOverlay : LinearLayout { private val _overlayRaid: ConstraintLayout; private val _overlayRaid_Name: TextView; + private val _overlayRaid_Message: TextView; private val _overlayRaid_Thumbnail: ImageView; private val _overlayRaid_ButtonGo: Button; @@ -146,6 +147,7 @@ class LiveChatOverlay : LinearLayout { _overlayRaid = findViewById(R.id.overlay_raid); _overlayRaid_Name = findViewById(R.id.raid_name); + _overlayRaid_Message = findViewById(R.id.textRaidMessage); _overlayRaid_Thumbnail = findViewById(R.id.raid_thumbnail); _overlayRaid_ButtonGo = findViewById(R.id.raid_button_go); _overlayRaid_ButtonDismiss = findViewById(R.id.raid_button_prevent); @@ -371,7 +373,14 @@ class LiveChatOverlay : LinearLayout { else _overlayRaid.visibility = View.GONE; - _overlayRaid_ButtonGo.visibility = if (raid?.isOutgoing == true) View.VISIBLE else View.GONE + if(raid?.isOutgoing ?: false) { + _overlayRaid_ButtonGo.visibility = View.VISIBLE + _overlayRaid_Message.text = "Viewers are raiding"; + } + else { + _overlayRaid_ButtonGo.visibility = View.GONE; + _overlayRaid_Message.text = "Raid incoming from"; + } } } fun setViewCount(viewCount: Int) { diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt index a497ab72..350ef6b0 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt @@ -909,6 +909,10 @@ class FutoVideoPlayer : FutoVideoPlayerBase { duration = expectedDurationMs.toLong() addUpdateListener { anim -> loaderDeterminate.progress = anim.animatedValue as Int + if(loaderDeterminate.progress > loaderDeterminate.max - 10) { + setLoading(true); + } + } start() }; diff --git a/app/src/main/res/drawable/progress_bar.xml b/app/src/main/res/drawable/progress_bar.xml index c6641d2c..d459af14 100644 --- a/app/src/main/res/drawable/progress_bar.xml +++ b/app/src/main/res/drawable/progress_bar.xml @@ -7,8 +7,8 @@ android:angle="270" android:centerColor="#ff5a5d5a" android:centerY="0.75" - android:endColor="#ff747674" - android:startColor="#ff9d9e9d" /> + android:endColor="#ff5a5d5a" + android:startColor="#ff5a5d5a" /> @@ -20,7 +20,7 @@ android:angle="270" android:centerColor="#80ffb600" android:centerY="0.75" - android:endColor="#a0ffcb00" + android:endColor="#80ffd300" android:startColor="#80ffd300" /> @@ -31,7 +31,7 @@ diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 2036f9b6..f793e625 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 2036f9b68255d2f9ef4e1232b4ad556e11cafb9a +Subproject commit f793e625641195f5fe60d94a829e7f44a93325a6 diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 2036f9b6..f793e625 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 2036f9b68255d2f9ef4e1232b4ad556e11cafb9a +Subproject commit f793e625641195f5fe60d94a829e7f44a93325a6 From c6caa59a907cc45186344d45257e42ddf0e2022b Mon Sep 17 00:00:00 2001 From: Kelvin Date: Fri, 4 Jul 2025 18:00:48 +0200 Subject: [PATCH 06/19] Refs --- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index f793e625..98255d17 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit f793e625641195f5fe60d94a829e7f44a93325a6 +Subproject commit 98255d1773d288bfa567f122993c7b2845f37ea5 diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index f793e625..98255d17 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit f793e625641195f5fe60d94a829e7f44a93325a6 +Subproject commit 98255d1773d288bfa567f122993c7b2845f37ea5 From cc247ce63447b5b6b17a7b85b5cf793f84b7ddab Mon Sep 17 00:00:00 2001 From: Koen J Date: Sat, 5 Jul 2025 12:58:10 +0200 Subject: [PATCH 07/19] Attempt at a loader game. --- .../platformplayer/activities/MainActivity.kt | 4 +- .../platformplayer/activities/TestActivity.kt | 12 + .../views/TargetTapLoaderView.kt | 273 ++++++++++++++++++ .../views/video/FutoVideoPlayer.kt | 47 +-- app/src/main/res/layout/activity_test.xml | 7 +- app/src/main/res/layout/video_view.xml | 30 +- 6 files changed, 302 insertions(+), 71 deletions(-) create mode 100644 app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt 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 829b7857..073033da 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -32,7 +32,6 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentContainerView import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.whenStateAtLeast import androidx.lifecycle.withStateAtLeast import androidx.media3.common.util.UnstableApi import com.futo.platformplayer.BuildConfig @@ -114,7 +113,6 @@ import java.io.PrintWriter import java.io.StringWriter import java.lang.reflect.InvocationTargetException import java.util.LinkedList -import java.util.Queue import java.util.UUID import java.util.concurrent.ConcurrentLinkedQueue @@ -610,6 +608,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { }, UIDialogs.ActionStyle.PRIMARY) ) } + + //startActivity(Intent(this, TestActivity::class.java)) } /* diff --git a/app/src/main/java/com/futo/platformplayer/activities/TestActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/TestActivity.kt index 608bda0a..5f9e0a10 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/TestActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/TestActivity.kt @@ -2,12 +2,24 @@ package com.futo.platformplayer.activities import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.R +import com.futo.platformplayer.views.TargetTapLoaderView +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch class TestActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_test); + + val view = findViewById(R.id.test_view) + view.startLoader(10000) + + lifecycleScope.launch { + delay(5000) + view.startLoader() + } } companion object { diff --git a/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt b/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt new file mode 100644 index 00000000..222d6cbc --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt @@ -0,0 +1,273 @@ +package com.futo.platformplayer.views + +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import android.view.HapticFeedbackConstants +import android.view.MotionEvent +import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator +import androidx.core.graphics.toColorInt +import kotlin.math.* +import kotlin.random.Random + +class TargetTapLoaderView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : View(context, attrs) { + + private var expectedDurationMs: Long? = null + private var startTime: Long = 0L + private var loaderFinished = false + private var forceIndeterminate = false + + private val isIndeterminate: Boolean + get() = forceIndeterminate || expectedDurationMs == null || expectedDurationMs == 0L + + private val targets = mutableListOf() + private val particles = mutableListOf() + private var score = 0 + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + textSize = 48f + textAlign = Paint.Align.CENTER + setShadowLayer(4f, 0f, 0f, Color.BLACK) + typeface = Typeface.DEFAULT_BOLD + } + private val progressBarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = "#2D63ED".toColorInt() + } + private val spinnerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = "#2D63ED".toColorInt() + strokeWidth = 12f + style = Paint.Style.STROKE + strokeCap = Paint.Cap.ROUND + } + private val outerRingPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + style = Paint.Style.FILL + } + private val middleRingPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.RED + style = Paint.Style.FILL + } + private val centerDotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.YELLOW + style = Paint.Style.FILL + } + private val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.argb(50, 0, 0, 0) + } + private val backgroundPaint = Paint() + + private var spinnerAngle = 0f + + private val frameRunnable = object : Runnable { + override fun run() { + invalidate() + if (!loaderFinished) postDelayed(this, 16L) + } + } + + init { + setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + handleTap(event.x, event.y) + } + true + } + } + + fun startLoader(durationMs: Long? = null) { + expectedDurationMs = durationMs?.takeIf { it > 0L } + forceIndeterminate = expectedDurationMs == null + loaderFinished = false + startTime = System.currentTimeMillis() + score = 0 + targets.clear() + particles.clear() + removeCallbacks(frameRunnable) + post(frameRunnable) + post { spawnTarget() } + + if (!isIndeterminate) { + postDelayed({ + if (!loaderFinished) { + forceIndeterminate = true + startTime = System.currentTimeMillis() + spawnTarget() + } + }, expectedDurationMs!!) + } + } + + fun finishLoader() { + loaderFinished = true + invalidate() + } + + fun stopAndResetLoader() { + loaderFinished = true + targets.clear() + particles.clear() + removeCallbacks(frameRunnable) + invalidate() + } + + private fun handleTap(x: Float, y: Float) { + val now = System.currentTimeMillis() + val hitIndex = targets.indexOfFirst { t -> !t.hit && hypot(x - t.x, y - t.y) <= t.radius } + if (hitIndex >= 0) { + performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) + val target = targets[hitIndex] + target.hit = true + target.hitTime = now + score += if (!isIndeterminate) 10 else 5 + spawnParticles(target.x, target.y, target.radius) + } + } + + private fun spawnTarget() { + if (loaderFinished) return + val radius = Random.nextInt(40, 80).toFloat() + val x = Random.nextFloat() * (width - 2 * radius) + radius + val y = Random.nextFloat() * (height - 2 * radius - 60f) + radius + targets.add(Target(x, y, radius, System.currentTimeMillis())) + + val delay = if (isIndeterminate) 1400L else 700L + postDelayed({ spawnTarget() }, delay) + } + + private fun spawnParticles(cx: Float, cy: Float, radius: Float) { + repeat(12) { + val angle = Random.nextFloat() * 2f * PI.toFloat() + val speed = Random.nextFloat() * 5f + 2f + val vx = cos(angle) * speed + val vy = sin(angle) * speed + particles.add(Particle(cx, cy, vx, vy, 255, System.currentTimeMillis())) + } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + drawBackground(canvas) + + val now = System.currentTimeMillis() + drawTargets(canvas, now) + drawParticles(canvas, now) + + if (!loaderFinished) { + if (isIndeterminate) drawIndeterminateSpinner(canvas) + else drawDeterministicProgressBar(canvas, now) + } + + canvas.drawText("Score: $score", width / 2f, height - 80f, textPaint) + + if (loaderFinished) { + canvas.drawText("Loading Complete!", width / 2f, height / 2f, textPaint) + } + } + + private fun drawBackground(canvas: Canvas) { + val gradient = LinearGradient( + 0f, 0f, 0f, height.toFloat(), + Color.rgb(20, 20, 40), Color.BLACK, + Shader.TileMode.CLAMP + ) + backgroundPaint.shader = gradient + canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), backgroundPaint) + } + + private fun drawTargets(canvas: Canvas, now: Long) { + val expireMs = if (isIndeterminate) 2500L else 1500L + targets.removeAll { it.hit && now - it.hitTime > 300L } + targets.removeAll { !it.hit && now - it.spawnTime > expireMs } + + for (t in targets) { + val scale = if (t.hit) 1f - ((now - t.hitTime) / 300f).coerceIn(0f, 1f) else 1f + val alpha = if (t.hit) ((1f - scale) * 255).toInt().coerceAtMost(255) else 255 + val safeRadius = (t.radius * scale).coerceAtLeast(1f) + val glowPaint = Paint().apply { + shader = RadialGradient(t.x, t.y, safeRadius, Color.YELLOW, Color.TRANSPARENT, Shader.TileMode.CLAMP) + } + canvas.drawCircle(t.x, t.y, safeRadius * 1.2f, glowPaint) + canvas.drawCircle(t.x + 4f, t.y + 4f, safeRadius, shadowPaint) + outerRingPaint.alpha = alpha + middleRingPaint.alpha = alpha + centerDotPaint.alpha = alpha + canvas.drawCircle(t.x, t.y, safeRadius, outerRingPaint) + canvas.drawCircle(t.x, t.y, safeRadius * 0.66f, middleRingPaint) + canvas.drawCircle(t.x, t.y, safeRadius * 0.33f, centerDotPaint) + } + } + + private fun drawParticles(canvas: Canvas, now: Long) { + val lifespan = 400L + val iterator = particles.iterator() + while (iterator.hasNext()) { + val p = iterator.next() + val age = now - p.startTime + if (age > lifespan) { + iterator.remove() + continue + } + val alpha = ((1f - (age / lifespan.toFloat())) * 255).toInt() + val paint = Paint().apply { + color = Color.YELLOW + this.alpha = alpha + } + p.x += p.vx + p.y += p.vy + canvas.drawCircle(p.x, p.y, 6f, paint) + } + } + + private fun drawDeterministicProgressBar(canvas: Canvas, now: Long) { + val duration = expectedDurationMs ?: return + val rawProgress = ((now - startTime).toFloat() / duration).coerceIn(0f, 1f) + val easedProgress = AccelerateDecelerateInterpolator().getInterpolation(rawProgress) + + val barHeight = 20f + val barRadius = 10f + val barWidth = width * easedProgress + + val rect = RectF(0f, height - barHeight, barWidth, height.toFloat()) + canvas.drawRoundRect(rect, barRadius, barRadius, progressBarPaint) + } + + private fun drawIndeterminateSpinner(canvas: Canvas) { + spinnerAngle = (spinnerAngle + 6f) % 360f + val cx = width / 2f + val cy = height / 2f + val radius = min(width, height) / 6f + val sweepAngle = 270f + + val glowPaint = Paint(spinnerPaint).apply { + maskFilter = BlurMaskFilter(15f, BlurMaskFilter.Blur.SOLID) + } + + val shader = SweepGradient(cx, cy, intArrayOf(Color.TRANSPARENT, Color.WHITE, Color.TRANSPARENT), floatArrayOf(0f, 0.5f, 1f)) + spinnerPaint.shader = shader + + canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius, spinnerAngle, sweepAngle, false, glowPaint) + canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius, spinnerAngle, sweepAngle, false, spinnerPaint) + } + + private data class Target( + val x: Float, + val y: Float, + val radius: Float, + val spawnTime: Long, + var hit: Boolean = false, + var hitTime: Long = 0L + ) + + private data class Particle( + var x: Float, + var y: Float, + val vx: Float, + val vy: Float, + var alpha: Int, + val startTime: Long + ) +} diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt index 350ef6b0..995c3c55 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt @@ -44,6 +44,7 @@ import com.futo.platformplayer.formatDuration import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePlayer +import com.futo.platformplayer.views.TargetTapLoaderView import com.futo.platformplayer.views.behavior.GestureControlView import com.futo.platformplayer.views.others.ProgressBar import kotlinx.coroutines.CoroutineScope @@ -154,10 +155,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase { val onChapterClicked = Event1(); - private val loaderOverlay: FrameLayout - private val loaderIndeterminate: android.widget.ProgressBar - private val loaderDeterminate: android.widget.ProgressBar - private var determinateAnimator: ValueAnimator? = null + private val _loaderGame: TargetTapLoaderView @OptIn(UnstableApi::class) constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) { @@ -199,13 +197,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase { _control_duration_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_duration); _control_pause_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_pause); - loaderOverlay = findViewById(R.id.loader_overlay) - loaderIndeterminate = findViewById(R.id.loader_indeterminate) - loaderDeterminate = findViewById(R.id.loader_determinate) - - loaderOverlay.visibility = View.GONE - loaderIndeterminate.visibility = View.GONE - loaderDeterminate.visibility = View.GONE + _loaderGame = findViewById(R.id.loader_overlay) + _loaderGame.visibility = View.GONE _control_chapter.setOnClickListener { _currentChapter?.let { @@ -884,37 +877,17 @@ class FutoVideoPlayer : FutoVideoPlayerBase { } override fun setLoading(isLoading: Boolean) { - determinateAnimator?.cancel() if (isLoading) { - loaderOverlay.visibility = View.VISIBLE - loaderIndeterminate.visibility = View.VISIBLE - loaderDeterminate.visibility = View.GONE + _loaderGame.visibility = View.VISIBLE + _loaderGame.startLoader() } else { - loaderOverlay.visibility = View.GONE - loaderIndeterminate.visibility = View.GONE - loaderDeterminate.visibility = View.GONE + _loaderGame.visibility = View.GONE + _loaderGame.stopAndResetLoader() } } override fun setLoading(expectedDurationMs: Int) { - determinateAnimator?.cancel() - - loaderOverlay.visibility = View.VISIBLE - loaderIndeterminate.visibility = View.GONE - loaderDeterminate.visibility = View.VISIBLE - loaderDeterminate.max = expectedDurationMs - loaderDeterminate.progress = 0 - - determinateAnimator = ValueAnimator.ofInt(0, expectedDurationMs).apply { - duration = expectedDurationMs.toLong() - addUpdateListener { anim -> - loaderDeterminate.progress = anim.animatedValue as Int - if(loaderDeterminate.progress > loaderDeterminate.max - 10) { - setLoading(true); - } - - } - start() - }; + _loaderGame.visibility = View.VISIBLE + _loaderGame.startLoader(expectedDurationMs.toLong()) } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_test.xml b/app/src/main/res/layout/activity_test.xml index bc4ebda9..8c9bf301 100644 --- a/app/src/main/res/layout/activity_test.xml +++ b/app/src/main/res/layout/activity_test.xml @@ -5,9 +5,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:background="@color/black"> - + android:layout_height="240dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/video_view.xml b/app/src/main/res/layout/video_view.xml index bc9a8142..cad54d41 100644 --- a/app/src/main/res/layout/video_view.xml +++ b/app/src/main/res/layout/video_view.xml @@ -65,36 +65,10 @@ android:visibility="gone" /> - - - - - - + android:visibility="gone" /> \ No newline at end of file From 83f520ca445e216ff610dc71f0c826236cc17a39 Mon Sep 17 00:00:00 2001 From: Koen J Date: Sat, 5 Jul 2025 13:47:48 +0200 Subject: [PATCH 08/19] Further fixes to TargetTApLoaderView. --- .../views/TargetTapLoaderView.kt | 106 ++++++++++++++---- 1 file changed, 82 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt b/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt index 222d6cbc..c4c92f75 100644 --- a/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt @@ -3,6 +3,7 @@ package com.futo.platformplayer.views import android.content.Context import android.graphics.* import android.util.AttributeSet +import android.util.TypedValue import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.View @@ -19,6 +20,9 @@ class TargetTapLoaderView @JvmOverloads constructor( private var startTime: Long = 0L private var loaderFinished = false private var forceIndeterminate = false + private var spinnerShader: SweepGradient? = null + private var lastFrameTime = System.currentTimeMillis() + private val bounceInterpolator = android.view.animation.OvershootInterpolator(2f) private val isIndeterminate: Boolean get() = forceIndeterminate || expectedDurationMs == null || expectedDurationMs == 0L @@ -29,7 +33,9 @@ class TargetTapLoaderView @JvmOverloads constructor( private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.WHITE - textSize = 48f + textSize = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, 18f, resources.displayMetrics + ) textAlign = Paint.Align.CENTER setShadowLayer(4f, 0f, 0f, Color.BLACK) typeface = Typeface.DEFAULT_BOLD @@ -58,6 +64,10 @@ class TargetTapLoaderView @JvmOverloads constructor( private val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.argb(50, 0, 0, 0) } + private val glowPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val particlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.YELLOW + } private val backgroundPaint = Paint() private var spinnerAngle = 0f @@ -79,7 +89,17 @@ class TargetTapLoaderView @JvmOverloads constructor( } fun startLoader(durationMs: Long? = null) { - expectedDurationMs = durationMs?.takeIf { it > 0L } + val isAlreadyRunning = !loaderFinished + + val newDuration = durationMs?.takeIf { it > 0L } + + if (isAlreadyRunning && newDuration == null) { + forceIndeterminate = true + startTime = System.currentTimeMillis() + return + } + + expectedDurationMs = newDuration forceIndeterminate = expectedDurationMs == null loaderFinished = false startTime = System.currentTimeMillis() @@ -103,6 +123,7 @@ class TargetTapLoaderView @JvmOverloads constructor( fun finishLoader() { loaderFinished = true + particles.clear() invalidate() } @@ -120,15 +141,22 @@ class TargetTapLoaderView @JvmOverloads constructor( if (hitIndex >= 0) { performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) val target = targets[hitIndex] - target.hit = true - target.hitTime = now - score += if (!isIndeterminate) 10 else 5 - spawnParticles(target.x, target.y, target.radius) + if (!target.hit) { + target.hit = true + target.hitTime = now + score += if (!isIndeterminate) 10 else 5 + spawnParticles(target.x, target.y, target.radius) + } } } private fun spawnTarget() { if (loaderFinished) return + if (width <= 0 || height <= 0) { + post { spawnTarget() } + return + } + val radius = Random.nextInt(40, 80).toFloat() val x = Random.nextFloat() * (width - 2 * radius) + radius val y = Random.nextFloat() * (height - 2 * radius - 60f) + radius @@ -144,7 +172,7 @@ class TargetTapLoaderView @JvmOverloads constructor( val speed = Random.nextFloat() * 5f + 2f val vx = cos(angle) * speed val vy = sin(angle) * speed - particles.add(Particle(cx, cy, vx, vy, 255, System.currentTimeMillis())) + particles.add(Particle(cx, cy, vx, vy, System.currentTimeMillis())) } } @@ -153,11 +181,14 @@ class TargetTapLoaderView @JvmOverloads constructor( drawBackground(canvas) val now = System.currentTimeMillis() + val deltaMs = now - lastFrameTime + lastFrameTime = now + drawTargets(canvas, now) drawParticles(canvas, now) if (!loaderFinished) { - if (isIndeterminate) drawIndeterminateSpinner(canvas) + if (isIndeterminate) drawIndeterminateSpinner(canvas, deltaMs) else drawDeterministicProgressBar(canvas, now) } @@ -184,17 +215,37 @@ class TargetTapLoaderView @JvmOverloads constructor( targets.removeAll { !it.hit && now - it.spawnTime > expireMs } for (t in targets) { - val scale = if (t.hit) 1f - ((now - t.hitTime) / 300f).coerceIn(0f, 1f) else 1f + val scale = when { + t.hit -> { + 1f - ((now - t.hitTime) / 300f).coerceIn(0f, 1f) + } + else -> { + val spawnElapsed = now - t.spawnAnimationStartTime + if (spawnElapsed < 300L) { + val animProgress = spawnElapsed / 300f + bounceInterpolator.getInterpolation(animProgress) + } else { + val pulseTime = ((now - t.spawnAnimationStartTime) / 1000f) * 2f * PI.toFloat() + t.idlePulseOffset + 1f + 0.02f * sin(pulseTime) + } + } + } + val alpha = if (t.hit) ((1f - scale) * 255).toInt().coerceAtMost(255) else 255 val safeRadius = (t.radius * scale).coerceAtLeast(1f) - val glowPaint = Paint().apply { - shader = RadialGradient(t.x, t.y, safeRadius, Color.YELLOW, Color.TRANSPARENT, Shader.TileMode.CLAMP) - } + + glowPaint.shader = RadialGradient( + t.x, t.y, safeRadius, + Color.YELLOW, Color.TRANSPARENT, + Shader.TileMode.CLAMP + ) canvas.drawCircle(t.x, t.y, safeRadius * 1.2f, glowPaint) canvas.drawCircle(t.x + 4f, t.y + 4f, safeRadius, shadowPaint) + outerRingPaint.alpha = alpha middleRingPaint.alpha = alpha centerDotPaint.alpha = alpha + canvas.drawCircle(t.x, t.y, safeRadius, outerRingPaint) canvas.drawCircle(t.x, t.y, safeRadius * 0.66f, middleRingPaint) canvas.drawCircle(t.x, t.y, safeRadius * 0.33f, centerDotPaint) @@ -212,13 +263,10 @@ class TargetTapLoaderView @JvmOverloads constructor( continue } val alpha = ((1f - (age / lifespan.toFloat())) * 255).toInt() - val paint = Paint().apply { - color = Color.YELLOW - this.alpha = alpha - } p.x += p.vx p.y += p.vy - canvas.drawCircle(p.x, p.y, 6f, paint) + particlePaint.alpha = alpha + canvas.drawCircle(p.x, p.y, 6f, particlePaint) } } @@ -235,20 +283,29 @@ class TargetTapLoaderView @JvmOverloads constructor( canvas.drawRoundRect(rect, barRadius, barRadius, progressBarPaint) } - private fun drawIndeterminateSpinner(canvas: Canvas) { - spinnerAngle = (spinnerAngle + 6f) % 360f + + private fun drawIndeterminateSpinner(canvas: Canvas, deltaMs: Long) { val cx = width / 2f val cy = height / 2f val radius = min(width, height) / 6f val sweepAngle = 270f + spinnerAngle = (spinnerAngle + 0.25f * deltaMs) % 360f + + if (spinnerShader == null) { + spinnerShader = SweepGradient( + cx, cy, + intArrayOf(Color.TRANSPARENT, Color.WHITE, Color.TRANSPARENT), + floatArrayOf(0f, 0.5f, 1f) + ) + } + + spinnerPaint.shader = spinnerShader + val glowPaint = Paint(spinnerPaint).apply { maskFilter = BlurMaskFilter(15f, BlurMaskFilter.Blur.SOLID) } - val shader = SweepGradient(cx, cy, intArrayOf(Color.TRANSPARENT, Color.WHITE, Color.TRANSPARENT), floatArrayOf(0f, 0.5f, 1f)) - spinnerPaint.shader = shader - canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius, spinnerAngle, sweepAngle, false, glowPaint) canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius, spinnerAngle, sweepAngle, false, spinnerPaint) } @@ -259,7 +316,9 @@ class TargetTapLoaderView @JvmOverloads constructor( val radius: Float, val spawnTime: Long, var hit: Boolean = false, - var hitTime: Long = 0L + var hitTime: Long = 0L, + val spawnAnimationStartTime: Long = System.currentTimeMillis(), + val idlePulseOffset: Float = Random.nextFloat() * 2f * PI.toFloat() ) private data class Particle( @@ -267,7 +326,6 @@ class TargetTapLoaderView @JvmOverloads constructor( var y: Float, val vx: Float, val vy: Float, - var alpha: Int, val startTime: Long ) } From 5528d71da8688fbe3f721370373a22927e8b0023 Mon Sep 17 00:00:00 2001 From: Koen J Date: Sat, 5 Jul 2025 14:07:49 +0200 Subject: [PATCH 09/19] Show score toast. --- .../com/futo/platformplayer/views/TargetTapLoaderView.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt b/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt index c4c92f75..21ce31b5 100644 --- a/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt @@ -9,6 +9,7 @@ import android.view.MotionEvent import android.view.View import android.view.animation.AccelerateDecelerateInterpolator import androidx.core.graphics.toColorInt +import com.futo.platformplayer.UIDialogs import kotlin.math.* import kotlin.random.Random @@ -128,6 +129,13 @@ class TargetTapLoaderView @JvmOverloads constructor( } fun stopAndResetLoader() { + if (score > 0) { + val now = System.currentTimeMillis() + val dt = (now - startTime) / 1000.0 + UIDialogs.toast("Nice! score was $score, ${"%.${1}f".format(score / dt).toDouble()} (per second)") + score = 0 + } + loaderFinished = true targets.clear() particles.clear() From 08e98b089c28e629b629e6144a16e75ca69e96be Mon Sep 17 00:00:00 2001 From: Koen J Date: Sat, 5 Jul 2025 17:32:31 +0200 Subject: [PATCH 10/19] Improvements to target tap loader game. --- .../views/TargetTapLoaderView.kt | 406 ++++++++++-------- 1 file changed, 217 insertions(+), 189 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt b/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt index 21ce31b5..6319cf73 100644 --- a/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt @@ -8,115 +8,104 @@ import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.View import android.view.animation.AccelerateDecelerateInterpolator +import android.view.animation.OvershootInterpolator +import androidx.core.graphics.ColorUtils import androidx.core.graphics.toColorInt -import com.futo.platformplayer.UIDialogs import kotlin.math.* import kotlin.random.Random +import com.futo.platformplayer.UIDialogs class TargetTapLoaderView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : View(context, attrs) { + private val primaryColor = "#2D63ED".toColorInt() + private val inactiveGlobalAlpha = 110 + private val streakAccelerationStep = .3f + private val minSpawnDelay = 200L + private val idleSpeedMultiplier = .015f + private val baseDeterministicDelay = 700L + private val baseIndeterminateDelay = 1400L + private val overshootInterpolator = OvershootInterpolator(2f) + private val initialSpawnFactor = 2f + private val floatAccel = .03f + private val idleMaxSpeed = .35f + private val idleInitialTargets = 10 + private val idleHintText = "Waiting for media to become available" private var expectedDurationMs: Long? = null - private var startTime: Long = 0L + private var loadStartTime = 0L + private var playStartTime = 0L private var loaderFinished = false - private var forceIndeterminate = false - private var spinnerShader: SweepGradient? = null + private var forceIndeterminate= false private var lastFrameTime = System.currentTimeMillis() - private val bounceInterpolator = android.view.animation.OvershootInterpolator(2f) - private val isIndeterminate: Boolean - get() = forceIndeterminate || expectedDurationMs == null || expectedDurationMs == 0L + private var streak = 0 + private var score = 0 + private var isPlaying = false private val targets = mutableListOf() private val particles = mutableListOf() - private var score = 0 private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.WHITE - textSize = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_SP, 18f, resources.displayMetrics - ) - textAlign = Paint.Align.CENTER + textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 18f, resources.displayMetrics) + textAlign = Paint.Align.LEFT setShadowLayer(4f, 0f, 0f, Color.BLACK) typeface = Typeface.DEFAULT_BOLD } - private val progressBarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = "#2D63ED".toColorInt() - } + private val progressBarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = primaryColor } private val spinnerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = "#2D63ED".toColorInt() - strokeWidth = 12f - style = Paint.Style.STROKE - strokeCap = Paint.Cap.ROUND - } - private val outerRingPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = Color.WHITE - style = Paint.Style.FILL - } - private val middleRingPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = Color.RED - style = Paint.Style.FILL - } - private val centerDotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = Color.YELLOW - style = Paint.Style.FILL - } - private val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = Color.argb(50, 0, 0, 0) + color = primaryColor; strokeWidth = 12f + style = Paint.Style.STROKE; strokeCap = Paint.Cap.ROUND } + private val outerRingPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val middleRingPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val centerDotPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.argb(50, 0, 0, 0) } private val glowPaint = Paint(Paint.ANTI_ALIAS_FLAG) - private val particlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = Color.YELLOW - } + private val particlePaint = Paint(Paint.ANTI_ALIAS_FLAG) private val backgroundPaint = Paint() - + private var spinnerShader: SweepGradient? = null private var spinnerAngle = 0f + private var lastSpawnDelayMs: Long = baseDeterministicDelay + private var currentSpawnDelayMs = baseDeterministicDelay.toFloat() + private val DELAY_SMOOTHING = 0.7f + private val MISS_PENALTY = 1 private val frameRunnable = object : Runnable { - override fun run() { - invalidate() - if (!loaderFinished) postDelayed(this, 16L) - } + override fun run() { invalidate(); if (!loaderFinished) postDelayed(this, 16L) } } - init { - setOnTouchListener { _, event -> - if (event.action == MotionEvent.ACTION_DOWN) { - handleTap(event.x, event.y) - } - true - } - } + init { setOnTouchListener { _, e -> if (e.action == MotionEvent.ACTION_DOWN) handleTap(e.x, e.y); true } } fun startLoader(durationMs: Long? = null) { - val isAlreadyRunning = !loaderFinished - - val newDuration = durationMs?.takeIf { it > 0L } - - if (isAlreadyRunning && newDuration == null) { + val alreadyRunning = !loaderFinished + if (alreadyRunning && durationMs == null) { + expectedDurationMs = null forceIndeterminate = true - startTime = System.currentTimeMillis() return } - expectedDurationMs = newDuration + expectedDurationMs = durationMs?.takeIf { it > 0 } forceIndeterminate = expectedDurationMs == null loaderFinished = false - startTime = System.currentTimeMillis() + isPlaying = false score = 0 - targets.clear() + streak = 0 particles.clear() + + post { if (targets.isEmpty()) prepopulateIdleTargets() } + + loadStartTime = System.currentTimeMillis() + playStartTime = 0 removeCallbacks(frameRunnable) post(frameRunnable) - post { spawnTarget() } if (!isIndeterminate) { postDelayed({ if (!loaderFinished) { forceIndeterminate = true - startTime = System.currentTimeMillis() - spawnTarget() + expectedDurationMs = null } }, expectedDurationMs!!) } @@ -125,73 +114,115 @@ class TargetTapLoaderView @JvmOverloads constructor( fun finishLoader() { loaderFinished = true particles.clear() + isPlaying = false invalidate() } fun stopAndResetLoader() { if (score > 0) { - val now = System.currentTimeMillis() - val dt = (now - startTime) / 1000.0 - UIDialogs.toast("Nice! score was $score, ${"%.${1}f".format(score / dt).toDouble()} (per second)") - score = 0 + val elapsed = (System.currentTimeMillis() - (if (playStartTime > 0) playStartTime else loadStartTime)) / 1000.0 + UIDialogs.toast("Nice! score $score | ${"%.1f".format(score / elapsed)} / s") } - loaderFinished = true + isPlaying = false targets.clear() particles.clear() removeCallbacks(frameRunnable) invalidate() } + private val isIndeterminate get() = forceIndeterminate || expectedDurationMs == null || expectedDurationMs == 0L + private fun handleTap(x: Float, y: Float) { - val now = System.currentTimeMillis() - val hitIndex = targets.indexOfFirst { t -> !t.hit && hypot(x - t.x, y - t.y) <= t.radius } - if (hitIndex >= 0) { + val idx = targets.indexOfFirst { !it.hit && hypot(x - it.x, y - it.y) <= it.radius } + if (idx >= 0) { performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) - val target = targets[hitIndex] - if (!target.hit) { - target.hit = true - target.hitTime = now - score += if (!isIndeterminate) 10 else 5 - spawnParticles(target.x, target.y, target.radius) + val t = targets[idx] + t.hit = true; t.hitTime = System.currentTimeMillis() + streak++ + score += if (!isIndeterminate) 10 else 5 + spawnParticles(t.x, t.y, t.radius) + + if (!isPlaying) { + isPlaying = true + playStartTime = System.currentTimeMillis() + score = 0 + streak = 0 + targets.retainAll { it === t } + spawnTarget() } - } + } else if (isPlaying) applyMissPenalty() } + private fun applyMissPenalty() { streak = max(0, streak - MISS_PENALTY) } + private fun spawnTarget() { - if (loaderFinished) return - if (width <= 0 || height <= 0) { - post { spawnTarget() } - return + if (loaderFinished || width == 0 || height == 0) { + postDelayed({ spawnTarget() }, 200L); return } - val radius = Random.nextInt(40, 80).toFloat() - val x = Random.nextFloat() * (width - 2 * radius) + radius - val y = Random.nextFloat() * (height - 2 * radius - 60f) + radius - targets.add(Target(x, y, radius, System.currentTimeMillis())) + if (!isPlaying) { postDelayed({ spawnTarget() }, 500L); return } - val delay = if (isIndeterminate) 1400L else 700L + val radius = Random.nextInt(40, 80).toFloat() + val x = Random.nextFloat() * (width - 2 * radius) + radius + val y = Random.nextFloat() * (height - 2 * radius - 60f) + radius + + val baseSpeed = Random.nextFloat() + .1f + val speed = baseSpeed + val angle = Random.nextFloat() * TAU + val vx = cos(angle) * speed + val vy = sin(angle) * speed + val alpha = Random.nextInt(150, 255) + + targets += Target(x, y, radius, System.currentTimeMillis(), baseAlpha = alpha, vx = vx, vy = vy) + val delayBase = if (isIndeterminate) baseIndeterminateDelay else baseDeterministicDelay + val streakBoost = 1f + streak * streakAccelerationStep + val baseFactor = if (streak == 0) initialSpawnFactor else 1f + val targetDelay = max(minSpawnDelay.toFloat(), delayBase * baseFactor / streakBoost) + + currentSpawnDelayMs = currentSpawnDelayMs * DELAY_SMOOTHING + targetDelay * (1 - DELAY_SMOOTHING) + val delay = currentSpawnDelayMs.roundToLong() + lastSpawnDelayMs = delay postDelayed({ spawnTarget() }, delay) } + private fun prepopulateIdleTargets() { + if (width == 0 || height == 0) { + post { prepopulateIdleTargets() } + return + } + repeat(idleInitialTargets) { + val radius = Random.nextInt(40, 80).toFloat() + val x = Random.nextFloat() * (width - 2 * radius) + radius + val y = Random.nextFloat() * (height - 2 * radius - 60f) + radius + val angle = Random.nextFloat() * TAU + val speed = (Random.nextFloat() * .3f + .05f) * idleSpeedMultiplier + val vx = cos(angle) * speed + val vy = sin(angle) * speed + val alpha = Random.nextInt(60, 110) + targets += Target(x, y, radius, System.currentTimeMillis(), baseAlpha = alpha, vx = vx, vy = vy) + } + } + private fun spawnParticles(cx: Float, cy: Float, radius: Float) { repeat(12) { - val angle = Random.nextFloat() * 2f * PI.toFloat() + val angle = Random.nextFloat() * TAU val speed = Random.nextFloat() * 5f + 2f val vx = cos(angle) * speed val vy = sin(angle) * speed - particles.add(Particle(cx, cy, vx, vy, System.currentTimeMillis())) + val col = ColorUtils.setAlphaComponent(primaryColor, Random.nextInt(120, 255)) + particles += Particle(cx, cy, vx, vy, System.currentTimeMillis(), col) } } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - drawBackground(canvas) - val now = System.currentTimeMillis() + val now = System.currentTimeMillis() val deltaMs = now - lastFrameTime lastFrameTime = now + drawBackground(canvas) drawTargets(canvas, now) drawParticles(canvas, now) @@ -200,140 +231,137 @@ class TargetTapLoaderView @JvmOverloads constructor( else drawDeterministicProgressBar(canvas, now) } - canvas.drawText("Score: $score", width / 2f, height - 80f, textPaint) + if (isPlaying) { + val margin = 24f + val scoreTxt = "Score $score" + val speed = 1000f / lastSpawnDelayMs + val speedTxt = "Speed ${"%.2f".format(speed)}/s" + val maxWidth = width - margin + val needRight = max(textPaint.measureText(scoreTxt), textPaint.measureText(speedTxt)) > maxWidth - if (loaderFinished) { - canvas.drawText("Loading Complete!", width / 2f, height / 2f, textPaint) + val alignX = if (needRight) (width - margin) else margin + textPaint.textAlign = if (needRight) Paint.Align.RIGHT else Paint.Align.LEFT + + canvas.drawText(scoreTxt, alignX, textPaint.textSize + margin, textPaint) + canvas.drawText(speedTxt, alignX, 2*textPaint.textSize + margin + 4f, textPaint) + textPaint.textAlign = Paint.Align.LEFT + } + else if (loaderFinished) + canvas.drawText("Loading Complete!", width/2f, height/2f, textPaint.apply { textAlign = Paint.Align.CENTER }) + else { + textPaint.textAlign = Paint.Align.CENTER + canvas.drawText(idleHintText, width / 2f, height - 48f, textPaint) + textPaint.textAlign = Paint.Align.LEFT } } private fun drawBackground(canvas: Canvas) { - val gradient = LinearGradient( + backgroundPaint.shader = LinearGradient( 0f, 0f, 0f, height.toFloat(), - Color.rgb(20, 20, 40), Color.BLACK, - Shader.TileMode.CLAMP + Color.rgb(20, 20, 40), Color.BLACK, Shader.TileMode.CLAMP ) - backgroundPaint.shader = gradient canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), backgroundPaint) } private fun drawTargets(canvas: Canvas, now: Long) { - val expireMs = if (isIndeterminate) 2500L else 1500L - targets.removeAll { it.hit && now - it.hitTime > 300L } - targets.removeAll { !it.hit && now - it.spawnTime > expireMs } - - for (t in targets) { - val scale = when { - t.hit -> { - 1f - ((now - t.hitTime) / 300f).coerceIn(0f, 1f) - } - else -> { - val spawnElapsed = now - t.spawnAnimationStartTime - if (spawnElapsed < 300L) { - val animProgress = spawnElapsed / 300f - bounceInterpolator.getInterpolation(animProgress) - } else { - val pulseTime = ((now - t.spawnAnimationStartTime) / 1000f) * 2f * PI.toFloat() + t.idlePulseOffset - 1f + 0.02f * sin(pulseTime) - } - } + val expireMsActive = if (isIndeterminate) 2500L else 1500L + val it = targets.iterator() + while (it.hasNext()) { + val t = it.next() + if (t.hit && now - t.hitTime > 300L) { it.remove(); continue } + if (isPlaying && !t.hit && now - t.spawnTime > expireMsActive) { + it.remove(); applyMissPenalty(); continue } + t.x += t.vx; t.y += t.vy + t.vx += (Random.nextFloat() - .5f) * floatAccel + t.vy += (Random.nextFloat() - .5f) * floatAccel + val speedCap = if (isPlaying) Float.MAX_VALUE else idleMaxSpeed + val mag = hypot(t.vx, t.vy) + if (mag > speedCap) { + val s = speedCap / mag + t.vx *= s; t.vy *= s + } + if (t.x - t.radius < 0 || t.x + t.radius > width) t.vx *= -1 + if (t.y - t.radius < 0 || t.y + t.radius > height) t.vy *= -1 + val scale = if (t.hit) 1f - ((now - t.hitTime) / 300f).coerceIn(0f,1f) + else { + val e = now - t.spawnAnimStart + if (e < 300L) overshootInterpolator.getInterpolation(e/300f) + else 1f + .02f * sin(((now - t.spawnAnimStart)/1000f)*TAU + t.pulseOffset) + } + val animAlpha = if (t.hit) ((1f - scale)*255).toInt() else 255 + val globalAlpha = if (isPlaying) 255 else inactiveGlobalAlpha + val alpha = (animAlpha * t.baseAlpha /255f * globalAlpha/255f).toInt().coerceAtMost(255) + val r = max(1f, t.radius*scale) + val outerCol = ColorUtils.setAlphaComponent(primaryColor, alpha) + val midCol = ColorUtils.setAlphaComponent(primaryColor, (alpha*.7f).toInt()) + val innerCol = ColorUtils.setAlphaComponent(primaryColor, (alpha*.4f).toInt()) + outerRingPaint.color = outerCol; middleRingPaint.color = midCol; centerDotPaint.color = innerCol - val alpha = if (t.hit) ((1f - scale) * 255).toInt().coerceAtMost(255) else 255 - val safeRadius = (t.radius * scale).coerceAtLeast(1f) + glowPaint.shader = RadialGradient(t.x, t.y, r, outerCol, Color.TRANSPARENT, Shader.TileMode.CLAMP) - glowPaint.shader = RadialGradient( - t.x, t.y, safeRadius, - Color.YELLOW, Color.TRANSPARENT, - Shader.TileMode.CLAMP - ) - canvas.drawCircle(t.x, t.y, safeRadius * 1.2f, glowPaint) - canvas.drawCircle(t.x + 4f, t.y + 4f, safeRadius, shadowPaint) - - outerRingPaint.alpha = alpha - middleRingPaint.alpha = alpha - centerDotPaint.alpha = alpha - - canvas.drawCircle(t.x, t.y, safeRadius, outerRingPaint) - canvas.drawCircle(t.x, t.y, safeRadius * 0.66f, middleRingPaint) - canvas.drawCircle(t.x, t.y, safeRadius * 0.33f, centerDotPaint) + canvas.drawCircle(t.x, t.y, r*1.2f, glowPaint) + canvas.drawCircle(t.x+4f, t.y+4f, r, shadowPaint) + canvas.drawCircle(t.x, t.y, r, outerRingPaint) + canvas.drawCircle(t.x, t.y, r*.66f, middleRingPaint) + canvas.drawCircle(t.x, t.y, r*.33f, centerDotPaint) } } private fun drawParticles(canvas: Canvas, now: Long) { val lifespan = 400L - val iterator = particles.iterator() - while (iterator.hasNext()) { - val p = iterator.next() + val it = particles.iterator() + while (it.hasNext()) { + val p = it.next() val age = now - p.startTime - if (age > lifespan) { - iterator.remove() - continue - } - val alpha = ((1f - (age / lifespan.toFloat())) * 255).toInt() - p.x += p.vx - p.y += p.vy - particlePaint.alpha = alpha + if (age > lifespan) { it.remove(); continue } + val a = ((1f - age/lifespan.toFloat())*255).toInt() + particlePaint.color = ColorUtils.setAlphaComponent(p.baseColor, a) + p.x += p.vx; p.y += p.vy canvas.drawCircle(p.x, p.y, 6f, particlePaint) } } private fun drawDeterministicProgressBar(canvas: Canvas, now: Long) { - val duration = expectedDurationMs ?: return - val rawProgress = ((now - startTime).toFloat() / duration).coerceIn(0f, 1f) - val easedProgress = AccelerateDecelerateInterpolator().getInterpolation(rawProgress) - - val barHeight = 20f - val barRadius = 10f - val barWidth = width * easedProgress - - val rect = RectF(0f, height - barHeight, barWidth, height.toFloat()) - canvas.drawRoundRect(rect, barRadius, barRadius, progressBarPaint) + val dur = expectedDurationMs ?: return + val prog = ((now - loadStartTime) / dur.toFloat()).coerceIn(0f, 1f) + val eased = AccelerateDecelerateInterpolator().getInterpolation(prog) + val h = 20f; val r=10f + canvas.drawRoundRect(RectF(0f, height-h, width*eased, height.toFloat()), r, r, progressBarPaint) } - - private fun drawIndeterminateSpinner(canvas: Canvas, deltaMs: Long) { - val cx = width / 2f - val cy = height / 2f - val radius = min(width, height) / 6f - val sweepAngle = 270f - - spinnerAngle = (spinnerAngle + 0.25f * deltaMs) % 360f - - if (spinnerShader == null) { - spinnerShader = SweepGradient( - cx, cy, - intArrayOf(Color.TRANSPARENT, Color.WHITE, Color.TRANSPARENT), - floatArrayOf(0f, 0.5f, 1f) - ) - } - + private fun drawIndeterminateSpinner(canvas: Canvas, dt: Long) { + val cx=width/2f; val cy=height/2f; val r=min(width,height)/6f + spinnerAngle = (spinnerAngle + .25f*dt)%360f + if(spinnerShader == null) spinnerShader = SweepGradient(cx,cy,intArrayOf(Color.TRANSPARENT,Color.WHITE,Color.TRANSPARENT),floatArrayOf(0f,.5f,1f)) spinnerPaint.shader = spinnerShader - - val glowPaint = Paint(spinnerPaint).apply { - maskFilter = BlurMaskFilter(15f, BlurMaskFilter.Blur.SOLID) - } - - canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius, spinnerAngle, sweepAngle, false, glowPaint) - canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius, spinnerAngle, sweepAngle, false, spinnerPaint) + val glow = Paint(spinnerPaint).apply{ maskFilter = BlurMaskFilter(15f,BlurMaskFilter.Blur.SOLID) } + val sweep = 270f + canvas.drawArc(cx-r,cy-r,cx+r,cy+r,spinnerAngle,sweep,false,glow) + canvas.drawArc(cx-r,cy-r,cx+r,cy+r,spinnerAngle,sweep,false,spinnerPaint) } private data class Target( - val x: Float, - val y: Float, + var x: Float, + var y: Float, val radius: Float, val spawnTime: Long, var hit: Boolean = false, var hitTime: Long = 0L, - val spawnAnimationStartTime: Long = System.currentTimeMillis(), - val idlePulseOffset: Float = Random.nextFloat() * 2f * PI.toFloat() + val baseAlpha: Int = 255, + var vx: Float=0f, + var vy:Float=0f, + val spawnAnimStart: Long = System.currentTimeMillis(), + val pulseOffset: Float = Random.nextFloat() * TAU + ) + private data class Particle( + var x:Float, + var y:Float, + val vx:Float, + val vy:Float, + val startTime:Long, + val baseColor:Int ) - private data class Particle( - var x: Float, - var y: Float, - val vx: Float, - val vy: Float, - val startTime: Long - ) + private companion object { private const val TAU = (2 * Math.PI).toFloat() } } From 8b53e9e5e3615b7733f9af0cc4c6ad3f65a070a3 Mon Sep 17 00:00:00 2001 From: Koen J Date: Sat, 5 Jul 2025 18:09:49 +0200 Subject: [PATCH 11/19] Processed last feedback on minigame. --- .../views/TargetTapLoaderView.kt | 92 +++++++++++-------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt b/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt index 6319cf73..6778ea0a 100644 --- a/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt @@ -20,13 +20,8 @@ class TargetTapLoaderView @JvmOverloads constructor( ) : View(context, attrs) { private val primaryColor = "#2D63ED".toColorInt() private val inactiveGlobalAlpha = 110 - private val streakAccelerationStep = .3f - private val minSpawnDelay = 200L private val idleSpeedMultiplier = .015f - private val baseDeterministicDelay = 700L - private val baseIndeterminateDelay = 1400L - private val overshootInterpolator = OvershootInterpolator(2f) - private val initialSpawnFactor = 2f + private val overshootInterpolator = OvershootInterpolator(1.5f) private val floatAccel = .03f private val idleMaxSpeed = .35f private val idleInitialTargets = 10 @@ -39,7 +34,6 @@ class TargetTapLoaderView @JvmOverloads constructor( private var forceIndeterminate= false private var lastFrameTime = System.currentTimeMillis() - private var streak = 0 private var score = 0 private var isPlaying = false @@ -53,6 +47,11 @@ class TargetTapLoaderView @JvmOverloads constructor( setShadowLayer(4f, 0f, 0f, Color.BLACK) typeface = Typeface.DEFAULT_BOLD } + private val idleHintPaint = Paint(textPaint).apply { + textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12f, resources.displayMetrics) + typeface = Typeface.DEFAULT + setShadowLayer(2f, 0f, 0f, Color.BLACK) + } private val progressBarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = primaryColor } private val spinnerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = primaryColor; strokeWidth = 12f @@ -67,10 +66,11 @@ class TargetTapLoaderView @JvmOverloads constructor( private val backgroundPaint = Paint() private var spinnerShader: SweepGradient? = null private var spinnerAngle = 0f - private var lastSpawnDelayMs: Long = baseDeterministicDelay - private var currentSpawnDelayMs = baseDeterministicDelay.toFloat() - private val DELAY_SMOOTHING = 0.7f - private val MISS_PENALTY = 1 + private val MIN_SPAWN_RATE = 1f + private val MAX_SPAWN_RATE = 20.0f + private val HIT_RATE_INCREMENT = 0.15f + private val MISS_RATE_DECREMENT = 0.09f + private var spawnRate = MIN_SPAWN_RATE private val frameRunnable = object : Runnable { override fun run() { invalidate(); if (!loaderFinished) postDelayed(this, 16L) } @@ -91,8 +91,8 @@ class TargetTapLoaderView @JvmOverloads constructor( loaderFinished = false isPlaying = false score = 0 - streak = 0 particles.clear() + spawnRate = MIN_SPAWN_RATE post { if (targets.isEmpty()) prepopulateIdleTargets() } @@ -122,6 +122,7 @@ class TargetTapLoaderView @JvmOverloads constructor( if (score > 0) { val elapsed = (System.currentTimeMillis() - (if (playStartTime > 0) playStartTime else loadStartTime)) / 1000.0 UIDialogs.toast("Nice! score $score | ${"%.1f".format(score / elapsed)} / s") + score = 0 } loaderFinished = true isPlaying = false @@ -139,7 +140,7 @@ class TargetTapLoaderView @JvmOverloads constructor( performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) val t = targets[idx] t.hit = true; t.hitTime = System.currentTimeMillis() - streak++ + accelerateSpawnRate() score += if (!isIndeterminate) 10 else 5 spawnParticles(t.x, t.y, t.radius) @@ -147,21 +148,29 @@ class TargetTapLoaderView @JvmOverloads constructor( isPlaying = true playStartTime = System.currentTimeMillis() score = 0 - streak = 0 + spawnRate = MIN_SPAWN_RATE targets.retainAll { it === t } spawnTarget() } - } else if (isPlaying) applyMissPenalty() + } else if (isPlaying) decelerateSpawnRate() } - private fun applyMissPenalty() { streak = max(0, streak - MISS_PENALTY) } + private inline fun accelerateSpawnRate() { + spawnRate = (spawnRate + HIT_RATE_INCREMENT).coerceAtMost(MAX_SPAWN_RATE) + } + + private inline fun decelerateSpawnRate() { + spawnRate = (spawnRate - MISS_RATE_DECREMENT).coerceAtLeast(MIN_SPAWN_RATE) + } private fun spawnTarget() { if (loaderFinished || width == 0 || height == 0) { postDelayed({ spawnTarget() }, 200L); return } - if (!isPlaying) { postDelayed({ spawnTarget() }, 500L); return } + if (!isPlaying) { + postDelayed({ spawnTarget() }, 500L); return + } val radius = Random.nextInt(40, 80).toFloat() val x = Random.nextFloat() * (width - 2 * radius) + radius @@ -175,14 +184,8 @@ class TargetTapLoaderView @JvmOverloads constructor( val alpha = Random.nextInt(150, 255) targets += Target(x, y, radius, System.currentTimeMillis(), baseAlpha = alpha, vx = vx, vy = vy) - val delayBase = if (isIndeterminate) baseIndeterminateDelay else baseDeterministicDelay - val streakBoost = 1f + streak * streakAccelerationStep - val baseFactor = if (streak == 0) initialSpawnFactor else 1f - val targetDelay = max(minSpawnDelay.toFloat(), delayBase * baseFactor / streakBoost) - currentSpawnDelayMs = currentSpawnDelayMs * DELAY_SMOOTHING + targetDelay * (1 - DELAY_SMOOTHING) - val delay = currentSpawnDelayMs.roundToLong() - lastSpawnDelayMs = delay + val delay = (1000f / spawnRate).roundToLong() postDelayed({ spawnTarget() }, delay) } @@ -234,8 +237,7 @@ class TargetTapLoaderView @JvmOverloads constructor( if (isPlaying) { val margin = 24f val scoreTxt = "Score $score" - val speed = 1000f / lastSpawnDelayMs - val speedTxt = "Speed ${"%.2f".format(speed)}/s" + val speedTxt = "Speed ${"%.2f".format(spawnRate)}/s" val maxWidth = width - margin val needRight = max(textPaint.measureText(scoreTxt), textPaint.measureText(speedTxt)) > maxWidth @@ -249,17 +251,29 @@ class TargetTapLoaderView @JvmOverloads constructor( else if (loaderFinished) canvas.drawText("Loading Complete!", width/2f, height/2f, textPaint.apply { textAlign = Paint.Align.CENTER }) else { - textPaint.textAlign = Paint.Align.CENTER - canvas.drawText(idleHintText, width / 2f, height - 48f, textPaint) - textPaint.textAlign = Paint.Align.LEFT + idleHintPaint.textAlign = Paint.Align.CENTER + canvas.drawText(idleHintText, width / 2f, height - 48f, idleHintPaint) } } private fun drawBackground(canvas: Canvas) { - backgroundPaint.shader = LinearGradient( - 0f, 0f, 0f, height.toFloat(), - Color.rgb(20, 20, 40), Color.BLACK, Shader.TileMode.CLAMP + val colors = intArrayOf( + Color.rgb(20, 20, 40), + Color.rgb(15, 15, 30), + Color.rgb(10, 10, 20), + Color.rgb( 5, 5, 10), + Color.BLACK ) + val pos = floatArrayOf(0f, 0.25f, 0.5f, 0.75f, 1f) + + if (backgroundPaint.shader == null) { + backgroundPaint.shader = LinearGradient( + 0f, 0f, 0f, height.toFloat(), + colors, pos, + Shader.TileMode.CLAMP + ) + } + canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), backgroundPaint) } @@ -270,7 +284,7 @@ class TargetTapLoaderView @JvmOverloads constructor( val t = it.next() if (t.hit && now - t.hitTime > 300L) { it.remove(); continue } if (isPlaying && !t.hit && now - t.spawnTime > expireMsActive) { - it.remove(); applyMissPenalty(); continue + it.remove(); decelerateSpawnRate(); continue } t.x += t.vx; t.y += t.vy t.vx += (Random.nextFloat() - .5f) * floatAccel @@ -289,13 +303,13 @@ class TargetTapLoaderView @JvmOverloads constructor( if (e < 300L) overshootInterpolator.getInterpolation(e/300f) else 1f + .02f * sin(((now - t.spawnAnimStart)/1000f)*TAU + t.pulseOffset) } - val animAlpha = if (t.hit) ((1f - scale)*255).toInt() else 255 + val animAlpha = if (t.hit) ((1f - scale)*255).toInt() else 255 val globalAlpha = if (isPlaying) 255 else inactiveGlobalAlpha - val alpha = (animAlpha * t.baseAlpha /255f * globalAlpha/255f).toInt().coerceAtMost(255) - val r = max(1f, t.radius*scale) - val outerCol = ColorUtils.setAlphaComponent(primaryColor, alpha) - val midCol = ColorUtils.setAlphaComponent(primaryColor, (alpha*.7f).toInt()) - val innerCol = ColorUtils.setAlphaComponent(primaryColor, (alpha*.4f).toInt()) + val alpha = (animAlpha * t.baseAlpha /255f * globalAlpha/255f).toInt().coerceAtMost(255) + val r = max(1f, t.radius*scale) + val outerCol = ColorUtils.setAlphaComponent(primaryColor, alpha) + val midCol = ColorUtils.setAlphaComponent(primaryColor, (alpha*.7f).toInt()) + val innerCol = ColorUtils.setAlphaComponent(primaryColor, (alpha*.4f).toInt()) outerRingPaint.color = outerCol; middleRingPaint.color = midCol; centerDotPaint.color = innerCol glowPaint.shader = RadialGradient(t.x, t.y, r, outerCol, Color.TRANSPARENT, Shader.TileMode.CLAMP) From cd3cea58a445c5e71d324b2ce5a3eba8702ec6f3 Mon Sep 17 00:00:00 2001 From: Koen J Date: Mon, 7 Jul 2025 10:52:42 +0200 Subject: [PATCH 12/19] Fixed race condition when awaiting and changing video source.. --- .../views/video/FutoVideoPlayerBase.kt | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) 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 14740c1a..023c79fc 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 @@ -3,10 +3,12 @@ package com.futo.platformplayer.views.video import android.content.Context import android.net.Uri import android.util.AttributeSet +import android.view.LayoutInflater import android.widget.RelativeLayout import androidx.annotation.OptIn import androidx.lifecycle.coroutineScope import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.lifecycleScope import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException @@ -29,6 +31,8 @@ import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.source.SingleSampleMediaSource import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import com.futo.platformplayer.Settings +import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource @@ -52,9 +56,14 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManif import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource +import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.constructs.TaskHandler +import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException +import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment +import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp @@ -71,6 +80,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.ByteArrayInputStream import java.io.File +import java.util.concurrent.atomic.AtomicInteger import kotlin.math.abs abstract class FutoVideoPlayerBase : RelativeLayout { @@ -117,7 +127,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout { private var _didCallSourceChange = false; private var _lastState: Int = -1; - + private val _swapIdAudio = AtomicInteger(0) + private val _swapIdVideo = AtomicInteger(0) var targetTrackVideoHeight = -1 private set @@ -436,13 +447,15 @@ abstract class FutoVideoPlayerBase : RelativeLayout { private fun swapSourceInternal(videoSource: IVideoSource?, play: Boolean, resume: Boolean): Boolean { + setLoading(false) + val swapId = _swapIdVideo.incrementAndGet() _lastGeneratedDash = null; val didSet = when(videoSource) { is LocalVideoSource -> { swapVideoSourceLocal(videoSource); true; } is JSVideoUrlRangeSource -> { swapVideoSourceUrlRange(videoSource); true; } is IDashManifestWidevineSource -> { swapVideoSourceDashWidevine(videoSource); true } is IDashManifestSource -> { swapVideoSourceDash(videoSource); true;} - is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume); + is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume, swapId); is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; } is IVideoUrlWidevineSource -> { swapVideoSourceUrlWidevine(videoSource); true; } is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; } @@ -453,11 +466,13 @@ abstract class FutoVideoPlayerBase : RelativeLayout { return didSet; } private fun swapSourceInternal(audioSource: IAudioSource?, play: Boolean, resume: Boolean): Boolean { + setLoading(false) + val swapId = _swapIdAudio.incrementAndGet() val didSet = when(audioSource) { is LocalAudioSource -> {swapAudioSourceLocal(audioSource); true; } is JSAudioUrlRangeSource -> { swapAudioSourceUrlRange(audioSource); true; } is JSHLSManifestAudioSource -> { swapAudioSourceHLS(audioSource); true; } - is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume); + is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume, swapId); is IAudioUrlWidevineSource -> { swapAudioSourceUrlWidevine(audioSource); true; } is IAudioUrlSource -> { swapAudioSourceUrl(audioSource); true; } null -> { _lastAudioMediaSource = null; true; } @@ -564,7 +579,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { }.createMediaSource(MediaItem.fromUri(videoSource.url)) } @OptIn(UnstableApi::class) - private fun swapVideoSourceDashRaw(videoSource: JSDashManifestRawSource, play: Boolean, resume: Boolean): Boolean { + private fun swapVideoSourceDashRaw(videoSource: JSDashManifestRawSource, play: Boolean, resume: Boolean, swapId: Int): Boolean { Logger.i(TAG, "Loading VideoSource [Dash]"); if(videoSource.hasGenerate) { @@ -583,6 +598,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout { } } val generated = generatedDef.await(); + if (_swapIdVideo.get() != swapId) { + return@launch + } + withContext(Dispatchers.Main) { setLoading(false) } @@ -708,7 +727,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { } @OptIn(UnstableApi::class) - private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean): Boolean { + private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean, swapId: Int): Boolean { Logger.i(TAG, "Loading AudioSource [DashRaw]"); if(audioSource.hasGenerate) { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { @@ -726,6 +745,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout { } } val generated = generatedDef.await(); + if (_swapIdAudio.get() != swapId) { + return@launch + } withContext(Dispatchers.Main) { setLoading(false) } @@ -889,6 +911,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout { fun clear() { exoPlayer?.player?.stop(); exoPlayer?.player?.clearMediaItems(); + setLoading(false) + _swapIdVideo.incrementAndGet() + _swapIdAudio.incrementAndGet() _lastVideoMediaSource = null; _lastAudioMediaSource = null; _lastSubtitleMediaSource = null; From 37dc7780099d5afb4f4d2ecdeca4138bc14dd33d Mon Sep 17 00:00:00 2001 From: Koen J Date: Mon, 7 Jul 2025 12:45:45 +0200 Subject: [PATCH 13/19] Fixed casting. --- .../platformplayer/casting/StateCasting.kt | 55 +++++++++++++++---- .../mainactivity/main/VideoDetailView.kt | 13 ++++- .../platformplayer/views/casting/CastView.kt | 28 ++++++++++ app/src/main/res/layout/view_cast.xml | 10 ++++ 4 files changed, 93 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index 1e8e1830..a4084383 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -64,6 +64,7 @@ import java.net.URLDecoder import java.net.URLEncoder import java.util.Collections import java.util.UUID +import java.util.concurrent.atomic.AtomicInteger class StateCasting { private val _scopeIO = CoroutineScope(Dispatchers.IO); @@ -89,6 +90,7 @@ class StateCasting { var _resumeCastingDevice: CastingDeviceInfo? = null; private var _nsdManager: NsdManager? = null val isCasting: Boolean get() = activeDevice != null; + private val _castId = AtomicInteger(0) private val _discoveryListeners = mapOf( "_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice), @@ -432,13 +434,18 @@ class StateCasting { action(); } - fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?): Boolean { + fun cancel() { + _castId.incrementAndGet() + } + + fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null): Boolean { val ad = activeDevice ?: return false; if (ad.connectionState != CastConnectionState.CONNECTED) { return false; } val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0; + val castId = _castId.incrementAndGet() var sourceCount = 0; if (videoSource != null) sourceCount++; @@ -466,7 +473,7 @@ class StateCasting { Logger.i(TAG, "Casting as raw DASH"); try { - castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed); + castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed, castId, onLoadingEstimate, onLoading); } catch (e: Throwable) { Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource} audioSource=${audioSource}.", e); } @@ -529,7 +536,7 @@ class StateCasting { StateApp.instance.scope.launch(Dispatchers.IO) { try { - castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed); + castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed, castId, onLoadingEstimate, onLoading); } catch (e: Throwable) { Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource}.", e); } @@ -539,7 +546,7 @@ class StateCasting { StateApp.instance.scope.launch(Dispatchers.IO) { try { - castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed); + castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed, castId, onLoadingEstimate, onLoading); } catch (e: Throwable) { Logger.e(TAG, "Failed to start casting DASH raw audioSource=${audioSource}.", e); } @@ -1236,7 +1243,7 @@ class StateCasting { } @OptIn(UnstableApi::class) - private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List { + private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?, castId: Int, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null) : List { val ad = activeDevice ?: return listOf(); cleanExecutors() @@ -1283,20 +1290,48 @@ class StateCasting { } } - var dashContent = withContext(Dispatchers.IO) { + var dashContent: String = withContext(Dispatchers.IO) { + stopVideo() + //TODO: Include subtitlesURl in the future - return@withContext if (audioSource != null && videoSource != null) { - JSDashManifestMergingRawSource(videoSource, audioSource).generate() + val deferred = if (audioSource != null && videoSource != null) { + JSDashManifestMergingRawSource(videoSource, audioSource).generateAsync(_scopeIO) } else if (audioSource != null) { - audioSource.generate() + audioSource.generateAsync(_scopeIO) } else if (videoSource != null) { - videoSource.generate() + videoSource.generateAsync(_scopeIO) } else { Logger.e(TAG, "Expected at least audio or video to be set") null } + + if (deferred != null) { + try { + withContext(Dispatchers.Main) { + if (deferred.estDuration >= 0) { + onLoadingEstimate?.invoke(deferred.estDuration) + } else { + onLoading?.invoke(true) + } + } + deferred.await() + } finally { + if (castId == _castId.get()) { + withContext(Dispatchers.Main) { + onLoading?.invoke(false) + } + } + } + } else { + return@withContext null + } } ?: throw Exception("Dash is null") + if (castId != _castId.get()) { + Log.i(TAG, "Get DASH cancelled.") + return emptyList() + } + for (representation in representationRegex.findAll(dashContent)) { val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found") dashContent = mediaInitializationRegex.replace(dashContent) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index e9be9fd6..d7ca2f7d 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -806,6 +806,8 @@ class VideoDetailView : ConstraintLayout { _lastVideoSource = null; _lastAudioSource = null; _lastSubtitleSource = null; + _cast.cancel() + StateCasting.instance.cancel() video = null; _container_content_liveChat?.close(); _player.clear(); @@ -1899,7 +1901,13 @@ class VideoDetailView : ConstraintLayout { private fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) { Logger.i(TAG, "loadCurrentVideoCast(video=$video, videoSource=$videoSource, audioSource=$audioSource, resumePositionMs=$resumePositionMs)") - if(StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed)) { + val castSucceeded = StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = { + _cast.setLoading(it) + }, onLoadingEstimate = { + _cast.setLoading(it) + }) + + if (castSucceeded) { _cast.setVideoDetails(video, resumePositionMs / 1000); setCastEnabled(true); } else throw IllegalStateException("Disconnected cast during loading"); @@ -2553,8 +2561,7 @@ class VideoDetailView : ConstraintLayout { _cast.visibility = View.VISIBLE; } else { StateCasting.instance.stopVideo(); - _cast.stopTimeJob(); - _cast.visibility = View.GONE; + _cast.cancel() if (video?.isLive == false) { _player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed()); diff --git a/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt b/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt index fe941fea..161f3dc3 100644 --- a/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt @@ -30,6 +30,7 @@ import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.formatDuration import com.futo.platformplayer.states.StateHistory import com.futo.platformplayer.states.StatePlayer +import com.futo.platformplayer.views.TargetTapLoaderView import com.futo.platformplayer.views.behavior.GestureControlView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -54,6 +55,7 @@ class CastView : ConstraintLayout { private val _timeBar: DefaultTimeBar; private val _background: FrameLayout; private val _gestureControlView: GestureControlView; + private val _loaderGame: TargetTapLoaderView private var _scope: CoroutineScope = CoroutineScope(Dispatchers.Main); private var _updateTimeJob: Job? = null; private var _inPictureInPicture: Boolean = false; @@ -88,6 +90,9 @@ class CastView : ConstraintLayout { _timeBar = findViewById(R.id.time_progress); _background = findViewById(R.id.layout_background); _gestureControlView = findViewById(R.id.gesture_control); + _loaderGame = findViewById(R.id.loader_overlay) + _loaderGame.visibility = View.GONE + _gestureControlView.fullScreenGestureEnabled = false _gestureControlView.setupTouchArea(); _gestureControlView.onSpeedHoldStart.subscribe { @@ -197,6 +202,12 @@ class CastView : ConstraintLayout { _updateTimeJob = null; } + fun cancel() { + stopTimeJob() + setLoading(false) + visibility = View.GONE + } + fun stopAllGestures() { _gestureControlView.stopAllGestures(); } @@ -279,6 +290,7 @@ class CastView : ConstraintLayout { _textDuration.text = (video.duration * 1000).formatDuration(); _timeBar.setPosition(position); _timeBar.setDuration(video.duration); + setLoading(false) } @OptIn(UnstableApi::class) @@ -295,6 +307,7 @@ class CastView : ConstraintLayout { _updateTimeJob?.cancel(); _updateTimeJob = null; _scope.cancel(); + setLoading(false) } private fun getPlaybackStateCompat(): Int { @@ -305,4 +318,19 @@ class CastView : ConstraintLayout { else -> PlaybackStateCompat.STATE_PAUSED; } } + + fun setLoading(isLoading: Boolean) { + if (isLoading) { + _loaderGame.visibility = View.VISIBLE + _loaderGame.startLoader() + } else { + _loaderGame.visibility = View.GONE + _loaderGame.stopAndResetLoader() + } + } + + fun setLoading(expectedDurationMs: Int) { + _loaderGame.visibility = View.VISIBLE + _loaderGame.startLoader(expectedDurationMs.toLong()) + } } \ No newline at end of file diff --git a/app/src/main/res/layout/view_cast.xml b/app/src/main/res/layout/view_cast.xml index 7c82c130..7b0779cd 100644 --- a/app/src/main/res/layout/view_cast.xml +++ b/app/src/main/res/layout/view_cast.xml @@ -189,4 +189,14 @@ app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" /> + + \ No newline at end of file From 736424ae35a50be4ae9f958604e291877da04f16 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Mon, 7 Jul 2025 14:14:39 +0200 Subject: [PATCH 14/19] Refs --- app/src/stable/assets/sources/dailymotion | 2 +- app/src/stable/assets/sources/odysee | 2 +- app/src/stable/assets/sources/twitch | 2 +- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/dailymotion | 2 +- app/src/unstable/assets/sources/odysee | 2 +- app/src/unstable/assets/sources/twitch | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/stable/assets/sources/dailymotion b/app/src/stable/assets/sources/dailymotion index d1154300..850eb812 160000 --- a/app/src/stable/assets/sources/dailymotion +++ b/app/src/stable/assets/sources/dailymotion @@ -1 +1 @@ -Subproject commit d11543001150f96f3383d83fec3341d9321746b8 +Subproject commit 850eb8122dd8348904d55ceb9c3a26b49bcb8a45 diff --git a/app/src/stable/assets/sources/odysee b/app/src/stable/assets/sources/odysee index d37268ab..736c6b95 160000 --- a/app/src/stable/assets/sources/odysee +++ b/app/src/stable/assets/sources/odysee @@ -1 +1 @@ -Subproject commit d37268abe2b32a4677c583ee252d9e439c56a38e +Subproject commit 736c6b953a4613145e32010ff5ee5b08be1baac6 diff --git a/app/src/stable/assets/sources/twitch b/app/src/stable/assets/sources/twitch index f526ce1b..8de3ab18 160000 --- a/app/src/stable/assets/sources/twitch +++ b/app/src/stable/assets/sources/twitch @@ -1 +1 @@ -Subproject commit f526ce1b7690c0e077279edca601c3e1dab14f15 +Subproject commit 8de3ab18f5a154f49f02e2bee1b126a302df260d diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 98255d17..2b724f21 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 98255d1773d288bfa567f122993c7b2845f37ea5 +Subproject commit 2b724f21a727c3fefe16adb38f06aa8730b1b8ec diff --git a/app/src/unstable/assets/sources/dailymotion b/app/src/unstable/assets/sources/dailymotion index d1154300..850eb812 160000 --- a/app/src/unstable/assets/sources/dailymotion +++ b/app/src/unstable/assets/sources/dailymotion @@ -1 +1 @@ -Subproject commit d11543001150f96f3383d83fec3341d9321746b8 +Subproject commit 850eb8122dd8348904d55ceb9c3a26b49bcb8a45 diff --git a/app/src/unstable/assets/sources/odysee b/app/src/unstable/assets/sources/odysee index d37268ab..736c6b95 160000 --- a/app/src/unstable/assets/sources/odysee +++ b/app/src/unstable/assets/sources/odysee @@ -1 +1 @@ -Subproject commit d37268abe2b32a4677c583ee252d9e439c56a38e +Subproject commit 736c6b953a4613145e32010ff5ee5b08be1baac6 diff --git a/app/src/unstable/assets/sources/twitch b/app/src/unstable/assets/sources/twitch index f526ce1b..8de3ab18 160000 --- a/app/src/unstable/assets/sources/twitch +++ b/app/src/unstable/assets/sources/twitch @@ -1 +1 @@ -Subproject commit f526ce1b7690c0e077279edca601c3e1dab14f15 +Subproject commit 8de3ab18f5a154f49f02e2bee1b126a302df260d diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 98255d17..2b724f21 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 98255d1773d288bfa567f122993c7b2845f37ea5 +Subproject commit 2b724f21a727c3fefe16adb38f06aa8730b1b8ec From a4d4835a89463997cee41f7c934586bc96401eab Mon Sep 17 00:00:00 2001 From: Kelvin Date: Mon, 7 Jul 2025 14:26:17 +0200 Subject: [PATCH 15/19] Reduice font size --- .../com/futo/platformplayer/views/TargetTapLoaderView.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt b/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt index 6778ea0a..bf9d3150 100644 --- a/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/TargetTapLoaderView.kt @@ -41,8 +41,8 @@ class TargetTapLoaderView @JvmOverloads constructor( private val particles = mutableListOf() private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = Color.WHITE - textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 18f, resources.displayMetrics) + color = Color.argb(0.7f, 1f, 1f, 1f) + textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12f, resources.displayMetrics) textAlign = Paint.Align.LEFT setShadowLayer(4f, 0f, 0f, Color.BLACK) typeface = Typeface.DEFAULT_BOLD @@ -236,8 +236,8 @@ class TargetTapLoaderView @JvmOverloads constructor( if (isPlaying) { val margin = 24f - val scoreTxt = "Score $score" - val speedTxt = "Speed ${"%.2f".format(spawnRate)}/s" + val scoreTxt = "Score: $score" + val speedTxt = "Speed: ${"%.2f".format(spawnRate)}/s" val maxWidth = width - margin val needRight = max(textPaint.measureText(scoreTxt), textPaint.measureText(speedTxt)) > maxWidth From 8b3b27a2a8045aae0d4ccc00090da3a0f6726f05 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Mon, 7 Jul 2025 16:04:19 +0200 Subject: [PATCH 16/19] Stop pagers silently if the underlying object is closed --- .../platformplayer/api/media/platforms/js/models/JSPager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f363a011..731d0e51 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,7 @@ abstract class JSPager : IPager { } override fun hasMorePages(): Boolean { - return _hasMorePages; + return _hasMorePages && !pager.isClosed; } override fun nextPage() { From 3bfcf655358c42a958ffa80ad98f907e63f89572 Mon Sep 17 00:00:00 2001 From: Koen J Date: Mon, 7 Jul 2025 16:12:03 +0200 Subject: [PATCH 17/19] Fixes to reload required exception handling for casting. --- .../platformplayer/casting/StateCasting.kt | 202 ++++++++---------- .../mainactivity/main/VideoDetailView.kt | 56 +++-- 2 files changed, 134 insertions(+), 124 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index a4084383..bb980d84 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -438,130 +438,108 @@ class StateCasting { _castId.incrementAndGet() } - fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null): Boolean { - val ad = activeDevice ?: return false; - if (ad.connectionState != CastConnectionState.CONNECTED) { - return false; - } + suspend fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null): Boolean { + return withContext(Dispatchers.IO) { + val ad = activeDevice ?: return@withContext false; + if (ad.connectionState != CastConnectionState.CONNECTED) { + return@withContext false; + } - val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0; - val castId = _castId.incrementAndGet() + val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0; + val castId = _castId.incrementAndGet() - var sourceCount = 0; - if (videoSource != null) sourceCount++; - if (audioSource != null) sourceCount++; - if (subtitleSource != null) sourceCount++; + var sourceCount = 0; + if (videoSource != null) sourceCount++; + if (audioSource != null) sourceCount++; + if (subtitleSource != null) sourceCount++; - if (sourceCount < 1) { - throw Exception("At least one source should be specified."); - } + if (sourceCount < 1) { + throw Exception("At least one source should be specified."); + } - if (sourceCount > 1) { - if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) { - if (ad is AirPlayCastingDevice) { - Logger.i(TAG, "Casting as local HLS"); - castLocalHls(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed); + if (sourceCount > 1) { + if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) { + if (ad is AirPlayCastingDevice) { + Logger.i(TAG, "Casting as local HLS"); + castLocalHls(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed); + } else { + Logger.i(TAG, "Casting as local DASH"); + castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed); + } } else { - Logger.i(TAG, "Casting as local DASH"); - castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed); - } - } else { - StateApp.instance.scope.launch(Dispatchers.IO) { - try { - val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource - if (isRawDash) { - Logger.i(TAG, "Casting as raw DASH"); + val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource + if (isRawDash) { + Logger.i(TAG, "Casting as raw DASH"); - try { - castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed, castId, onLoadingEstimate, onLoading); - } catch (e: Throwable) { - Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource} audioSource=${audioSource}.", e); - } + castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed, castId, onLoadingEstimate, onLoading); + } else { + if (ad is FCastCastingDevice) { + Logger.i(TAG, "Casting as DASH direct"); + castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed); + } else if (ad is AirPlayCastingDevice) { + Logger.i(TAG, "Casting as HLS indirect"); + castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed); } else { - if (ad is FCastCastingDevice) { - Logger.i(TAG, "Casting as DASH direct"); - castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed); - } else if (ad is AirPlayCastingDevice) { - Logger.i(TAG, "Casting as HLS indirect"); - castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed); - } else { - Logger.i(TAG, "Casting as DASH indirect"); - castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed); - } + Logger.i(TAG, "Casting as DASH indirect"); + castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed); } - } catch (e: Throwable) { - Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e); - } - } - } - } else { - val proxyStreams = Settings.instance.casting.alwaysProxyRequests; - val url = getLocalUrl(ad); - val id = UUID.randomUUID(); - - if (videoSource is IVideoUrlSource) { - val videoPath = "/video-${id}" - val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl(); - Logger.i(TAG, "Casting as singular video"); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed); - } else if (audioSource is IAudioUrlSource) { - val audioPath = "/audio-${id}" - val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl(); - Logger.i(TAG, "Casting as singular audio"); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed); - } else if(videoSource is IHLSManifestSource) { - if (proxyStreams || ad is ChromecastCastingDevice) { - Logger.i(TAG, "Casting as proxied HLS"); - castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed); - } else { - Logger.i(TAG, "Casting as non-proxied HLS"); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed); - } - } else if(audioSource is IHLSManifestAudioSource) { - if (proxyStreams || ad is ChromecastCastingDevice) { - Logger.i(TAG, "Casting as proxied audio HLS"); - castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed); - } else { - Logger.i(TAG, "Casting as non-proxied audio HLS"); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble(), speed); - } - } else if (videoSource is LocalVideoSource) { - Logger.i(TAG, "Casting as local video"); - castLocalVideo(video, videoSource, resumePosition, speed); - } else if (audioSource is LocalAudioSource) { - Logger.i(TAG, "Casting as local audio"); - castLocalAudio(video, audioSource, resumePosition, speed); - } else if (videoSource is JSDashManifestRawSource) { - Logger.i(TAG, "Casting as JSDashManifestRawSource video"); - - StateApp.instance.scope.launch(Dispatchers.IO) { - try { - castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed, castId, onLoadingEstimate, onLoading); - } catch (e: Throwable) { - Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource}.", e); - } - } - } else if (audioSource is JSDashManifestRawAudioSource) { - Logger.i(TAG, "Casting as JSDashManifestRawSource audio"); - - StateApp.instance.scope.launch(Dispatchers.IO) { - try { - castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed, castId, onLoadingEstimate, onLoading); - } catch (e: Throwable) { - Logger.e(TAG, "Failed to start casting DASH raw audioSource=${audioSource}.", e); } } } else { - var str = listOf( - if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null, - if(audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null, - if(subtitleSource != null) "Subtitles: ${subtitleSource::class.java.simpleName}" else null - ).filterNotNull().joinToString(", "); - throw UnsupportedCastException(str); - } - } + val proxyStreams = Settings.instance.casting.alwaysProxyRequests; + val url = getLocalUrl(ad); + val id = UUID.randomUUID(); - return true; + if (videoSource is IVideoUrlSource) { + val videoPath = "/video-${id}" + val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl(); + Logger.i(TAG, "Casting as singular video"); + ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed); + } else if (audioSource is IAudioUrlSource) { + val audioPath = "/audio-${id}" + val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl(); + Logger.i(TAG, "Casting as singular audio"); + ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed); + } else if(videoSource is IHLSManifestSource) { + if (proxyStreams || ad is ChromecastCastingDevice) { + Logger.i(TAG, "Casting as proxied HLS"); + castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed); + } else { + Logger.i(TAG, "Casting as non-proxied HLS"); + ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed); + } + } else if(audioSource is IHLSManifestAudioSource) { + if (proxyStreams || ad is ChromecastCastingDevice) { + Logger.i(TAG, "Casting as proxied audio HLS"); + castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed); + } else { + Logger.i(TAG, "Casting as non-proxied audio HLS"); + ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble(), speed); + } + } else if (videoSource is LocalVideoSource) { + Logger.i(TAG, "Casting as local video"); + castLocalVideo(video, videoSource, resumePosition, speed); + } else if (audioSource is LocalAudioSource) { + Logger.i(TAG, "Casting as local audio"); + castLocalAudio(video, audioSource, resumePosition, speed); + } else if (videoSource is JSDashManifestRawSource) { + Logger.i(TAG, "Casting as JSDashManifestRawSource video"); + castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed, castId, onLoadingEstimate, onLoading); + } else if (audioSource is JSDashManifestRawAudioSource) { + Logger.i(TAG, "Casting as JSDashManifestRawSource audio"); + castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed, castId, onLoadingEstimate, onLoading); + } else { + var str = listOf( + if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null, + if(audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null, + if(subtitleSource != null) "Subtitles: ${subtitleSource::class.java.simpleName}" else null + ).filterNotNull().joinToString(", "); + throw UnsupportedCastException(str); + } + } + + return@withContext true; + } } fun resumeVideo(): Boolean { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index d7ca2f7d..780917ed 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -4,6 +4,7 @@ import android.app.PictureInPictureParams import android.app.RemoteAction import android.content.ClipData import android.content.ClipboardManager +import android.content.ContentResolver import android.content.Context import android.content.Intent import android.content.res.Configuration @@ -79,7 +80,9 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo 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.models.JSVideo import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails +import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.casting.CastConnectionState import com.futo.platformplayer.casting.StateCasting @@ -1900,17 +1903,46 @@ class VideoDetailView : ConstraintLayout { } private fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) { Logger.i(TAG, "loadCurrentVideoCast(video=$video, videoSource=$videoSource, audioSource=$audioSource, resumePositionMs=$resumePositionMs)") + castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed) + } - val castSucceeded = StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = { - _cast.setLoading(it) - }, onLoadingEstimate = { - _cast.setLoading(it) - }) + private fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) { + fragment.lifecycleScope.launch(Dispatchers.IO) { + try { + val plugin = if (videoSource is JSSource) videoSource.getUnderlyingPlugin() + else if (audioSource is JSSource) audioSource.getUnderlyingPlugin() + else null - if (castSucceeded) { - _cast.setVideoDetails(video, resumePositionMs / 1000); - setCastEnabled(true); - } else throw IllegalStateException("Disconnected cast during loading"); + val startId = plugin?.getUnderlyingPlugin()?.runtimeId + try { + val castingSucceeded = StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = { + _cast.setLoading(it) + }, onLoadingEstimate = { + _cast.setLoading(it) + }) + + if (castingSucceeded) { + withContext(Dispatchers.Main) { + _cast.setVideoDetails(video, resumePositionMs / 1000); + setCastEnabled(true); + } + } + } catch (e: ScriptReloadRequiredException) { + Log.i(TAG, "Reload required exception", e) + if (plugin == null) + throw e + + if (startId != -1 && plugin.getUnderlyingPlugin().runtimeId != startId) + throw e + + StatePlatform.instance.handleReloadRequired(e, { + fetchVideo() + }); + } + } catch (e: Throwable) { + Logger.e(TAG, "loadCurrentVideoCast", e) + } + } } //Events @@ -2423,7 +2455,7 @@ class VideoDetailView : ConstraintLayout { val d = StateCasting.instance.activeDevice; if (d != null && d.connectionState == CastConnectionState.CONNECTED) - StateCasting.instance.castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); + castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true)) _player.hideControls(false); //TODO: Disable player? @@ -2438,7 +2470,7 @@ class VideoDetailView : ConstraintLayout { val d = StateCasting.instance.activeDevice; if (d != null && d.connectionState == CastConnectionState.CONNECTED) - StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); + castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed) else(!_player.swapSources(_lastVideoSource, audioSource, true, true, true)) _player.hideControls(false); //TODO: Disable player? @@ -2454,7 +2486,7 @@ class VideoDetailView : ConstraintLayout { val d = StateCasting.instance.activeDevice; if (d != null && d.connectionState == CastConnectionState.CONNECTED) - StateCasting.instance.castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); + castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); else _player.swapSubtitles(fragment.lifecycleScope, toSet); From 96ca3f62a2140a066d26935bc8b5319cc0e3421a Mon Sep 17 00:00:00 2001 From: Kelvin Date: Mon, 7 Jul 2025 16:18:23 +0200 Subject: [PATCH 18/19] Missing invokev8 wrappers --- .../api/media/platforms/js/models/JSArticleDetails.kt | 5 +++-- .../api/media/platforms/js/models/JSComment.kt | 3 ++- .../api/media/platforms/js/models/JSPostDetails.kt | 5 +++-- .../api/media/platforms/js/models/JSSubtitleSource.kt | 3 ++- .../api/media/platforms/js/models/JSVideoDetails.kt | 7 ++++--- 5 files changed, 14 insertions(+), 9 deletions(-) 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 0e2c0e32..40c63d48 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 @@ -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.invokeV8 import com.futo.platformplayer.states.StateDeveloper open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails { @@ -85,12 +86,12 @@ open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced } private fun getContentRecommendationsJS(client: JSClient): JSContentPager { - val contentPager = _content.invoke("getContentRecommendations", arrayOf()); + val contentPager = _content.invokeV8("getContentRecommendations", arrayOf()); return JSContentPager(_pluginConfig, client, contentPager); } private fun getCommentsJS(client: JSClient): JSCommentPager { - val commentPager = _content.invoke("getComments", arrayOf()); + val commentPager = _content.invokeV8("getComments", arrayOf()); return JSCommentPager(_pluginConfig, client, commentPager); } 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 ab847b6b..7767ef78 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 @@ -12,6 +12,7 @@ import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrowNullable +import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer import java.time.LocalDateTime import java.time.OffsetDateTime @@ -60,7 +61,7 @@ class JSComment : IPlatformComment { if(!_hasGetReplies) return null; - val obj = _comment!!.invoke("getReplies", arrayOf()); + 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); } 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 6c80d7dc..4d48a354 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.invokeV8 import com.futo.platformplayer.states.StateDeveloper class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails { @@ -68,12 +69,12 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails { return null; } private fun getContentRecommendationsJS(client: JSClient): JSContentPager { - val contentPager = _content.invoke("getContentRecommendations", arrayOf()); + val contentPager = _content.invokeV8("getContentRecommendations", arrayOf()); return JSContentPager(_pluginConfig, client, contentPager); } private fun getCommentsJS(client: JSClient): JSCommentPager { - val commentPager = _content.invoke("getComments", arrayOf()); + val commentPager = _content.invokeV8("getComments", arrayOf()); return JSCommentPager(_pluginConfig, client, commentPager); } 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 259a89e4..74843d22 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 @@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getSourcePlugin +import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.states.StateApp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -38,7 +39,7 @@ class JSSubtitleSource : ISubtitleSource { throw IllegalStateException("This subtitle doesn't support getSubtitles.."); return _obj.getSourcePlugin()?.busy { - val v8String = _obj.invoke("getSubtitles", arrayOf()); + val v8String = _obj.invokeV8("getSubtitles", arrayOf()); return@busy v8String.value; } ?: ""; } 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 cecb2913..abea9550 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 @@ -24,6 +24,7 @@ import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrowNullable +import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.states.StateDeveloper class JSVideoDetails : JSVideo, IPlatformVideoDetails { @@ -86,7 +87,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { private fun getPlaybackTrackerJS(): IPlaybackTracker? { return _plugin.busy { V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") { - val tracker = _content.invoke("getPlaybackTracker", arrayOf()) + val tracker = _content.invokeV8("getPlaybackTracker", arrayOf()) ?: return@catchScriptErrors null; if(tracker is V8ValueObject) return@catchScriptErrors JSPlaybackTracker(_plugin, tracker); @@ -111,7 +112,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { } private fun getContentRecommendationsJS(client: JSClient): JSContentPager { return _plugin.busy { - val contentPager = _content.invoke("getContentRecommendations", arrayOf()); + val contentPager = _content.invokeV8("getContentRecommendations", arrayOf()); return@busy JSContentPager(_pluginConfig, client, contentPager); } } @@ -130,7 +131,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { private fun getCommentsJS(client: JSClient): IPager? { return _plugin.busy { - val commentPager = _content.invoke("getComments", arrayOf()); + val commentPager = _content.invokeV8("getComments", arrayOf()); if (commentPager !is V8ValueObject) //TODO: Maybe handle this better? return@busy null; From 01cb544dfd3e8a00c1da10cff3beccbb856a2396 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Mon, 7 Jul 2025 16:42:37 +0200 Subject: [PATCH 19/19] Dont lock clients when disabling --- .../com/futo/platformplayer/states/StatePlatform.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 1f1e5625..bd952cf2 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt @@ -395,8 +395,9 @@ class StatePlatform { } suspend fun selectClients(afterLoad: (() -> Unit)?, vararg ids: String) { withContext(Dispatchers.IO) { + var removed: MutableList; synchronized(_clientsLock) { - val removed = _enabledClients.toMutableList(); + removed = _enabledClients.toMutableList(); _enabledClients.clear(); for (id in ids) { val client = getClient(id); @@ -412,11 +413,11 @@ class StatePlatform { } _enabledClientsPersistent.set(*ids); _enabledClientsPersistent.save(); + } - for (oldClient in removed) { - oldClient.disable(); - onSourceDisabled.emit(oldClient); - } + for (oldClient in removed) { + oldClient.disable(); + onSourceDisabled.emit(oldClient); } afterLoad?.invoke(); };