diff --git a/app/build.gradle b/app/build.gradle index fcbd422c..278e8b0f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -180,6 +180,7 @@ dependencies { //JS implementation("com.caoccao.javet:javet-android:3.0.2") + //implementation 'com.caoccao.javet:javet-v8-android:4.1.4' //Change after extensive testing the freezing edge cases are solved. //Exoplayer implementation 'androidx.media3:media3-exoplayer:1.2.1' diff --git a/app/src/androidTest/java/com/futo/platformplayer/SyncServerTests.kt b/app/src/androidTest/java/com/futo/platformplayer/SyncServerTests.kt index 7607a2c9..f3e12645 100644 --- a/app/src/androidTest/java/com/futo/platformplayer/SyncServerTests.kt +++ b/app/src/androidTest/java/com/futo/platformplayer/SyncServerTests.kt @@ -11,7 +11,7 @@ import java.nio.ByteBuffer import kotlin.random.Random import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds - +/* class SyncServerTests { //private val relayHost = "relay.grayjay.app" @@ -335,4 +335,4 @@ class SyncServerTests { class AlwaysAuthorized : IAuthorizable { override val isAuthorized: Boolean get() = true -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/app/src/androidTest/java/com/futo/platformplayer/SyncTests.kt b/app/src/androidTest/java/com/futo/platformplayer/SyncTests.kt index 1b9f19cd..d34bfad4 100644 --- a/app/src/androidTest/java/com/futo/platformplayer/SyncTests.kt +++ b/app/src/androidTest/java/com/futo/platformplayer/SyncTests.kt @@ -13,7 +13,7 @@ import kotlin.random.Random import java.io.InputStream import java.io.OutputStream import kotlin.time.Duration.Companion.seconds - +/* data class PipeStreams( val initiatorInput: LittleEndianDataInputStream, val initiatorOutput: LittleEndianDataOutputStream, @@ -509,4 +509,4 @@ class Authorized : IAuthorizable { class Unauthorized : IAuthorizable { override val isAuthorized: Boolean = false -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/app/src/main/assets/scripts/source.js b/app/src/main/assets/scripts/source.js index 0638f079..9f38404d 100644 --- a/app/src/main/assets/scripts/source.js +++ b/app/src/main/assets/scripts/source.js @@ -103,6 +103,12 @@ class UnavailableException extends ScriptException { super("UnavailableException", msg); } } +class ReloadRequiredException extends ScriptException { + constructor(msg, reloadData) { + super("ReloadRequiredException", msg); + this.reloadData = reloadData; + } +} class AgeException extends ScriptException { constructor(msg) { super("AgeException", msg); 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 e31d3dac..240a6cfc 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt @@ -5,7 +5,9 @@ import com.caoccao.javet.values.primitive.* import com.caoccao.javet.values.reference.V8ValueArray import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.exceptions.ScriptImplementationException +import com.futo.platformplayer.logging.Logger //V8 @@ -24,6 +26,10 @@ fun V8Value?.orDefault(default: R, handler: (V8Value)->R): R { return handler(this); } +inline fun V8Value.getSourcePlugin(): V8Plugin? { + return V8Plugin.getPluginFromRuntime(this.v8Runtime); +} + inline fun V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T { if(this !is T) throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}"); @@ -89,7 +95,29 @@ inline fun V8ValueArray.expectV8Variants(config: IV8PluginConfig, co .map { kv-> kv.second.orNull { it.expectV8Variant(config, contextName + "[${kv.first}]", ) } as T }; } +inline fun V8Plugin.ensureIsBusy() { + this.let { + if (!it.isThreadAlreadyBusy()) { + //throw IllegalStateException("Tried to access V8Plugin without busy"); + val stacktrace = Thread.currentThread().stackTrace; + Logger.w("Extensions_V8", + "V8 USE OUTSIDE BUSY: " + stacktrace.drop(3)?.firstOrNull().toString() + + ", " + stacktrace.drop(4)?.firstOrNull().toString() + + ", " + stacktrace.drop(5)?.firstOrNull()?.toString() + + ", " + stacktrace.drop(6)?.firstOrNull()?.toString() + ); + } + } +} +inline fun V8Value.ensureIsBusy() { + this?.getSourcePlugin()?.let { + it.ensureIsBusy(); + } +} + inline fun V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T { + if(false) + ensureIsBusy(); return when(T::class) { String::class -> this.expectOrThrow(config, contextName).value as T; Int::class -> { diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index e8f4f70a..da414d8f 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -584,6 +584,25 @@ class Settings : FragmentedStorageFileJson() { playbackSpeeds.sort(); return playbackSpeeds; } + + @FormField(R.string.hold_playback_speed, FieldForm.DROPDOWN, R.string.hold_playback_speed_description, 27) + @DropdownFieldOptionsId(R.array.hold_playback_speeds) + var holdPlaybackSpeed: Int = 4; + + fun getHoldPlaybackSpeed(): Double { + return when(holdPlaybackSpeed) { + 0 -> 1.0 + 1 -> 1.25 + 2 -> 1.5 + 3 -> 1.75 + 4 -> 2.0 + 5 -> 2.25 + 6 -> 2.5 + 7 -> 2.75 + 8 -> 3.0 + else -> 2.0 + } + } } @FormField(R.string.comments, "group", R.string.comments_description, 6) @@ -999,10 +1018,13 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3) var playlistAllowDups: Boolean = true; - @FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4) + @FormField(R.string.watch_later_add_start, FieldForm.TOGGLE, R.string.watch_later_add_start_description, 4) + var watchLaterAddStart: Boolean = true; + + @FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 5) var polycentricEnabled: Boolean = true; - @FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5) + @FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7) var polycentricLocalCache: Boolean = true; } diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index 3db07410..83387081 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -1151,6 +1151,8 @@ class UISlideOverlays { call = { if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true)) UIDialogs.appToast("Added to watch later", false); + else + UIDialogs.toast(container.context.getString(R.string.already_in_watch_later)) }), ) ); diff --git a/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt index a0a0fac1..9cf58134 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/PolycentricBackupActivity.kt @@ -14,10 +14,12 @@ import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.R import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StateApp.Companion.withContext import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.views.buttons.BigButton import com.futo.polycentric.core.ContentType @@ -29,6 +31,9 @@ import com.futo.polycentric.core.toBase64Url import com.google.zxing.BarcodeFormat import com.google.zxing.MultiFormatWriter import com.google.zxing.common.BitMatrix +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import userpackage.Protocol import userpackage.Protocol.ExportBundle import userpackage.Protocol.URLInfo @@ -39,6 +44,7 @@ class PolycentricBackupActivity : AppCompatActivity() { private lateinit var _imageQR: ImageView; private lateinit var _exportBundle: String; private lateinit var _textQR: TextView; + private lateinit var _loader: View override fun attachBaseContext(newBase: Context?) { super.attachBaseContext(StateApp.instance.getLocaleContext(newBase)) @@ -49,24 +55,47 @@ class PolycentricBackupActivity : AppCompatActivity() { setContentView(R.layout.activity_polycentric_backup); setNavigationBarColorAndIcons(); - _buttonShare = findViewById(R.id.button_share); - _buttonCopy = findViewById(R.id.button_copy); - _imageQR = findViewById(R.id.image_qr); - _textQR = findViewById(R.id.text_qr); + _buttonShare = findViewById(R.id.button_share) + _buttonCopy = findViewById(R.id.button_copy) + _imageQR = findViewById(R.id.image_qr) + _textQR = findViewById(R.id.text_qr) + _loader = findViewById(R.id.progress_loader) findViewById(R.id.button_back).setOnClickListener { finish(); }; - _exportBundle = createExportBundle(); + _imageQR.visibility = View.INVISIBLE + _textQR.visibility = View.INVISIBLE + _loader.visibility = View.VISIBLE + _buttonShare.visibility = View.INVISIBLE + _buttonCopy.visibility = View.INVISIBLE - try { - val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt(); - val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension); - _imageQR.setImageBitmap(qrCodeBitmap); - } catch (e: Exception) { - Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e); - _imageQR.visibility = View.INVISIBLE; - _textQR.visibility = View.INVISIBLE; + lifecycleScope.launch { + try { + val pair = withContext(Dispatchers.IO) { + val bundle = createExportBundle() + val dimension = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics + ).toInt() + val qr = generateQRCode(bundle, dimension, dimension) + Pair(bundle, qr) + } + + _exportBundle = pair.first + _imageQR.setImageBitmap(pair.second) + _imageQR.visibility = View.VISIBLE + _textQR.visibility = View.VISIBLE + _buttonShare.visibility = View.VISIBLE + _buttonCopy.visibility = View.VISIBLE + } catch (e: Exception) { + Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e) + _imageQR.visibility = View.INVISIBLE + _textQR.visibility = View.INVISIBLE + _buttonShare.visibility = View.INVISIBLE + _buttonCopy.visibility = View.INVISIBLE + } finally { + _loader.visibility = View.GONE + } } _buttonShare.onClick.subscribe { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/PlatformID.kt b/app/src/main/java/com/futo/platformplayer/api/media/PlatformID.kt index 9b063c9b..4ff3a549 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/PlatformID.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/PlatformID.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrowNullable @@ -44,6 +45,7 @@ class PlatformID { val NONE = PlatformID("Unknown", null); fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID { + value.ensureIsBusy(); val contextName = "PlatformID"; return PlatformID( value.getOrThrow(config, "platform", contextName), diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorLink.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorLink.kt index 330597a4..831f8ef7 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorLink.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorLink.kt @@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.models.JSContent +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow @@ -33,6 +34,7 @@ open class PlatformAuthorLink { val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null); fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink { + value.ensureIsBusy(); if(value.has("membershipUrl")) return PlatformAuthorMembershipLink.fromV8(config, value); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorMembershipLink.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorMembershipLink.kt index 03abad1a..6b73842f 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorMembershipLink.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/PlatformAuthorMembershipLink.kt @@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow @@ -20,6 +21,7 @@ class PlatformAuthorMembershipLink: PlatformAuthorLink { companion object { fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink { + value.ensureIsBusy(); val context = "AuthorMembershipLink" return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)), value.getOrThrow(config ,"name", context), diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/ResultCapabilities.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/ResultCapabilities.kt index fd24de30..e95b3fe0 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/ResultCapabilities.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/ResultCapabilities.kt @@ -5,6 +5,7 @@ import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.reference.V8ValueArray import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.expectV8Variant import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow @@ -46,6 +47,7 @@ class ResultCapabilities( fun fromV8(config: IV8PluginConfig, value: V8ValueObject): ResultCapabilities { val contextName = "ResultCapabilities"; + value.ensureIsBusy(); return ResultCapabilities( value.getOrThrow(config, "types", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.types") }, value.getOrThrow(config, "sorts", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.sorts"); }, @@ -69,6 +71,7 @@ class FilterGroup( companion object { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): FilterGroup { + value.ensureIsBusy(); return FilterGroup( value.getString("name"), value.getOrDefault(config, "filters", "FilterGroup", null) @@ -90,6 +93,7 @@ class FilterCapability( companion object { fun fromV8(obj: V8ValueObject): FilterCapability { + obj.ensureIsBusy(); val value = obj.get("value") as V8Value; return FilterCapability( obj.getString("name"), diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/Thumbnails.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/Thumbnails.kt index a30d31c9..b25936a0 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/Thumbnails.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/Thumbnails.kt @@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueArray import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.V8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow @@ -31,6 +32,7 @@ class Thumbnails { companion object { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails { + value.ensureIsBusy(); return Thumbnails((value.getOrThrow(config, "sources", "Thumbnails")) .toArray() .map { Thumbnail.fromV8(config, it as V8ValueObject) } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/IPlatformLiveEvent.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/IPlatformLiveEvent.kt index 89826b01..19b4bbb9 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/IPlatformLiveEvent.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/IPlatformLiveEvent.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow interface IPlatformLiveEvent { @@ -10,6 +11,7 @@ interface IPlatformLiveEvent { companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent { + obj.ensureIsBusy(); val t = LiveEventType.fromInt(obj.getOrThrow(config, "type", contextName)); return when(t) { LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventComment.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventComment.kt index 28bbe15a..8b9883ef 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventComment.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventComment.kt @@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueArray import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.api.media.models.ratings.RatingLikes import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow @@ -27,6 +28,8 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage { companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment { + obj.ensureIsBusy(); + val contextName = "LiveEventComment" val colorName = obj.getOrDefault(config, "colorName", contextName, null); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventDonation.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventDonation.kt index a4ac5d47..f8cbafe6 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventDonation.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventDonation.kt @@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.live import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.api.media.models.ratings.RatingLikes import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow @@ -37,6 +38,7 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage { companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventDonation { + obj.ensureIsBusy(); val contextName = "LiveEventDonation" return LiveEventDonation( obj.getOrThrow(config, "name", contextName), diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventEmojis.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventEmojis.kt index 6e29bac5..7028d59d 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventEmojis.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventEmojis.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow class LiveEventEmojis: IPlatformLiveEvent { @@ -15,6 +16,7 @@ class LiveEventEmojis: IPlatformLiveEvent { companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis { + obj.ensureIsBusy(); val contextName = "LiveEventEmojis" return LiveEventEmojis( obj.getOrThrow(config, "emojis", contextName)); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventRaid.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventRaid.kt index ff5dd36f..f43a7c5b 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventRaid.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventRaid.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow class LiveEventRaid: IPlatformLiveEvent { @@ -19,6 +20,7 @@ class LiveEventRaid: IPlatformLiveEvent { companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventRaid { + obj.ensureIsBusy(); val contextName = "LiveEventRaid" return LiveEventRaid( obj.getOrThrow(config, "targetName", contextName), diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventViewCount.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventViewCount.kt index adcfb883..5e48e984 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventViewCount.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventViewCount.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow class LiveEventViewCount: IPlatformLiveEvent { @@ -15,6 +16,7 @@ class LiveEventViewCount: IPlatformLiveEvent { companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount { + obj.ensureIsBusy(); val contextName = "LiveEventViewCount" return LiveEventViewCount( obj.getOrThrow(config, "viewCount", contextName)); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/IRating.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/IRating.kt index 75286b44..1fdbb442 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/IRating.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/IRating.kt @@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.ratings import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.orDefault import com.futo.platformplayer.serializers.IRatingSerializer @@ -13,8 +14,12 @@ interface IRating { companion object { - fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) }; + fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating): IRating { + obj?.ensureIsBusy(); + return obj.orDefault(default) { fromV8(config, it as V8ValueObject) } + }; fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating { + obj.ensureIsBusy(); val t = RatingType.fromInt(obj.getOrThrow(config, "type", contextName)); return when(t) { RatingType.LIKES -> RatingLikes.fromV8(config, obj); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikeDislikes.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikeDislikes.kt index 6d0e787b..8ccc6b2e 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikeDislikes.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikeDislikes.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow /** @@ -14,6 +15,7 @@ class RatingLikeDislikes(val likes: Long, val dislikes: Long) : IRating { companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikeDislikes { + obj.ensureIsBusy(); return RatingLikeDislikes(obj.getOrThrow(config, "likes", "RatingLikeDislikes"), obj.getOrThrow(config, "dislikes", "RatingLikeDislikes")); } } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikes.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikes.kt index e40169f2..0a45f15b 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikes.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingLikes.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow /** @@ -13,6 +14,7 @@ class RatingLikes(val likes: Long) : IRating { companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikes { + obj.ensureIsBusy(); return RatingLikes(obj.getOrThrow(config, "likes", "RatingLikes")); } } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingScaler.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingScaler.kt index 7646cf24..d656df5f 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingScaler.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/ratings/RatingScaler.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow /** @@ -13,6 +14,7 @@ class RatingScaler(val value: Float) : IRating { companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingScaler { + obj.ensureIsBusy() return RatingScaler(obj.getOrThrow(config, "value", "RatingScaler")); } } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt index b26abe45..1f29bf2a 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/DevJSClient.kt @@ -56,6 +56,7 @@ class DevJSClient : JSClient { override fun getCopy(privateCopy: Boolean, noSaveState: Boolean): JSClient { val client = DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, if (noSaveState) null else saveState(), devID); + client.setReloadData(getReloadData(true)); if (noSaveState) client.initialize() return client diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt index 476bad8a..d61ebc0b 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt @@ -59,9 +59,13 @@ import com.futo.platformplayer.states.AnnouncementType import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlugins +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.runBlocking import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.time.OffsetDateTime +import java.util.Random import kotlin.Exception import kotlin.reflect.full.findAnnotations import kotlin.reflect.jvm.kotlinFunction @@ -83,6 +87,8 @@ open class JSClient : IPlatformClient { private var _channelCapabilities: ResultCapabilities? = null; private var _peekChannelTypes: List? = null; + private var _usedReloadData: String? = null; + protected val _script: String; private var _initialized: Boolean = false; @@ -98,14 +104,14 @@ open class JSClient : IPlatformClient { override val icon: ImageVariable; override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities(); - private val _busyLock = Object(); - private var _busyCounter = 0; private var _busyAction = ""; - val isBusy: Boolean get() = _busyCounter > 0; + val isBusy: Boolean get() = _plugin.isBusy; val isBusyAction: String get() { return _busyAction; } + val declareOnEnable = HashMap(); + val settings: HashMap get() = descriptor.settings; val flags: Array; @@ -197,6 +203,7 @@ open class JSClient : IPlatformClient { open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient { val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials); + client.setReloadData(getReloadData(true)); if (noSaveState) client.initialize() return client @@ -213,14 +220,31 @@ open class JSClient : IPlatformClient { return plugin.httpClientOthers[id]; } + fun setReloadData(data: String?) { + if(data == null) { + if(declareOnEnable.containsKey("__reloadData")) + declareOnEnable.remove("__reloadData"); + } + else + declareOnEnable.put("__reloadData", data ?: ""); + } + fun getReloadData(orLast: Boolean): String? { + if(declareOnEnable.containsKey("__reloadData")) + return declareOnEnable["__reloadData"]; + else if(orLast) + return _usedReloadData; + return null; + } + override fun initialize() { if (_initialized) return - Logger.i(TAG, "Plugin [${config.name}] initializing"); plugin.start(); + plugin.execute("plugin.config = ${Json.encodeToString(config)}"); plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})"); + descriptor.appSettings.loadDefaults(descriptor.config); _initialized = true; @@ -260,19 +284,28 @@ open class JSClient : IPlatformClient { } @JSDocs(0, "source.enable()", "Called when the plugin is enabled/started") - fun enable() { + fun enable() = isBusyWith("enable") { if(!_initialized) initialize(); + for(toDeclare in declareOnEnable) { + plugin.execute("var ${toDeclare.key} = " + Json.encodeToString(toDeclare.value)); + } plugin.execute("source.enable(${Json.encodeToString(config)}, parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())}), ${Json.encodeToString(_injectedSaveState)})"); + + if(declareOnEnable.containsKey("__reloadData")) { + Logger.i(TAG, "Plugin [${config.name}] enabled with reload data: ${declareOnEnable["__reloadData"]}"); + _usedReloadData = declareOnEnable["__reloadData"]; + declareOnEnable.remove("__reloadData"); + } _enabled = true; } @JSDocs(1, "source.saveState()", "Provide a string that is passed to enable for quicker startup of multiple instances") - fun saveState(): String? { + fun saveState(): String? = isBusyWith("saveState") { ensureEnabled(); if(!capabilities.hasSaveState) - return null; + return@isBusyWith null; val resp = plugin.executeTyped("source.saveState()").value; - return resp; + return@isBusyWith resp; } @JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped") @@ -313,8 +346,10 @@ open class JSClient : IPlatformClient { return _searchCapabilities!!; } - _searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()")); - return _searchCapabilities!!; + return busy { + _searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()")); + return@busy _searchCapabilities!!; + } } catch(ex: Throwable) { announcePluginUnhandledException("getSearchCapabilities", ex); @@ -342,8 +377,10 @@ open class JSClient : IPlatformClient { if (_searchChannelContentsCapabilities != null) return _searchChannelContentsCapabilities!!; - _searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()")); - return _searchChannelContentsCapabilities!!; + return busy { + _searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()")); + return@busy _searchChannelContentsCapabilities!!; + } } @JSDocs(5, "source.searchChannelContents(query)", "Searches for videos on the platform") @JSDocsParameter("channelUrl", "Channel url to search") @@ -375,14 +412,14 @@ open class JSClient : IPlatformClient { @JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform") @JSDocsParameter("url", "A channel url (May not be your platform)") - override fun isChannelUrl(url: String): Boolean { + override fun isChannelUrl(url: String): Boolean = isBusyWith("isChannelUrl") { try { - return plugin.executeTyped("source.isChannelUrl(${Json.encodeToString(url)})") + return@isBusyWith plugin.executeTyped("source.isChannelUrl(${Json.encodeToString(url)})") .value; } catch(ex: Throwable) { announcePluginUnhandledException("isChannelUrl", ex); - return false; + return@isBusyWith false; } } @JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url") @@ -400,9 +437,10 @@ open class JSClient : IPlatformClient { if (_channelCapabilities != null) { return _channelCapabilities!!; } - - _channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()")); - return _channelCapabilities!!; + return busy { + _channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()")); + return@busy _channelCapabilities!!; + }; } catch(ex: Throwable) { announcePluginUnhandledException("getChannelCapabilities", ex); @@ -513,14 +551,14 @@ open class JSClient : IPlatformClient { @JSDocs(13, "source.isContentDetailsUrl(url)", "Validates if an content url is for this platform") @JSDocsParameter("url", "A content url (May not be your platform)") - override fun isContentDetailsUrl(url: String): Boolean { + override fun isContentDetailsUrl(url: String): Boolean = isBusyWith("isContentDetailsUrl") { try { - return plugin.executeTyped("source.isContentDetailsUrl(${Json.encodeToString(url)})") + return@isBusyWith plugin.executeTyped("source.isContentDetailsUrl(${Json.encodeToString(url)})") .value; } catch(ex: Throwable) { announcePluginUnhandledException("isContentDetailsUrl", ex); - return false; + return@isBusyWith false; } } @JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url") @@ -552,7 +590,7 @@ open class JSClient : IPlatformClient { Logger.i(TAG, "JSClient.getPlaybackTracker(${url})"); val tracker = plugin.executeTyped("source.getPlaybackTracker(${Json.encodeToString(url)})"); if(tracker is V8ValueObject) - return@isBusyWith JSPlaybackTracker(config, tracker); + return@isBusyWith JSPlaybackTracker(this, tracker); else return@isBusyWith null; } @@ -622,17 +660,19 @@ open class JSClient : IPlatformClient { @JSOptional @JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform") @JSDocsParameter("url", "Url of playlist") - override fun isPlaylistUrl(url: String): Boolean { + override fun isPlaylistUrl(url: String): Boolean = isBusyWith("isPlaylistUrl") { if (!capabilities.hasGetPlaylist) - return false; + return@isBusyWith false; try { - return plugin.executeTyped("source.isPlaylistUrl(${Json.encodeToString(url)})") - .value; + return@isBusyWith busy { + return@busy plugin.executeTyped("source.isPlaylistUrl(${Json.encodeToString(url)})") + .value; + } } catch(ex: Throwable) { announcePluginUnhandledException("isPlaylistUrl", ex); - return false; + return@isBusyWith false; } } @JSOptional @@ -734,19 +774,29 @@ open class JSClient : IPlatformClient { return urls; } - - private fun isBusyWith(actionName: String, handle: ()->T): T { - try { - synchronized(_busyLock) { - _busyCounter++; - } - _busyAction = actionName; - return handle(); + fun busy(handle: ()->T): T { + return _plugin.busy { + return@busy handle(); } - finally { - _busyAction = ""; - synchronized(_busyLock) { - _busyCounter--; + } + fun busyBlockingSuspended(handle: suspend ()->T): T { + return _plugin.busy { + return@busy runBlocking { + return@runBlocking handle(); + } + } + } + + fun isBusyWith(actionName: String, handle: ()->T): T { + //val busyId = kotlin.random.Random.nextInt(9999); + return busy { + try { + _busyAction = actionName; + return@busy handle(); + + } + finally { + _busyAction = ""; } } } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt index a637e89d..e318b5c2 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt @@ -4,6 +4,7 @@ import android.net.Uri import com.futo.platformplayer.SignatureProvider import com.futo.platformplayer.api.media.Serializer import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.matchesDomain import com.futo.platformplayer.states.StatePlugins import kotlinx.serialization.Contextual @@ -168,12 +169,17 @@ class SourcePluginConfig( } fun validate(text: String): Boolean { - if(scriptPublicKey.isNullOrEmpty()) - throw IllegalStateException("No public key present"); - if(scriptSignature.isNullOrEmpty()) - throw IllegalStateException("No signature present"); + try { + if (scriptPublicKey.isNullOrEmpty()) + throw IllegalStateException("No public key present"); + if (scriptSignature.isNullOrEmpty()) + throw IllegalStateException("No signature present"); - return SignatureProvider.verify(text, scriptSignature, scriptPublicKey); + return SignatureProvider.verify(text, scriptSignature, scriptPublicKey); + } catch (e: Throwable) { + Logger.e(TAG, "Failed to verify due to an unhandled exception", e) + return false + } } fun isUrlAllowed(url: String): Boolean { @@ -204,6 +210,8 @@ class SourcePluginConfig( obj.sourceUrl = sourceUrl; return obj; } + + private val TAG = "SourcePluginConfig" } @kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt index 6f835304..03c5c2c6 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/internal/JSHttpClient.kt @@ -67,6 +67,25 @@ class JSHttpClient : ManagedHttpClient { } + fun resetAuthCookies() { + _currentCookieMap.clear(); + if(!_auth?.cookieMap.isNullOrEmpty()) { + for(domainCookies in _auth!!.cookieMap!!) + _currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value)); + } + if(!_captcha?.cookieMap.isNullOrEmpty()) { + for(domainCookies in _captcha!!.cookieMap!!) { + if(_currentCookieMap.containsKey(domainCookies.key)) + _currentCookieMap[domainCookies.key]?.putAll(domainCookies.value); + else + _currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value)); + } + } + } + fun clearOtherCookies() { + _otherCookieMap.clear(); + } + override fun clone(): ManagedHttpClient { val newClient = JSHttpClient(_jsClient, _auth); newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) }) diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt index 777981bf..326b4086 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContent.kt @@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow @@ -13,6 +14,7 @@ interface IJSContent: IPlatformContent { companion object { fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent { + obj.ensureIsBusy(); val config = plugin.config; val type: Int = obj.getOrThrow(config, "contentType", "ContentItem"); val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt index 21b475ff..16470c17 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/IJSContentDetails.kt @@ -6,12 +6,14 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow interface IJSContentDetails: IPlatformContent { companion object { fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails { + obj.ensureIsBusy(); val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails"); return when(ContentType.fromInt(type)) { ContentType.MEDIA -> JSVideoDetails(plugin, obj); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt index 27731fea..dc2ba7b2 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSLiveEventPager.kt @@ -15,7 +15,7 @@ class JSLiveEventPager : JSPager, IPlatformLiveEventPager { nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager"); } - override fun nextPage() { + override fun nextPage() = plugin.isBusyWith("JSLiveEventPager.nextPage") { super.nextPage(); nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager"); } 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 8782b742..e81a288d 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 @@ -29,7 +29,9 @@ abstract class JSPager : IPager { this.pager = pager; this.config = config; - _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; + plugin.busy { + _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; + } getResults(); } @@ -44,11 +46,14 @@ abstract class JSPager : IPager { override fun nextPage() { warnIfMainThread("JSPager.nextPage"); - pager = plugin.getUnderlyingPlugin().catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") { - pager.invoke("nextPage", arrayOf()); - }; - _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; - _resultChanged = true; + val pluginV8 = plugin.getUnderlyingPlugin(); + pluginV8.busy { + pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") { + pager.invoke("nextPage", arrayOf()); + }; + _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; + _resultChanged = true; + } /* try { } @@ -70,15 +75,18 @@ abstract class JSPager : IPager { return previousResults; warnIfMainThread("JSPager.getResults"); - val items = pager.getOrThrow(config, "results", "JSPager"); - if(items.v8Runtime.isDead || items.v8Runtime.isClosed) - throw IllegalStateException("Runtime closed"); - val newResults = items.toArray() - .map { convertResult(it as V8ValueObject) } - .toList(); - _lastResults = newResults; - _resultChanged = false; - return newResults; + + return plugin.getUnderlyingPlugin().busy { + val items = pager.getOrThrow(config, "results", "JSPager"); + if (items.v8Runtime.isDead || items.v8Runtime.isClosed) + throw IllegalStateException("Runtime closed"); + val newResults = items.toArray() + .map { convertResult(it as V8ValueObject) } + .toList(); + _lastResults = newResults; + _resultChanged = false; + return@busy newResults; + } } abstract fun convertResult(obj: V8ValueObject): T; 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 e5ee7b68..15a7d854 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 @@ -2,37 +2,50 @@ package com.futo.platformplayer.api.media.platforms.js.models import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker +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.logging.Logger +import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.warnIfMainThread class JSPlaybackTracker: IPlaybackTracker { - private val _config: IV8PluginConfig; - private val _obj: V8ValueObject; + private lateinit var _client: JSClient; + private lateinit var _config: IV8PluginConfig; + private lateinit var _obj: V8ValueObject; private var _hasCalledInit: Boolean = false; - private val _hasInit: Boolean; + private var _hasInit: Boolean = false; private var _lastRequest: Long = Long.MIN_VALUE; - private val _hasOnConcluded: Boolean; + private var _hasOnConcluded: Boolean = false; override var nextRequest: Int = 1000 private set; - constructor(config: IV8PluginConfig, obj: V8ValueObject) { + constructor(client: JSClient, obj: V8ValueObject) { warnIfMainThread("JSPlaybackTracker.constructor"); - if(!obj.has("onProgress")) - throw ScriptImplementationException(config, "Missing onProgress on PlaybackTracker"); - if(!obj.has("nextRequest")) - throw ScriptImplementationException(config, "Missing nextRequest on PlaybackTracker"); - _hasOnConcluded = obj.has("onConcluded"); - this._config = config; - this._obj = obj; - this._hasInit = obj.has("onInit"); + client.busy { + if (!obj.has("onProgress")) + throw ScriptImplementationException( + client.config, + "Missing onProgress on PlaybackTracker" + ); + if (!obj.has("nextRequest")) + throw ScriptImplementationException( + client.config, + "Missing nextRequest on PlaybackTracker" + ); + _hasOnConcluded = obj.has("onConcluded"); + + this._client = client; + this._config = client.config; + this._obj = obj; + this._hasInit = obj.has("onInit"); + } } override fun onInit(seconds: Double) { @@ -40,12 +53,15 @@ class JSPlaybackTracker: IPlaybackTracker { synchronized(_obj) { if(_hasCalledInit) return; - if (_hasInit) { - Logger.i("JSPlaybackTracker", "onInit (${seconds})"); - _obj.invokeVoid("onInit", seconds); + + _client.busy { + if (_hasInit) { + Logger.i("JSPlaybackTracker", "onInit (${seconds})"); + _obj.invokeVoid("onInit", seconds); + } + nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false)); + _hasCalledInit = true; } - nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false)); - _hasCalledInit = true; } } @@ -55,10 +71,12 @@ class JSPlaybackTracker: IPlaybackTracker { if(!_hasCalledInit && _hasInit) onInit(seconds); else { - Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})"); - _obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying); - nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false)); - _lastRequest = System.currentTimeMillis(); + _client.busy { + Logger.i("JSPlaybackTracker", "onProgress (${seconds}, ${isPlaying})"); + _obj.invokeVoid("onProgress", Math.floor(seconds), isPlaying); + nextRequest = Math.max(100, _obj.getOrThrow(_config, "nextRequest", "PlaybackTracker", false)); + _lastRequest = System.currentTimeMillis(); + } } } } @@ -67,7 +85,9 @@ class JSPlaybackTracker: IPlaybackTracker { if(_hasOnConcluded) { synchronized(_obj) { Logger.i("JSPlaybackTracker", "onConcluded"); - _obj.invokeVoid("onConcluded", -1); + _client.busy { + _obj.invokeVoid("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 70dfecfd..36cfc7db 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 @@ -46,16 +46,18 @@ class JSRequestExecutor { if (_executor.isClosed) throw IllegalStateException("Executor object is closed"); - val result = if(_plugin is DevJSClient) - StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") { - V8Plugin.catchScriptErrors( - _config, - "[${_config.name}] JSRequestExecutor", - "builder.modifyRequest()" - ) { - _executor.invoke("executeRequest", url, headers, method, body); - } as V8Value; - } + return _plugin.getUnderlyingPlugin().busy { + + val result = if(_plugin is DevJSClient) + StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") { + V8Plugin.catchScriptErrors( + _config, + "[${_config.name}] JSRequestExecutor", + "builder.modifyRequest()" + ) { + _executor.invoke("executeRequest", url, headers, method, body); + } as V8Value; + } else V8Plugin.catchScriptErrors( _config, "[${_config.name}] JSRequestExecutor", @@ -64,34 +66,35 @@ class JSRequestExecutor { _executor.invoke("executeRequest", url, headers, method, body); } as V8Value; - try { - if(result is V8ValueString) { - val base64Result = Base64.getDecoder().decode(result.value); - return base64Result; - } - if(result is V8ValueTypedArray) { - val buffer = result.buffer; - val byteBuffer = buffer.byteBuffer; - val bytesResult = ByteArray(result.byteLength); - byteBuffer.get(bytesResult, 0, result.byteLength); - buffer.close(); - return bytesResult; - } - if(result is V8ValueObject && result.has("type")) { - val type = result.getOrThrow(_config, "type", "JSRequestModifier"); - when(type) { - //TODO: Buffer type? + try { + if(result is V8ValueString) { + val base64Result = Base64.getDecoder().decode(result.value); + return@busy base64Result; } + if(result is V8ValueTypedArray) { + val buffer = result.buffer; + val byteBuffer = buffer.byteBuffer; + val bytesResult = ByteArray(result.byteLength); + byteBuffer.get(bytesResult, 0, result.byteLength); + buffer.close(); + return@busy bytesResult; + } + if(result is V8ValueObject && result.has("type")) { + val type = result.getOrThrow(_config, "type", "JSRequestModifier"); + when(type) { + //TODO: Buffer type? + } + } + if(result is V8ValueUndefined) { + if(_plugin is DevJSClient) + StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined"); + throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null); + } + throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name); } - if(result is V8ValueUndefined) { - if(_plugin is DevJSClient) - StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined"); - throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null); + finally { + result.close(); } - throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name); - } - finally { - result.close(); } } @@ -99,24 +102,25 @@ class JSRequestExecutor { open fun cleanup() { if (!hasCleanup || _executor.isClosed) return; - - if(_plugin is DevJSClient) - StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") { - V8Plugin.catchScriptErrors( - _config, - "[${_config.name}] JSRequestExecutor", - "builder.modifyRequest()" - ) { - _executor.invokeVoid("cleanup", null); - }; - } - else V8Plugin.catchScriptErrors( - _config, - "[${_config.name}] JSRequestExecutor", - "builder.modifyRequest()" - ) { - _executor.invokeVoid("cleanup", null); - }; + _plugin.busy { + if(_plugin is DevJSClient) + StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") { + V8Plugin.catchScriptErrors( + _config, + "[${_config.name}] JSRequestExecutor", + "builder.modifyRequest()" + ) { + _executor.invokeVoid("cleanup", null); + }; + } + else V8Plugin.catchScriptErrors( + _config, + "[${_config.name}] JSRequestExecutor", + "builder.modifyRequest()" + ) { + _executor.invokeVoid("cleanup", null); + }; + } } protected fun finalize() { 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 150189e7..f7d169af 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 @@ -16,7 +16,7 @@ class JSRequestModifier: IRequestModifier { private val _plugin: JSClient; private val _config: IV8PluginConfig; private var _modifier: V8ValueObject; - override var allowByteSkip: Boolean; + override var allowByteSkip: Boolean = false; constructor(plugin: JSClient, modifier: V8ValueObject) { this._plugin = plugin; @@ -24,10 +24,13 @@ class JSRequestModifier: IRequestModifier { this._config = plugin.config; val config = plugin.config; - allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true; + plugin.busy { + allowByteSkip = modifier.getOrNull(config, "allowByteSkip", "JSRequestModifier") ?: true; + + if(!modifier.has("modifyRequest")) + throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null); + } - if(!modifier.has("modifyRequest")) - throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null); } override fun modifyRequest(url: String, headers: Map): IRequest { @@ -35,13 +38,15 @@ class JSRequestModifier: IRequestModifier { return Request(url, headers); } - val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") { - _modifier.invoke("modifyRequest", url, headers); - } as V8ValueObject; + return _plugin.busy { + val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") { + _modifier.invoke("modifyRequest", url, headers); + } as V8ValueObject; - val req = JSRequest(_plugin, result, url, headers); - result.close(); - return req; + val req = JSRequest(_plugin, result, url, headers); + result.close(); + return@busy req; + } } 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 bb4650f6..259a89e4 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 @@ -6,6 +6,7 @@ import com.caoccao.javet.values.reference.V8ValueObject 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.states.StateApp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -35,8 +36,11 @@ class JSSubtitleSource : ISubtitleSource { override fun getSubtitles(): String { if(!hasFetch) throw IllegalStateException("This subtitle doesn't support getSubtitles.."); - val v8String = _obj.invoke("getSubtitles", arrayOf()); - return v8String.value; + + return _obj.getSourcePlugin()?.busy { + val v8String = _obj.invoke("getSubtitles", arrayOf()); + return@busy v8String.value; + } ?: ""; } override suspend fun getSubtitlesURI(): Uri? { 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 da495498..cecb2913 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 @@ -27,6 +27,7 @@ import com.futo.platformplayer.getOrThrowNullable import com.futo.platformplayer.states.StateDeveloper class JSVideoDetails : JSVideo, IPlatformVideoDetails { + private val _plugin: JSClient; private val _hasGetComments: Boolean; private val _hasGetContentRecommendations: Boolean; private val _hasGetPlaybackTracker: Boolean; @@ -48,6 +49,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) { val contextName = "VideoDetails"; + _plugin = plugin; val config = plugin.config; description = _content.getOrThrow(config, "description", contextName); video = JSVideoSourceDescriptor.fromV8(plugin, _content.getOrThrow(config, "video", contextName)); @@ -82,14 +84,16 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { return getPlaybackTrackerJS(); } private fun getPlaybackTrackerJS(): IPlaybackTracker? { - return V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") { - val tracker = _content.invoke("getPlaybackTracker", arrayOf()) - ?: return@catchScriptErrors null; - if(tracker is V8ValueObject) - return@catchScriptErrors JSPlaybackTracker(_pluginConfig, tracker); - else - return@catchScriptErrors null; - }; + return _plugin.busy { + V8Plugin.catchScriptErrors(_pluginConfig, "VideoDetails", "videoDetails.getPlaybackTracker()") { + val tracker = _content.invoke("getPlaybackTracker", arrayOf()) + ?: return@catchScriptErrors null; + if(tracker is V8ValueObject) + return@catchScriptErrors JSPlaybackTracker(_plugin, tracker); + else + return@catchScriptErrors null; + } + } } override fun getContentRecommendations(client: IPlatformClient): IPager? { @@ -106,8 +110,10 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { return null; } private fun getContentRecommendationsJS(client: JSClient): JSContentPager { - val contentPager = _content.invoke("getContentRecommendations", arrayOf()); - return JSContentPager(_pluginConfig, client, contentPager); + return _plugin.busy { + val contentPager = _content.invoke("getContentRecommendations", arrayOf()); + return@busy JSContentPager(_pluginConfig, client, contentPager); + } } override fun getComments(client: IPlatformClient): IPager? { @@ -123,10 +129,12 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { } private fun getCommentsJS(client: JSClient): IPager? { - val commentPager = _content.invoke("getComments", arrayOf()); - if (commentPager !is V8ValueObject) //TODO: Maybe handle this better? - return null; + return _plugin.busy { + val commentPager = _content.invoke("getComments", arrayOf()); + if (commentPager !is V8ValueObject) //TODO: Maybe handle this better? + return@busy null; - return JSCommentPager(_pluginConfig, client, commentPager); + return@busy JSCommentPager(_pluginConfig, client, commentPager); + } } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawAudioSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawAudioSource.kt index ae35207b..f4994e0b 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawAudioSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawAudioSource.kt @@ -62,21 +62,27 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS if(_plugin is DevJSClient) result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { - _obj.invokeString("generate"); + _plugin.isBusyWith("dashAudio.generate") { + _obj.invokeString("generate"); + } } } else result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { - _obj.invokeString("generate"); + _plugin.isBusyWith("dashAudio.generate") { + _obj.invokeString("generate"); + } } if(result != null){ - val initStart = _obj.getOrDefault(_config, "initStart", "JSDashManifestRawSource", null) ?: 0; - val initEnd = _obj.getOrDefault(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0; - val indexStart = _obj.getOrDefault(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0; - val indexEnd = _obj.getOrDefault(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0; - if(initEnd > 0 && indexStart > 0 && indexEnd > 0) { - streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd); + plugin.busy { + val initStart = _obj.getOrDefault(_config, "initStart", "JSDashManifestRawSource", null) ?: 0; + val initEnd = _obj.getOrDefault(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0; + val indexStart = _obj.getOrDefault(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0; + val indexEnd = _obj.getOrDefault(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0; + if(initEnd > 0 && indexStart > 0 && indexEnd > 0) { + streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd); + } } } return result; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt index d6ff7455..184b783d 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt @@ -32,7 +32,7 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo override val duration: Long; override val priority: Boolean; - var url: String?; + val url: String?; override var manifest: String?; override val hasGenerate: Boolean; @@ -67,22 +67,28 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo if(_plugin is DevJSClient) { result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { - _obj.invokeString("generate"); + _plugin.isBusyWith("dashVideo.generate") { + _obj.invokeString("generate"); + } }); } } else result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { - _obj.invokeString("generate"); + _plugin.isBusyWith("dashVideo.generate") { + _obj.invokeString("generate"); + } }); if(result != null){ - val initStart = _obj.getOrDefault(_config, "initStart", "JSDashManifestRawSource", null) ?: 0; - val initEnd = _obj.getOrDefault(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0; - val indexStart = _obj.getOrDefault(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0; - val indexEnd = _obj.getOrDefault(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0; - if(initEnd > 0 && indexStart > 0 && indexEnd > 0) { - streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd); + _plugin.busy { + val initStart = _obj.getOrDefault(_config, "initStart", "JSDashManifestRawSource", null) ?: 0; + val initEnd = _obj.getOrDefault(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0; + val indexStart = _obj.getOrDefault(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0; + val indexEnd = _obj.getOrDefault(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0; + if(initEnd > 0 && indexStart > 0 && indexEnd > 0) { + streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd); + } } } return result; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt index 9e328df3..18cd71fc 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSHLSManifestAudioSource.kt @@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudi import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.V8Plugin +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.orNull @@ -38,7 +39,13 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource { companion object { - fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) }; - fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource = JSHLSManifestAudioSource(plugin, obj); + fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? { + obj?.ensureIsBusy(); + return obj.orNull { fromV8HLS(plugin, it as V8ValueObject) } + }; + fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestAudioSource { + obj.ensureIsBusy(); + return JSHLSManifestAudioSource(plugin, obj) + }; } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt index 3c76e23d..22bf2a60 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 @@ -14,6 +14,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier 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.logging.Logger import com.futo.platformplayer.orNull @@ -53,36 +54,39 @@ abstract class JSSource { hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor"); } - fun getRequestModifier(): IRequestModifier? { + fun getRequestModifier(): IRequestModifier? = _plugin.isBusyWith("getRequestModifier") { if(_requestModifier != null) - return AdhocRequestModifier { url, headers -> + return@isBusyWith AdhocRequestModifier { url, headers -> return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers); }; if (!hasRequestModifier || _obj.isClosed) - return null; + return@isBusyWith null; val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") { _obj.invoke("getRequestModifier", arrayOf()); }; if (result !is V8ValueObject) - return null; + return@isBusyWith null; - return JSRequestModifier(_plugin, result) + return@isBusyWith JSRequestModifier(_plugin, result) } - open fun getRequestExecutor(): JSRequestExecutor? { + open fun getRequestExecutor(): JSRequestExecutor? = _plugin.isBusyWith("getRequestExecutor") { if (!hasRequestExecutor || _obj.isClosed) - return null; + return@isBusyWith null; + Logger.v("JSSource", "Request executor for [${type}] requesting"); val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") { _obj.invoke("getRequestExecutor", arrayOf()); }; - if (result !is V8ValueObject) - return null; + Logger.v("JSSource", "Request executor for [${type}] received"); - return JSRequestExecutor(_plugin, result) + if (result !is V8ValueObject) + return@isBusyWith null; + + return@isBusyWith JSRequestExecutor(_plugin, result) } fun getUnderlyingPlugin(): JSClient? { @@ -105,8 +109,12 @@ abstract class JSSource { const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource" const val TYPE_VIDEOURL_WIDEVINE = "VideoUrlWidevineSource" - fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) }; + fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? { + obj?.ensureIsBusy(); + return obj.orNull { fromV8Video(plugin, it as V8ValueObject) } + }; fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? { + obj.ensureIsBusy() val type = obj.getString("plugin_type"); return when(type) { TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj); @@ -123,13 +131,26 @@ abstract class JSSource { } } fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) }; - fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj); - fun fromV8DashRaw(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawSource = JSDashManifestRawSource(plugin, obj); - fun fromV8DashRawAudio(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawAudioSource = JSDashManifestRawAudioSource(plugin, obj); + fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource{ + obj.ensureIsBusy(); + return JSDashManifestSource(plugin, obj) + }; + fun fromV8DashRaw(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawSource{ + obj.ensureIsBusy() + return JSDashManifestRawSource(plugin, obj); + } + fun fromV8DashRawAudio(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawAudioSource { + obj?.ensureIsBusy(); + return JSDashManifestRawAudioSource(plugin, obj) + }; fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) }; - fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj); + fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource { + obj.ensureIsBusy(); + return JSHLSManifestSource(plugin, obj) + }; fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource? { + obj.ensureIsBusy(); val type = obj.getString("plugin_type"); return when(type) { TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt index e68f0ae0..e7c0fe50 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt @@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.platforms.js.JSClient +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor { @@ -31,6 +32,7 @@ class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor { fun fromV8(plugin: JSClient, obj: V8ValueObject) : IVideoSourceDescriptor { + obj.ensureIsBusy(); val type = obj.getString("plugin_type") return when(type) { TYPE_MUXED -> JSVideoSourceDescriptor(plugin, obj); diff --git a/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt index 3d362efd..226a0a66 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt @@ -35,7 +35,7 @@ class ChromecastCastingDevice : CastingDevice { override var usedRemoteAddress: InetAddress? = null; override var localAddress: InetAddress? = null; override val canSetVolume: Boolean get() = true; - override val canSetSpeed: Boolean get() = false; //TODO: Implement + override val canSetSpeed: Boolean get() = true; var addresses: Array? = null; var port: Int = 0; @@ -144,6 +144,23 @@ class ChromecastCastingDevice : CastingDevice { sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.media", json); } + override fun changeSpeed(speed: Double) { + if (invokeInIOScopeIfRequired { changeSpeed(speed) }) return + + val speedClamped = speed.coerceAtLeast(1.0).coerceAtLeast(1.0).coerceAtMost(2.0) + setSpeed(speedClamped) + val mediaSessionId = _mediaSessionId ?: return + val transportId = _transportId ?: return + val setSpeedObject = JSONObject().apply { + put("type", "SET_PLAYBACK_RATE") + put("mediaSessionId", mediaSessionId) + put("playbackRate", speedClamped) + put("requestId", _requestId++) + } + + sendChannelMessage(sourceId = "sender-0", destinationId = transportId, namespace = "urn:x-cast:com.google.cast.media", json = setSpeedObject.toString()) + } + override fun changeVolume(volume: Double) { if (invokeInIOScopeIfRequired({ changeVolume(volume) })) { return; diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index c0d9d0bf..5e64c3e3 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -724,7 +724,7 @@ class VideoDownload { val t = cue.groupValues[1]; val d = cue.groupValues[2]; - val url = foundTemplateUrl.replace("\$Number\$", (indexCounter + 1).toString()); + val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString()); val data = if(executor != null) executor.executeRequest("GET", url, null, mapOf()); diff --git a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt index 15412fd9..0cb2f196 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -6,8 +6,6 @@ import com.caoccao.javet.exceptions.JavetException import com.caoccao.javet.exceptions.JavetExecutionException import com.caoccao.javet.interop.V8Host import com.caoccao.javet.interop.V8Runtime -import com.caoccao.javet.interop.options.V8Flags -import com.caoccao.javet.interop.options.V8RuntimeOptions import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.primitive.V8ValueBoolean import com.caoccao.javet.values.primitive.V8ValueInteger @@ -26,6 +24,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptException import com.futo.platformplayer.engine.exceptions.ScriptExecutionException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException +import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException import com.futo.platformplayer.engine.exceptions.ScriptTimeoutException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.engine.internal.V8Converter @@ -40,6 +39,8 @@ import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateAssets import com.futo.platformplayer.warnIfMainThread import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock class V8Plugin { val config: IV8PluginConfig; @@ -47,10 +48,13 @@ class V8Plugin { private val _clientAuth: ManagedHttpClient; private val _clientOthers: ConcurrentHashMap = ConcurrentHashMap(); + val httpClient: ManagedHttpClient get() = _client; val httpClientAuth: ManagedHttpClient get() = _clientAuth; val httpClientOthers: Map get() = _clientOthers; + var runtimeId: Int = 0; + fun registerHttpClient(client: JSHttpClient) { synchronized(_clientOthers) { _clientOthers.put(client.clientId, client); @@ -67,10 +71,8 @@ class V8Plugin { var isStopped = true; val onStopped = Event1(); - //TODO: Implement a more universal isBusy system for plugins + JSClient + pooling? TBD if propagation would be beneficial - private val _busyCounterLock = Object(); - private var _busyCounter = 0; - val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 }; + private val _busyLock = ReentrantLock() + val isBusy get() = _busyLock.isLocked; var allowDevSubmit: Boolean = false private set(value) { @@ -140,6 +142,7 @@ class V8Plugin { synchronized(_runtimeLock) { if (_runtime != null) return; + runtimeId = runtimeId + 1; //V8RuntimeOptions.V8_FLAGS.setUseStrict(true); val host = V8Host.getV8Instance(); val options = host.jsRuntimeType.getRuntimeOptions(); @@ -148,6 +151,8 @@ class V8Plugin { if (!host.isIsolateCreated) throw IllegalStateException("Isolate not created"); + _runtimeMap.put(_runtime!!, this); + //Setup bridge _runtime?.let { it.converter = V8Converter(); @@ -184,10 +189,13 @@ class V8Plugin { } fun stop(){ Logger.i(TAG, "Stopping plugin [${config.name}]"); - isStopped = true; - whenNotBusy { + busy { + Logger.i(TAG, "Plugin stopping"); synchronized(_runtimeLock) { + if(isStopped) + return@busy; isStopped = true; + runtimeId = runtimeId + 1; //Cleanup http for(pack in _depsPackages) { @@ -197,6 +205,7 @@ class V8Plugin { } _runtime?.let { + _runtimeMap.remove(it); _runtime = null; if(!it.isClosed && !it.isDead) { try { @@ -211,10 +220,20 @@ class V8Plugin { Logger.i(TAG, "Stopped plugin [${config.name}]"); }; } + Logger.i(TAG, "Plugin stopped"); onStopped.emit(this); } } + fun isThreadAlreadyBusy(): Boolean { + return _busyLock.isHeldByCurrentThread; + } + fun busy(handle: ()->T): T { + _busyLock.withLock { + //Logger.i(TAG, "Entered busy: " + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()); + return handle(); + } + } fun execute(js: String) : V8Value { return executeTyped(js); } @@ -223,49 +242,17 @@ class V8Plugin { if(isStopped) throw PluginEngineStoppedException(config, "Instance is stopped", js); - synchronized(_busyCounterLock) { - _busyCounter++; - } + return busy { - val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet"); - try { - return catchScriptErrors("Plugin[${config.name}]", js) { + val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet"); + return@busy catchScriptErrors("Plugin[${config.name}]", js) { runtime.getExecutor(js).execute() }; } - finally { - synchronized(_busyCounterLock) { - //Free busy *after* afterBusy calls are done to prevent calls on dead runtimes - try { - afterBusy.emit(_busyCounter - 1); - } - catch(ex: Throwable) { - Logger.e(TAG, "Unhandled V8Plugin.afterBusy", ex); - } - _busyCounter--; - } - } - } - fun executeBoolean(js: String) : Boolean? = catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value }; - fun executeString(js: String) : String? = catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value }; - fun executeInteger(js: String) : Int? = catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value }; - - fun whenNotBusy(handler: (V8Plugin)->Unit) { - synchronized(_busyCounterLock) { - if(_busyCounter == 0) - handler(this); - else { - val tag = Object(); - afterBusy.subscribe(tag) { - if(it == 0) { - Logger.w(TAG, "V8Plugin afterBusy handled"); - afterBusy.remove(tag); - handler(this); - } - } - } - } } + fun executeBoolean(js: String) : Boolean? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value } } + fun executeString(js: String) : String? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value } } + fun executeInteger(js: String) : Int? = busy { catchScriptErrors("Plugin[${config.name}]") { executeTyped(js).value } } private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? { //TODO: Auto get all package types? @@ -292,8 +279,14 @@ class V8Plugin { private val REGEX_EX_FALLBACK = Regex(".*throw.*?[\"](.*)[\"].*"); private val REGEX_EX_FALLBACK2 = Regex(".*throw.*?['](.*)['].*"); + private val _runtimeMap = ConcurrentHashMap(); + val TAG = "V8Plugin"; + fun getPluginFromRuntime(runtime: V8Runtime): V8Plugin? { + return _runtimeMap.getOrDefault(runtime, null); + } + fun catchScriptErrors(config: IV8PluginConfig, context: String, code: String? = null, handle: ()->T): T { var codeStripped = code; if(codeStripped != null) { //TODO: Improve code stripped @@ -327,14 +320,23 @@ class V8Plugin { throw ScriptCompilationException(config, "Compilation: [${context}]: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped); } catch(executeEx: JavetExecutionException) { - if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true) { - val pluginType = executeEx.scriptingError.context["plugin_type"].toString(); + val obj = executeEx.scriptingError?.context + if(obj != null && obj.containsKey("plugin_type") == true) { + val pluginType = obj["plugin_type"].toString(); //Captcha if (pluginType == "CaptchaRequiredException") { throw ScriptCaptchaRequiredException(config, - executeEx.scriptingError.context["url"]?.toString(), - executeEx.scriptingError.context["body"]?.toString(), + obj["url"]?.toString(), + obj["body"]?.toString(), + executeEx, executeEx.scriptingError?.stack, codeStripped); + } + + //Reload Required + if (pluginType == "ReloadRequiredException") { + throw ScriptReloadRequiredException(config, + obj["msg"]?.toString(), + obj["reloadData"]?.toString(), executeEx, executeEx.scriptingError?.stack, codeStripped); } @@ -348,6 +350,41 @@ class V8Plugin { codeStripped ); } + /* //Required for newer V8 versions + if(executeEx.scriptingError?.context is IJavetEntityError) { + val obj = executeEx.scriptingError?.context as IJavetEntityError + if(obj.context.containsKey("plugin_type") == true) { + val pluginType = obj.context["plugin_type"].toString(); + + //Captcha + if (pluginType == "CaptchaRequiredException") { + throw ScriptCaptchaRequiredException(config, + obj.context["url"]?.toString(), + obj.context["body"]?.toString(), + executeEx, executeEx.scriptingError?.stack, codeStripped); + } + + //Reload Required + if (pluginType == "ReloadRequiredException") { + throw ScriptReloadRequiredException(config, + obj.context["msg"]?.toString(), + obj.context["reloadData"]?.toString(), + executeEx, executeEx.scriptingError?.stack, codeStripped); + } + + //Others + throwExceptionFromV8( + config, + pluginType, + (extractJSExceptionMessage(executeEx) ?: ""), + executeEx, + executeEx.scriptingError?.stack, + codeStripped + ); + } + + } + */ throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped); } catch(ex: Exception) { @@ -398,9 +435,4 @@ class V8Plugin { return StateAssets.readAsset(context, path) ?: throw java.lang.IllegalStateException("script ${path} not found"); } } - - - /** - * Methods available for scripts (bridge object) - */ } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/NoInternetException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/NoInternetException.kt index bce39025..4011b0a8 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/NoInternetException.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/NoInternetException.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow open class NoInternetException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) { @@ -11,6 +12,7 @@ open class NoInternetException(config: IV8PluginConfig, error: String, ex: Excep companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : NoInternetException { + obj.ensureIsBusy(); return NoInternetException(config, obj.getOrThrow(config, "message", "NoInternetException")); } } diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptAgeException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptAgeException.kt index ef1ca13f..48c3142f 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptAgeException.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptAgeException.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow open class ScriptAgeException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) { @@ -11,6 +12,7 @@ open class ScriptAgeException(config: IV8PluginConfig, error: String, ex: Except companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { + obj.ensureIsBusy(); return ScriptException(config, obj.getOrThrow(config, "message", "ScriptAgeException")); } } diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCaptchaRequiredException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCaptchaRequiredException.kt index 8aa7f2c8..6bbf536b 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCaptchaRequiredException.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCaptchaRequiredException.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrThrow @@ -9,6 +10,7 @@ class ScriptCaptchaRequiredException(config: IV8PluginConfig, val url: String?, companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { + obj.ensureIsBusy(); val contextName = "ScriptCaptchaRequiredException"; return ScriptCaptchaRequiredException(config, obj.getOrDefault(config, "url", contextName, null), diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCompilationException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCompilationException.kt index 2db245d3..26b2eebc 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCompilationException.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCompilationException.kt @@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow class ScriptCompilationException(config: IV8PluginConfig, error: String, ex: Exception? = null, code: String? = null) : PluginException(config, error, ex, code) { companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptCompilationException { + obj.ensureIsBusy(); return ScriptCompilationException(config, obj.getOrThrow(config, "message", "ScriptCompilationException")); } } diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCriticalException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCriticalException.kt index 6581ec25..d8eda509 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCriticalException.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptCriticalException.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow open class ScriptCriticalException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) { @@ -11,6 +12,7 @@ open class ScriptCriticalException(config: IV8PluginConfig, error: String, ex: E companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { + obj.ensureIsBusy(); return ScriptCriticalException(config, obj.getOrThrow(config, "message", "ScriptCriticalException")); } } diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptException.kt index cf038a23..de777a9f 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptException.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptException.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow open class ScriptException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptExecutionException(config, error, ex, stack, code) { @@ -11,6 +12,7 @@ open class ScriptException(config: IV8PluginConfig, error: String, ex: Exception companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { + obj.ensureIsBusy(); return ScriptException(config, obj.getOrThrow(config, "message", "ScriptException")); } } diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptExecutionException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptExecutionException.kt index 28b9b0e9..8bfd49d6 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptExecutionException.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptExecutionException.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.engine.exceptions import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow open class ScriptExecutionException(config: IV8PluginConfig, error: String, ex: Exception? = null, val stack: String? = null, code: String? = null) : PluginException(config, error, ex, code) { @@ -11,6 +12,7 @@ open class ScriptExecutionException(config: IV8PluginConfig, error: String, ex: companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptExecutionException { + obj.ensureIsBusy(); return ScriptExecutionException(config, obj.getOrThrow(config, "message", "ScriptExecutionException")); } } diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptImplementationException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptImplementationException.kt index dd2aaf7a..943b4fe9 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptImplementationException.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptImplementationException.kt @@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow class ScriptImplementationException(config: IV8PluginConfig, error: String, ex: Exception? = null, var pluginId: String? = null, code: String? = null) : PluginException(config, error, ex, code) { companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptImplementationException { + obj.ensureIsBusy(); return ScriptImplementationException(config, obj.getOrThrow(config, "message", "ScriptImplementationException")); } } diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptLoginRequiredException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptLoginRequiredException.kt index 423d5786..4acf0c55 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptLoginRequiredException.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptLoginRequiredException.kt @@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow class ScriptLoginRequiredException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) { companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { + obj.ensureIsBusy(); return ScriptLoginRequiredException(config, obj.getOrThrow(config, "message", "ScriptLoginRequiredException")); } } diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptReloadRequiredException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptReloadRequiredException.kt new file mode 100644 index 00000000..6c792a32 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptReloadRequiredException.kt @@ -0,0 +1,22 @@ +package com.futo.platformplayer.engine.exceptions + +import com.caoccao.javet.values.reference.V8ValueObject +import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig +import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.engine.V8PluginConfig +import com.futo.platformplayer.ensureIsBusy +import com.futo.platformplayer.getOrDefault +import com.futo.platformplayer.getOrThrow + +class ScriptReloadRequiredException(config: IV8PluginConfig, val msg: String?, val reloadData: String?, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, msg ?: "ReloadRequired", ex, stack, code) { + + companion object { + fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { + obj.ensureIsBusy(); + val contextName = "ScriptReloadRequiredException"; + return ScriptReloadRequiredException(config, + obj.getOrThrow(config, "message", contextName), + obj.getOrDefault(config, "reloadData", contextName, null)); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptTimeoutException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptTimeoutException.kt index 6f883854..17d02073 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptTimeoutException.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptTimeoutException.kt @@ -2,11 +2,13 @@ package com.futo.platformplayer.engine.exceptions import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow class ScriptTimeoutException(config: IV8PluginConfig, error: String, ex: Exception? = null) : ScriptException(config, error, ex) { companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptTimeoutException { + obj.ensureIsBusy(); return ScriptTimeoutException(config, obj.getOrThrow(config, "message", "ScriptException")); } } diff --git a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptUnavailableException.kt b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptUnavailableException.kt index 5d331b8b..feb47c35 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptUnavailableException.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptUnavailableException.kt @@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.exceptions import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow class ScriptUnavailableException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) { companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { + obj.ensureIsBusy(); return ScriptUnavailableException(config, obj.getOrThrow(config, "message", "ScriptUnavailableException")); } } diff --git a/app/src/main/java/com/futo/platformplayer/engine/internal/V8BindObject.kt b/app/src/main/java/com/futo/platformplayer/engine/internal/V8BindObject.kt index 4e861b72..fd30af6f 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/internal/V8BindObject.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/internal/V8BindObject.kt @@ -13,8 +13,8 @@ open class V8BindObject : IV8Convertable { override fun toV8(runtime: V8Runtime): V8Value? { synchronized(this) { - if(_runtimeObj != null) - return _runtimeObj; + //if(_runtimeObj != null) + // return _runtimeObj; val v8Obj = runtime.createV8ValueObject(); v8Obj.bind(this); diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt index d2d7cf04..72bdf34f 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt @@ -4,6 +4,7 @@ import android.media.MediaCodec import android.media.MediaCodecList import com.caoccao.javet.annotations.V8Function import com.caoccao.javet.annotations.V8Property +import com.caoccao.javet.interop.callback.JavetCallbackContext import com.caoccao.javet.utils.JavetResourceUtils import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.reference.V8ValueFunction @@ -26,6 +27,7 @@ import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import java.util.concurrent.ConcurrentHashMap class PackageBridge : V8Package { @Transient @@ -78,6 +80,14 @@ class PackageBridge : V8Package { return "android"; } + @V8Property + fun supportedFeatures(): Array { + return arrayOf( + "ReloadRequiredException", + "HttpBatchClient" + ); + } + @V8Property fun supportedContent(): Array { return arrayOf( @@ -101,45 +111,51 @@ class PackageBridge : V8Package { } var timeoutCounter = 0; - var timeoutMap = HashSet(); + var timeoutMap = ConcurrentHashMap(); @V8Function fun setTimeout(func: V8ValueFunction, timeout: Long): Int { val id = timeoutCounter++; - val funcClone = func.toClone() StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { delay(timeout); - synchronized(timeoutMap) { - if(!timeoutMap.contains(id)) { - JavetResourceUtils.safeClose(funcClone); - return@launch; + if (_plugin.isStopped) + return@launch; + if (!timeoutMap.containsKey(id)) { + _plugin.busy { + if (!_plugin.isStopped) + JavetResourceUtils.safeClose(funcClone); } - timeoutMap.remove(id); + return@launch; } + timeoutMap.remove(id); try { - _plugin.whenNotBusy { - funcClone.callVoid(null, arrayOf()); + _plugin.busy { + if (!_plugin.isStopped) + funcClone.callVoid(null, arrayOf()); } - } - catch(ex: Throwable) { + } catch (ex: Throwable) { Logger.e(TAG, "Failed timeout callback", ex); - } - finally { - JavetResourceUtils.safeClose(funcClone); + } finally { + _plugin.busy { + if (!_plugin.isStopped) + JavetResourceUtils.safeClose(funcClone); + } + //_plugin.whenNotBusy { + //} } }; - synchronized(timeoutMap) { - timeoutMap.add(id); - } + timeoutMap.put(id, true); return id; } @V8Function fun clearTimeout(id: Int) { - synchronized(timeoutMap) { - if(timeoutMap.contains(id)) - timeoutMap.remove(id); - } + if (timeoutMap.containsKey(id)) + timeoutMap.remove(id); + } + @V8Function + fun sleep(length: Int) { + Thread.sleep(length.toLong()); } @V8Function @@ -147,7 +163,7 @@ class PackageBridge : V8Package { Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}"); StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { try { - UIDialogs.toast(str); + UIDialogs.appToast(str); } catch (e: Throwable) { Logger.e(TAG, "Failed to show toast.", e); } 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 900eb6f0..82edb023 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 @@ -44,6 +44,17 @@ class PackageHttp: V8Package { private val aliveSockets = mutableListOf(); private var _cleanedUp = false; + private val _clients = mutableMapOf() + + fun getClient(id: String?): PackageHttpClient { + if(id == null) + throw IllegalArgumentException("Http client ${id} doesn't exist"); + if(_packageClient.clientId() == id) + return _packageClient; + if(_packageClientAuth.clientId() == id) + return _packageClientAuth; + return _clients.getOrDefault(id, null) ?: throw IllegalArgumentException("Http client ${id} doesn't exist"); + } constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) { _config = config; @@ -112,6 +123,8 @@ class PackageHttp: V8Package { _plugin.registerHttpClient(httpClient); val client = PackageHttpClient(this, httpClient); + _clients.put(client.clientId() ?: "", client); + return client; } @V8Function @@ -246,18 +259,18 @@ class PackageHttp: V8Package { @V8Function fun request(method: String, url: String, headers: MutableMap = HashMap(), useAuth: Boolean = false) : BatchBuilder { - return clientRequest(_package.getDefaultClient(useAuth), method, url, headers); + return clientRequest(_package.getDefaultClient(useAuth).clientId(), method, url, headers); } @V8Function fun requestWithBody(method: String, url: String, body:String, headers: MutableMap = HashMap(), useAuth: Boolean = false) : BatchBuilder { - return clientRequestWithBody(_package.getDefaultClient(useAuth), method, url, body, headers); + return clientRequestWithBody(_package.getDefaultClient(useAuth).clientId(), method, url, body, headers); } @V8Function fun GET(url: String, headers: MutableMap = HashMap(), useAuth: Boolean = false) : BatchBuilder - = clientGET(_package.getDefaultClient(useAuth), url, headers); + = clientGET(_package.getDefaultClient(useAuth).clientId(), url, headers); @V8Function fun POST(url: String, body: String, headers: MutableMap = HashMap(), useAuth: Boolean = false) : BatchBuilder - = clientPOST(_package.getDefaultClient(useAuth), url, body, headers); + = clientPOST(_package.getDefaultClient(useAuth).clientId(), url, body, headers); @V8Function fun DUMMY(): BatchBuilder { @@ -268,21 +281,21 @@ class PackageHttp: V8Package { //Client-specific @V8Function - fun clientRequest(client: PackageHttpClient, method: String, url: String, headers: MutableMap = HashMap()) : BatchBuilder { - _reqs.add(Pair(client, RequestDescriptor(method, url, headers))); + fun clientRequest(clientId: String?, method: String, url: String, headers: MutableMap = HashMap()) : BatchBuilder { + _reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers))); return BatchBuilder(_package, _reqs); } @V8Function - fun clientRequestWithBody(client: PackageHttpClient, method: String, url: String, body:String, headers: MutableMap = HashMap()) : BatchBuilder { - _reqs.add(Pair(client, RequestDescriptor(method, url, headers, body))); + fun clientRequestWithBody(clientId: String?, method: String, url: String, body:String, headers: MutableMap = HashMap()) : BatchBuilder { + _reqs.add(Pair(_package.getClient(clientId), RequestDescriptor(method, url, headers, body))); return BatchBuilder(_package, _reqs); } @V8Function - fun clientGET(client: PackageHttpClient, url: String, headers: MutableMap = HashMap()) : BatchBuilder - = clientRequest(client, "GET", url, headers); + fun clientGET(clientId: String?, url: String, headers: MutableMap = HashMap()) : BatchBuilder + = clientRequest(clientId, "GET", url, headers); @V8Function - fun clientPOST(client: PackageHttpClient, url: String, body: String, headers: MutableMap = HashMap()) : BatchBuilder - = clientRequestWithBody(client, "POST", url, body, headers); + fun clientPOST(clientId: String?, url: String, body: String, headers: MutableMap = HashMap()) : BatchBuilder + = clientRequestWithBody(clientId, "POST", url, body, headers); //Finalizer @@ -321,6 +334,7 @@ class PackageHttp: V8Package { @Transient private val _clientId: String?; + @V8Property fun clientId(): String? { return _clientId; @@ -333,6 +347,17 @@ class PackageHttp: V8Package { _clientId = if(_client is JSHttpClient) _client.clientId else null; } + @V8Function + fun resetAuthCookies(){ + if(_client is JSHttpClient) + _client.resetAuthCookies(); + } + @V8Function + fun clearOtherCookies(){ + if(_client is JSHttpClient) + _client.clearOtherCookies(); + } + @V8Function fun setDefaultHeaders(defaultHeaders: Map) { for(pair in defaultHeaders) @@ -429,8 +454,23 @@ class PackageHttp: V8Package { }; } @V8Function - fun POST(url: String, body: String, headers: MutableMap = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse - = POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING) + fun POST(url: String, body: Any, headers: MutableMap = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse { + if(body is V8ValueString) + return POSTInternal(url, body.value, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING); + else if(body is String) + return POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING); + else if(body is V8ValueTypedArray) + return POSTInternal(url, body.toBytes(), headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING); + else if(body is ByteArray) + return POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING); + else if(body is ArrayList<*>) //Avoid this case, used purely for testing + return POSTInternal(url, body.map { (it as Double).toInt().toByte() }.toByteArray(), headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING); + else + throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST"); + } + + + // = POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING) fun POSTInternal(url: String, body: String, headers: MutableMap = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse { applyDefaultHeaders(headers); return logExceptions { @@ -452,9 +492,6 @@ class PackageHttp: V8Package { } }; } - @V8Function - fun POST(url: String, body: ByteArray, headers: MutableMap = HashMap(), useBytes: Boolean = false) : IBridgeHttpResponse - = POSTInternal(url, body, headers, if(useBytes) ReturnType.BYTES else ReturnType.STRING) fun POSTInternal(url: String, body: ByteArray, headers: MutableMap = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse { applyDefaultHeaders(headers); return logExceptions { @@ -630,7 +667,9 @@ class PackageHttp: V8Package { _isOpen = true; if(hasOpen && _listeners?.isClosed != true) { try { - _listeners?.invokeVoid("open", arrayOf()); + _package._plugin.busy { + _listeners?.invokeVoid("open", arrayOf()); + } } catch(ex: Throwable){ Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex); @@ -640,7 +679,9 @@ class PackageHttp: V8Package { override fun message(msg: String) { if(hasMessage && _listeners?.isClosed != true) { try { - _listeners?.invokeVoid("message", msg); + _package._plugin.busy { + _listeners?.invokeVoid("message", msg); + } } catch(ex: Throwable) {} } @@ -649,7 +690,9 @@ class PackageHttp: V8Package { if(hasClosing && _listeners?.isClosed != true) { try { - _listeners?.invokeVoid("closing", code, reason); + _package._plugin.busy { + _listeners?.invokeVoid("closing", code, reason); + } } catch(ex: Throwable){ Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex); @@ -660,7 +703,9 @@ class PackageHttp: V8Package { _isOpen = false; if(hasClosed && _listeners?.isClosed != true) { try { - _listeners?.invokeVoid("closed", code, reason); + _package._plugin.busy { + _listeners?.invokeVoid("closed", code, reason); + } } catch(ex: Throwable){ Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); @@ -676,7 +721,9 @@ class PackageHttp: V8Package { Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception); if(hasFailure && _listeners?.isClosed != true) { try { - _listeners?.invokeVoid("failure", exception.message); + _package._plugin.busy { + _listeners?.invokeVoid("failure", exception.message); + } } catch(ex: Throwable){ Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ArticleDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ArticleDetailFragment.kt index 989a19e1..d052e0f1 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ArticleDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ArticleDetailFragment.kt @@ -778,6 +778,8 @@ class ArticleDetailFragment : MainFragment { view.onAddToWatchLaterClicked.subscribe { a -> if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)) UIDialogs.toast("Added to watch later\n[${content.name}]") + else + UIDialogs.toast(context.getString(R.string.already_in_watch_later)) } } else if(content is IPlatformPost) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt index 63b60c1f..4cd8455c 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt @@ -172,7 +172,7 @@ class ChannelFragment : MainFragment() { _buttonSubscribe = findViewById(R.id.button_subscribe) _buttonSubscriptionSettings = findViewById(R.id.button_sub_settings) _overlayLoading = findViewById(R.id.channel_loading_overlay) - _overlayLoadingSpinner = findViewById(R.id.channel_loader) + _overlayLoadingSpinner = findViewById(R.id.channel_loader_frag) _overlayContainer = findViewById(R.id.overlay_container) _buttonSubscribe.onSubscribed.subscribe { UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer) @@ -226,6 +226,8 @@ class ChannelFragment : MainFragment() { if (content is IPlatformVideo) { if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)) UIDialogs.toast("Added to watch later\n[${content.name}]") + else + UIDialogs.toast(context.getString(R.string.already_in_watch_later)) } } adapter.onUrlClicked.subscribe { url -> diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt index cc528a2b..fbb85dac 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt @@ -86,6 +86,8 @@ abstract class ContentFeedView : FeedView { + StatePlatform.instance.handleReloadRequired(it, { + fetchVideo(); + }); + } .exception { Logger.w(TAG, "exception", it) diff --git a/app/src/main/java/com/futo/platformplayer/models/Playlist.kt b/app/src/main/java/com/futo/platformplayer/models/Playlist.kt index 9862e675..758929d5 100644 --- a/app/src/main/java/com/futo/platformplayer/models/Playlist.kt +++ b/app/src/main/java/com/futo/platformplayer/models/Playlist.kt @@ -5,6 +5,7 @@ import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.models.JSVideo +import com.futo.platformplayer.ensureIsBusy import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.serializers.OffsetDateTimeSerializer import kotlinx.serialization.Serializable @@ -43,6 +44,7 @@ class Playlist { fun fromV8(config: SourcePluginConfig, obj: V8ValueObject?): Playlist? { if(obj == null) return null; + obj.ensureIsBusy(); val contextName = "Playlist"; diff --git a/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt b/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt index b39b4592..5ab75011 100644 --- a/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt +++ b/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt @@ -62,7 +62,7 @@ class DownloadService : Service() { Logger.i(TAG, "onStartCommand"); synchronized(this) { if(_started) - return START_STICKY; + return START_NOT_STICKY; if(!FragmentedStorage.isInitialized) { Logger.i(TAG, "Attempted to start DownloadService without initialized files"); 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 c843ea9f..1f1e5625 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.states import android.content.Context import androidx.collection.LruCache +import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs @@ -38,6 +39,7 @@ import com.futo.platformplayer.awaitFirstNotNullDeferred import com.futo.platformplayer.constructs.BatchedTaskHandler import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException import com.futo.platformplayer.fromPool import com.futo.platformplayer.getNowDiffDays import com.futo.platformplayer.getNowDiffSeconds @@ -316,7 +318,18 @@ class StatePlatform { _platformOrderPersistent.save(); } - suspend fun reloadClient(context: Context, id: String) : JSClient? { + fun handleReloadRequired(reloadRequiredException: ScriptReloadRequiredException, afterReload: (() -> Unit)? = null) { + val id = if(reloadRequiredException.config is SourcePluginConfig) reloadRequiredException.config.id else ""; + UIDialogs.appToast("Reloading [${reloadRequiredException.config.name}] by plugin request"); + StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { + if(!reloadRequiredException.reloadData.isNullOrEmpty()) + reEnableClientWithData(id, reloadRequiredException.reloadData, afterReload); + else + reEnableClient(id, afterReload); + } + } + + suspend fun reloadClient(context: Context, id: String, afterReload: (()->Unit)? = null) : JSClient? { return withContext(Dispatchers.IO) { val client = getClient(id); if (client !is JSClient) @@ -347,10 +360,27 @@ class StatePlatform { _availableClients.removeIf { it.id == id }; _availableClients.add(newClient); } + afterReload?.invoke(); return@withContext newClient; }; } + suspend fun reEnableClientWithData(id: String, data: String? = null, afterReload: (()->Unit)? = null) { + val enabledBefore = getEnabledClients().map { it.id }; + if(data != null) { + val client = getClientOrNull(id); + if(client != null && client is JSClient) + client.setReloadData(data); + } + selectClients({ + _scope.launch(Dispatchers.IO) { + selectClients({ + afterReload?.invoke(); + }, *(enabledBefore).distinct().toTypedArray()); + } + }, *(enabledBefore.filter { it != id }).distinct().toTypedArray()) + } + suspend fun reEnableClient(id: String, afterReload: (()->Unit)? = null) = reEnableClientWithData(id, null, afterReload); suspend fun enableClient(ids: List) { val currentClients = getEnabledClients().map { it.id }; @@ -361,6 +391,9 @@ class StatePlatform { * If a client is disabled, NO requests are made to said client */ suspend fun selectClients(vararg ids: String) { + selectClients(null, *ids); + } + suspend fun selectClients(afterLoad: (() -> Unit)?, vararg ids: String) { withContext(Dispatchers.IO) { synchronized(_clientsLock) { val removed = _enabledClients.toMutableList(); @@ -385,6 +418,7 @@ class StatePlatform { onSourceDisabled.emit(oldClient); } } + afterLoad?.invoke(); }; } @@ -935,7 +969,7 @@ class StatePlatform { return EmptyPager(); if(!StateApp.instance.privateMode) - return client.fromPool(_mainClientPool).getComments(url); + return client.fromPool(_pagerClientPool).getComments(url); else return client.fromPool(_privateClientPool).getComments(url); } diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt index 6ebf7be6..cbe1c518 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt @@ -3,7 +3,6 @@ package com.futo.platformplayer.states import android.content.Context import android.net.Uri import androidx.core.content.FileProvider -import androidx.fragment.app.Fragment import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException @@ -21,7 +20,6 @@ import com.futo.platformplayer.models.ImportCache import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.sToOffsetDateTimeUTC import com.futo.platformplayer.smartMerge -import com.futo.platformplayer.states.StateSubscriptionGroups.Companion import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.StringArrayStorage import com.futo.platformplayer.stores.StringDateMapStorage @@ -30,15 +28,12 @@ import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ReconstructStore import com.futo.platformplayer.sync.internal.GJSyncOpcodes import com.futo.platformplayer.sync.models.SyncPlaylistsPackage -import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage -import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage import com.futo.platformplayer.sync.models.SyncWatchLaterPackage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.io.File -import java.time.Instant import java.time.OffsetDateTime import java.time.ZoneOffset @@ -178,31 +173,30 @@ class StatePlaylists { StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER); } } - fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1): Boolean { - var wasNew = false; + fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false): Boolean { synchronized(_watchlistStore) { - if(!_watchlistStore.hasItem { it.url == video.url }) - wasNew = true; - _watchlistStore.saveAsync(video); - if(orderPosition == -1) - _watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray()); - else { - val existing = _watchlistOrderStore.getAllValues().toMutableList(); - existing.add(orderPosition, video.url); - _watchlistOrderStore.set(*existing.toTypedArray()); + if (_watchlistStore.hasItem { it.url == video.url }) { + return false } - _watchlistOrderStore.save(); + + _watchlistStore.saveAsync(video) + if (Settings.instance.other.watchLaterAddStart) { + _watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray()) + } else { + _watchlistOrderStore.set(*(_watchlistOrderStore.values + listOf(video.url)).toTypedArray()) + } + _watchlistOrderStore.save() } onWatchLaterChanged.emit(); - if(isUserInteraction) { + if (isUserInteraction) { val now = OffsetDateTime.now(); _watchLaterAdds.setAndSave(video.url, now); broadcastWatchLaterAddition(video, now); } StateDownloads.instance.checkForOutdatedPlaylists(); - return wasNew; + return true; } fun getLastPlayedPlaylist() : Playlist? { diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt index 90c334e3..25cff055 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt @@ -78,7 +78,13 @@ class StateSync { onAuthorized = { sess, isNewlyAuthorized, isNewSession -> if (isNewSession) { deviceUpdatedOrAdded.emit(sess.remotePublicKey, sess) - StateApp.instance.scope.launch(Dispatchers.IO) { checkForSync(sess) } + StateApp.instance.scope.launch(Dispatchers.IO) { + try { + checkForSync(sess) + } catch (e: Throwable) { + Logger.e(TAG, "Failed to check for sync.", e) + } + } } } diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt index b72e840c..2740ca8b 100644 --- a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt +++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt @@ -174,7 +174,7 @@ abstract class SubscriptionsTaskFetchAlgorithm( if (resolve != null) { resolveCount = resolves.size; - UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size}") + UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size})") for(result in resolve){ val task = providedTasks?.find { it.url == result.channelUrl }; if(task != null) { diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt index 42cef197..b059d2c9 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListEditorViewHolder.kt @@ -95,7 +95,13 @@ class VideoListEditorViewHolder : ViewHolder { .into(_imageThumbnail); _textName.text = v.name; _textAuthor.text = v.author.name; - _textVideoDuration.text = v.duration.toHumanTime(false); + + if(v.duration > 0) { + _textVideoDuration.text = v.duration.toHumanTime(false); + _textVideoDuration.visibility = View.VISIBLE; + } + else + _textVideoDuration.visibility = View.GONE; val historyPosition = StateHistory.instance.getHistoryPosition(v.url) _timeBar.progress = historyPosition.toFloat() / v.duration.toFloat(); diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt index bcabda4f..898b7e14 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt @@ -204,8 +204,14 @@ open class PreviewVideoView : LinearLayout { .into(_imageVideo); }; - if(!isPlanned) - _textVideoDuration.text = video.duration.toHumanTime(false); + if(!isPlanned) { + if(video.duration > 0) { + _textVideoDuration.text = video.duration.toHumanTime(false); + _textVideoDuration.visibility = View.VISIBLE; + } + else + _textVideoDuration.visibility = View.GONE; + } else _textVideoDuration.text = context.getString(R.string.planned); diff --git a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt index f90102b3..10a88341 100644 --- a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt @@ -39,6 +39,9 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.util.Locale class GestureControlView : LinearLayout { @@ -79,6 +82,9 @@ class GestureControlView : LinearLayout { private var _adjustingFullscreenDown: Boolean = false; private var _fullScreenFactorUp = 1.0f; private var _fullScreenFactorDown = 1.0f; + private val _layoutHoldSpeed: LinearLayout + private val _textHoldFastForward: TextView + private val _imageHoldFastForward: ImageView private var _scaleGestureDetector: ScaleGestureDetector private var _scaleFactor = 1.0f @@ -92,6 +98,11 @@ class GestureControlView : LinearLayout { private var _surfaceView: View? = null private var _layoutIndicatorFill: FrameLayout; private var _layoutIndicatorFit: FrameLayout; + private var _speedHolding = false + + private val _speedFormatter = DecimalFormat("#.##", DecimalFormatSymbols(Locale.US)).apply { + roundingMode = java.math.RoundingMode.HALF_UP + } private val _gestureController: GestureDetectorCompat; @@ -103,6 +114,8 @@ class GestureControlView : LinearLayout { val onZoom = Event1(); val onSoundAdjusted = Event1(); val onToggleFullscreen = Event0(); + val onSpeedHoldStart = Event0() + val onSpeedHoldEnd = Event0() var fullScreenGestureEnabled = true @@ -124,6 +137,9 @@ class GestureControlView : LinearLayout { _layoutControlsFullscreen = findViewById(R.id.layout_controls_fullscreen); _layoutIndicatorFill = findViewById(R.id.layout_indicator_fill); _layoutIndicatorFit = findViewById(R.id.layout_indicator_fit); + _layoutHoldSpeed = findViewById(R.id.layout_controls_increased_speed) + _textHoldFastForward = findViewById(R.id.text_holdFastForward) + _imageHoldFastForward = findViewById(R.id.image_holdFastForward) _scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { @@ -216,7 +232,21 @@ class GestureControlView : LinearLayout { return true; } - override fun onLongPress(p0: MotionEvent) = Unit + override fun onLongPress(p0: MotionEvent) { + if (!_isControlsLocked + && !_skipping + && !_adjustingBrightness + && !_adjustingSound + && !_adjustingFullscreenUp + && !_adjustingFullscreenDown + && !_isPanning + && !_isZooming + && Settings.instance.playback.getHoldPlaybackSpeed() > 1.0) { + _speedHolding = true + showHoldSpeedControls() + onSpeedHoldStart.emit() + } + } }); _gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener { @@ -301,6 +331,17 @@ class GestureControlView : LinearLayout { onPan.emit(_translationX, _translationY) } + private fun showHoldSpeedControls() { + _layoutHoldSpeed.visibility = View.VISIBLE + _textHoldFastForward.text = _speedFormatter.format(Settings.instance.playback.getHoldPlaybackSpeed()) + "x" + (_imageHoldFastForward.drawable as? Animatable)?.start() + } + + private fun hideHoldSpeedControls() { + _layoutHoldSpeed.visibility = View.GONE + (_imageHoldFastForward.drawable as? Animatable)?.stop() + } + fun setupTouchArea(layoutControls: ViewGroup? = null, background: View? = null) { _layoutControls = layoutControls; _background = background; @@ -309,6 +350,12 @@ class GestureControlView : LinearLayout { override fun onTouchEvent(event: MotionEvent?): Boolean { val ev = event ?: return super.onTouchEvent(event); + if (ev.action == MotionEvent.ACTION_UP && _speedHolding) { + _speedHolding = false + hideHoldSpeedControls() + onSpeedHoldEnd.emit() + } + cancelHideJob(); if (_skipping) { 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 fff853a8..fe941fea 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 @@ -18,6 +18,7 @@ import androidx.media3.ui.DefaultTimeBar import androidx.media3.ui.TimeBar import com.bumptech.glide.Glide import com.futo.platformplayer.R +import com.futo.platformplayer.Settings import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.casting.AirPlayCastingDevice @@ -58,6 +59,8 @@ class CastView : ConstraintLayout { private var _inPictureInPicture: Boolean = false; private var _chapters: List? = null; private var _currentChapter: IChapter? = null; + private var _speedHoldPrevRate = 1.0 + private var _speedHoldWasPlaying = false val onChapterChanged = Event2(); val onMinimizeClick = Event0(); @@ -87,6 +90,20 @@ class CastView : ConstraintLayout { _gestureControlView = findViewById(R.id.gesture_control); _gestureControlView.fullScreenGestureEnabled = false _gestureControlView.setupTouchArea(); + _gestureControlView.onSpeedHoldStart.subscribe { + val d = StateCasting.instance.activeDevice ?: return@subscribe; + _speedHoldWasPlaying = d.isPlaying + _speedHoldPrevRate = d.speed + if (d.canSetSpeed) + d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed()) + d.resumeVideo() + } + _gestureControlView.onSpeedHoldEnd.subscribe { + val d = StateCasting.instance.activeDevice ?: return@subscribe; + if (!_speedHoldWasPlaying) d.pauseVideo() + d.changeSpeed(_speedHoldPrevRate) + } + _gestureControlView.onSeek.subscribe { val d = StateCasting.instance.activeDevice ?: return@subscribe; StateCasting.instance.videoSeekTo(d.expectedCurrentTime + it / 1000); diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt index 58850998..72500a49 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt @@ -13,6 +13,7 @@ import android.widget.RelativeLayout import android.widget.TextView import androidx.core.animation.doOnEnd import androidx.core.view.children +import androidx.core.view.isVisible import com.futo.platformplayer.R import com.futo.platformplayer.constructs.Event0 @@ -42,10 +43,14 @@ class SlideUpMenuOverlay : RelativeLayout { constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List, hideButtons: Boolean = false): super(context){ init(animated, okText); _container = parent; - if(!_container!!.children.contains(this)) { - _container!!.removeAllViews(); - _container!!.addView(this); + _container!!.removeAllViews(); + _container!!.addView(this); + if (_container!!.isVisible) { + isVisible = true + _viewBackground.alpha = 1.0f; + _viewOverlayContainer.translationY = 0.0f; } + _textTitle.text = titleText; groupItems = items; @@ -56,6 +61,12 @@ class SlideUpMenuOverlay : RelativeLayout { } setItems(items); + + if (!isVisible) { + _viewOverlayContainer.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + _viewOverlayContainer.translationY = _viewOverlayContainer.measuredHeight.toFloat() + _viewBackground.alpha = 0f; + } } @@ -146,16 +157,9 @@ class SlideUpMenuOverlay : RelativeLayout { } isVisible = true; - _container?.post { - _container?.visibility = View.VISIBLE; - _container?.bringToFront(); - } + _container?.visibility = View.VISIBLE; if (_animated) { - _viewOverlayContainer.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); - _viewOverlayContainer.translationY = _viewOverlayContainer.measuredHeight.toFloat() - _viewBackground.alpha = 0f; - val animations = arrayListOf(); animations.add(ObjectAnimator.ofFloat(_viewBackground, "alpha", 0.0f, 1.0f).setDuration(ANIMATION_DURATION_MS)); animations.add(ObjectAnimator.ofFloat(_viewOverlayContainer, "translationY", _viewOverlayContainer.measuredHeight.toFloat(), 0.0f).setDuration(ANIMATION_DURATION_MS)); 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 e209f937..b1ba431a 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 @@ -117,6 +117,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase { private var _isControlsLocked: Boolean = false; + private var _speedHoldPrevRate = 1f + private var _speedHoldWasPlaying = false + private val _time_bar_listener: TimeBar.OnScrubListener; var isFitMode : Boolean = false @@ -254,6 +257,20 @@ class FutoVideoPlayer : FutoVideoPlayerBase { gestureControl = findViewById(R.id.gesture_control); gestureControl.setupTouchArea(_layoutControls, background); + gestureControl.onSpeedHoldStart.subscribe { + exoPlayer?.player?.let { player -> + _speedHoldWasPlaying = player.isPlaying + _speedHoldPrevRate = getPlaybackRate() + setPlaybackRate(Settings.instance.playback.getHoldPlaybackSpeed().toFloat()) + player.play() + } + } + gestureControl.onSpeedHoldEnd.subscribe { + exoPlayer?.player?.let { player -> + if (!_speedHoldWasPlaying) player.pause() + setPlaybackRate(_speedHoldPrevRate) + } + } gestureControl.onSeek.subscribe { seekFromCurrent(it); }; gestureControl.onSoundAdjusted.subscribe { if (Settings.instance.gestureControls.useSystemVolume) { 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 60c5dbf2..43ed541d 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 @@ -52,10 +52,13 @@ 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.constructs.Event0 import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.video.PlayerManager import com.futo.platformplayer.views.video.datasources.PluginMediaDrmCallback import com.futo.platformplayer.views.video.datasources.JSHttpDataSource @@ -108,6 +111,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout { val onPositionDiscontinuity = Event1(); val onDatasourceError = Event1(); + val onReloadRequired = Event0(); + private var _didCallSourceChange = false; private var _lastState: Int = -1; @@ -348,8 +353,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout { var videoSourceUsed = videoSource; var audioSourceUsed = audioSource; if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){ - videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource); - audioSourceUsed = null; + videoSource.getUnderlyingPlugin()?.busy { + videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource); + audioSourceUsed = null; + } } val didSetVideo = swapSourceInternal(videoSourceUsed, play, resume); @@ -560,17 +567,20 @@ abstract class FutoVideoPlayerBase : RelativeLayout { if(videoSource.hasGenerate) { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { + var startId = -1; try { - val generated = videoSource.generate(); + val plugin = videoSource.getUnderlyingPlugin() ?: return@launch; + startId = plugin.getUnderlyingPlugin()?.runtimeId ?: -1; + val generated = plugin.busy { videoSource.generate(); }; if (generated != null) { withContext(Dispatchers.Main) { val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource)) - videoSource.getHttpDataSourceFactory() + withContext(Dispatchers.IO) { videoSource.getHttpDataSourceFactory() } else DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); if(dataSource is JSHttpDataSource.Factory && videoSource is JSDashManifestMergingRawSource) - dataSource.setRequestExecutor2(videoSource.audio.getRequestExecutor()); + dataSource.setRequestExecutor2(withContext(Dispatchers.IO){videoSource.audio.getRequestExecutor()}); _lastVideoMediaSource = DashMediaSource.Factory(dataSource) .createMediaSource( DashManifestParser().parse( @@ -585,6 +595,17 @@ abstract class FutoVideoPlayerBase : RelativeLayout { } } } + catch(reloadRequired: ScriptReloadRequiredException) { + Logger.i(TAG, "Reload required detected"); + val plugin = videoSource.getUnderlyingPlugin(); + if(plugin == null) + return@launch; + if(startId != -1 && plugin.getUnderlyingPlugin()?.runtimeId != startId) + return@launch; + StatePlatform.instance.handleReloadRequired(reloadRequired, { + onReloadRequired.emit(); + }); + } catch(ex: Throwable) { Logger.e(TAG, "DashRaw generator failed", ex); } @@ -671,25 +692,47 @@ abstract class FutoVideoPlayerBase : RelativeLayout { @OptIn(UnstableApi::class) private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean): Boolean { Logger.i(TAG, "Loading AudioSource [DashRaw]"); - val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource)) - audioSource.getHttpDataSourceFactory() - else - DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); if(audioSource.hasGenerate) { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { - val generated = audioSource.generate(); - if(generated != null) { - withContext(Dispatchers.Main) { - _lastVideoMediaSource = DashMediaSource.Factory(dataSource) - .createMediaSource(DashManifestParser().parse(Uri.parse(audioSource.url), - ByteArrayInputStream(generated?.toByteArray() ?: ByteArray(0)))); - loadSelectedSources(play, resume); + var startId = -1; + try { + startId = audioSource.getUnderlyingPlugin()?.getUnderlyingPlugin()?.runtimeId ?: -1; + val generated = audioSource.generate(); + if(generated != null) { + val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource)) + audioSource.getHttpDataSourceFactory() + else + DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); + withContext(Dispatchers.Main) { + _lastVideoMediaSource = DashMediaSource.Factory(dataSource) + .createMediaSource(DashManifestParser().parse(Uri.parse(audioSource.url), + ByteArrayInputStream(generated?.toByteArray() ?: ByteArray(0)))); + loadSelectedSources(play, resume); + } } } + catch(reloadRequired: ScriptReloadRequiredException) { + Logger.i(TAG, "Reload required detected"); + val plugin = audioSource.getUnderlyingPlugin(); + if(plugin == null) + return@launch; + if(startId != -1 && plugin.getUnderlyingPlugin()?.runtimeId != startId) + return@launch; + StatePlatform.instance.reEnableClient(plugin.id, { + onReloadRequired.emit(); + }); + } + catch(ex: Throwable) { + + } } return false; } else { + val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource)) + audioSource.getHttpDataSourceFactory() + else + DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); _lastVideoMediaSource = DashMediaSource.Factory(dataSource) .createMediaSource( DashManifestParser().parse( diff --git a/app/src/main/res/layout/activity_polycentric_backup.xml b/app/src/main/res/layout/activity_polycentric_backup.xml index e31e8584..d6579dd7 100644 --- a/app/src/main/res/layout/activity_polycentric_backup.xml +++ b/app/src/main/res/layout/activity_polycentric_backup.xml @@ -76,4 +76,15 @@ app:buttonIcon="@drawable/ic_copy" android:layout_marginTop="8dp" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml index ea9d4f52..29292034 100644 --- a/app/src/main/res/layout/fragment_channel.xml +++ b/app/src/main/res/layout/fragment_channel.xml @@ -173,7 +173,7 @@ android:background="#77000000" android:gravity="center"> - - + diff --git a/app/src/main/res/layout/list_locked_preview.xml b/app/src/main/res/layout/list_locked_preview.xml index 2413c98c..1aefd5b8 100644 --- a/app/src/main/res/layout/list_locked_preview.xml +++ b/app/src/main/res/layout/list_locked_preview.xml @@ -116,9 +116,9 @@ android:layout_marginBottom="6dp" android:background="#DD000000" android:visibility="gone" + android:gravity="center" android:orientation="vertical"> - + android:gravity="center" + android:orientation="vertical"> + + - - - + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimensions.xml b/app/src/main/res/values/dimensions.xml index 2e4468b5..5aab83f2 100644 --- a/app/src/main/res/values/dimensions.xml +++ b/app/src/main/res/values/dimensions.xml @@ -3,4 +3,5 @@ 500dp 200dp 400 + 600 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b2513e21..39cf819a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -433,6 +433,8 @@ Minimum Available Speed Maximum Playback Speed Maximum Available Speed + Hold playback speed + Playback speed when pressing down on the video Playback Speed Step Size The step size of playback speeds, may not affect higher playback speeds. Fast-Forward / Fast-Rewind duration @@ -466,6 +468,9 @@ Show confirmation dialog when deleting media from a playlist Allow duplicate playlist videos Allow adding duplicate videos to playlists + Add new videos to the beginning of Watch Later + When adding videos to Watch Later add them to the beginning of the list instead of the end + Already in watch later Enable Polycentric Enable Polycentric Local Caching Caches polycentric results on-device to reduce load times, changing requires app reboot @@ -1106,6 +1111,17 @@ 4.0 5.0 + + Disabled + 1.25 + 1.5 + 1.75 + 2.0 + 2.25 + 2.5 + 2.75 + 3.0 + 0.25 0.5 diff --git a/app/src/stable/assets/sources/kick b/app/src/stable/assets/sources/kick index ffdf4cda..b7173f15 160000 --- a/app/src/stable/assets/sources/kick +++ b/app/src/stable/assets/sources/kick @@ -1 +1 @@ -Subproject commit ffdf4cda380e5e4e9e370412f014e704bd14c09e +Subproject commit b7173f1538a8259ace0c606dfc3441426a659536 diff --git a/app/src/stable/assets/sources/patreon b/app/src/stable/assets/sources/patreon index d98c7f8a..b811f8bd 160000 --- a/app/src/stable/assets/sources/patreon +++ b/app/src/stable/assets/sources/patreon @@ -1 +1 @@ -Subproject commit d98c7f8aee36101d60a0c671d16a0800d5d715d0 +Subproject commit b811f8bdfbbff73cf0d7581c9d7596911cb132b6 diff --git a/app/src/stable/assets/sources/soundcloud b/app/src/stable/assets/sources/soundcloud index 54564312..048acef1 160000 --- a/app/src/stable/assets/sources/soundcloud +++ b/app/src/stable/assets/sources/soundcloud @@ -1 +1 @@ -Subproject commit 54564312683a0ae06d7085405478f96cade325e3 +Subproject commit 048acef152823d2621da177d3b4e1535cf4ca8ac diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 2e258294..97480075 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 2e25829494addebbf9292b7c68b2d6130e1fc461 +Subproject commit 97480075fd8a50fa7eb601b5ed4bc1e531207dd0 diff --git a/app/src/unstable/assets/sources/kick b/app/src/unstable/assets/sources/kick index ffdf4cda..b7173f15 160000 --- a/app/src/unstable/assets/sources/kick +++ b/app/src/unstable/assets/sources/kick @@ -1 +1 @@ -Subproject commit ffdf4cda380e5e4e9e370412f014e704bd14c09e +Subproject commit b7173f1538a8259ace0c606dfc3441426a659536 diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 2e258294..97480075 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 2e25829494addebbf9292b7c68b2d6130e1fc461 +Subproject commit 97480075fd8a50fa7eb601b5ed4bc1e531207dd0