From 8202513993aca4fdd4e720331a6167648243a6fa Mon Sep 17 00:00:00 2001 From: Kai Date: Thu, 22 May 2025 12:12:34 -0500 Subject: [PATCH 01/87] fix stutter when switching to background Changelog: changed --- .../main/java/com/futo/platformplayer/Settings.kt | 5 ----- .../fragment/mainactivity/main/VideoDetailView.kt | 2 +- .../views/video/FutoVideoPlayerBase.kt | 14 ++++++++++++-- app/src/main/res/values/strings.xml | 1 - 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 7f827f66..cc70191a 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -467,11 +467,6 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9) var useLiveChatWindow: Boolean = true; - - - @FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 10) - var backgroundSwitchToAudio: Boolean = true; - @FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11) @DropdownFieldOptionsId(R.array.restart_playback_after_loss) var restartPlaybackAfterLoss: Int = 1; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index d1a3e41e..901a4898 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -1104,7 +1104,7 @@ class VideoDetailView : ConstraintLayout { when (Settings.instance.playback.backgroundPlay) { 0 -> handlePause(); 1 -> { - if(!(video?.isLive ?: false) && Settings.instance.playback.backgroundSwitchToAudio) + if(!(video?.isLive ?: false)) _player.switchToAudioMode(); StatePlayer.instance.startOrUpdateMediaSession(context, video); } 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 c872ca02..60c5dbf2 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 @@ -252,12 +252,22 @@ abstract class FutoVideoPlayerBase : RelativeLayout { fun switchToVideoMode() { Logger.i(TAG, "Switching to Video Mode"); isAudioMode = false; - loadSelectedSources(playing, true); + val player = exoPlayer ?: return + player.player.trackSelectionParameters = + player.player.trackSelectionParameters + .buildUpon() + .setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, isAudioMode) + .build() } fun switchToAudioMode() { Logger.i(TAG, "Switching to Audio Mode"); isAudioMode = true; - loadSelectedSources(playing, true); + val player = exoPlayer ?: return + player.player.trackSelectionParameters = + player.player.trackSelectionParameters + .buildUpon() + .setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, isAudioMode) + .build() } fun seekTo(ms: Long) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f81f1642..85445796 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -427,7 +427,6 @@ Seek duration Fast-Forward / Fast-Rewind duration Switch to Audio in Background - Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter Groups Show Subscription Groups Use Subscription Exchange (Experimental) From 29f1bef0997e4b0a17fb457643aff398b878fb76 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 4 Jun 2025 20:43:37 +0200 Subject: [PATCH 02/87] Advanced settings option, Playlist id saving for exports and backups, Sync synchronization to prevent dups --- .../java/com/futo/platformplayer/Settings.kt | 31 +++++++- .../platformplayer/states/StatePlaylists.kt | 14 +++- .../futo/platformplayer/states/StateSync.kt | 79 +++++++++++-------- .../views/fields/ButtonField.kt | 4 +- .../views/fields/DropdownField.kt | 7 +- .../futo/platformplayer/views/fields/Field.kt | 8 +- .../platformplayer/views/fields/FieldForm.kt | 37 +++++++-- .../platformplayer/views/fields/GroupField.kt | 3 +- .../views/fields/ReadOnlyTextField.kt | 3 +- .../views/fields/ToggleField.kt | 9 ++- app/src/main/res/values/strings.xml | 2 + 11 files changed, 146 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 7f827f66..363eb917 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -29,6 +29,7 @@ import com.futo.platformplayer.states.StateUpdate import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorageFileJson import com.futo.platformplayer.views.FeedStyle +import com.futo.platformplayer.views.fields.AdvancedField import com.futo.platformplayer.views.fields.DropdownFieldOptionsId import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FormField @@ -175,6 +176,10 @@ class Settings : FragmentedStorageFileJson() { } }*/ + + @FormField(R.string.advanced_settings, FieldForm.TOGGLE, R.string.advanced_settings_description, -1, "advancedSettings") + var advancedSettings: Boolean = false; + @FormField(R.string.language, "group", -1, 0) var language = LanguageSettings(); @Serializable @@ -224,7 +229,7 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) var previewFeedItems: Boolean = true; - + @AdvancedField @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) var progressBar: Boolean = true; @@ -256,6 +261,7 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5) var previewFeedItems: Boolean = true; + @AdvancedField @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) var progressBar: Boolean = true; @@ -277,6 +283,7 @@ class Settings : FragmentedStorageFileJson() { @Serializable class ChannelSettings { + @AdvancedField @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) var progressBar: Boolean = true; } @@ -302,16 +309,20 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6) var useSubscriptionExchange: Boolean = false; + @AdvancedField @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) var previewFeedItems: Boolean = true; + @AdvancedField @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7) var progressBar: Boolean = true; + @AdvancedField @FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8) @Serializable(with = FlexibleBooleanSerializer::class) var fetchOnAppBoot: Boolean = true; + @AdvancedField @FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9) var fetchOnTabOpen: Boolean = true; @@ -342,13 +353,16 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12) var showWatchMetrics: Boolean = false; + @AdvancedField @FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13) var allowPlaytimeTracking: Boolean = true; + @AdvancedField @FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14) var alwaysReloadFromCache: Boolean = false; + @AdvancedField @FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15) var peekChannelContents: Boolean = false; @@ -425,9 +439,11 @@ class Settings : FragmentedStorageFileJson() { var preferredPreviewQuality: Int = 5; fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality); + @AdvancedField @FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4) var simplifySources: Boolean = true; + @AdvancedField @FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5) var alwaysAllowReverseLandscapeAutoRotate: Boolean = true @@ -438,6 +454,7 @@ class Settings : FragmentedStorageFileJson() { fun isBackgroundContinue() = backgroundPlay == 1; fun isBackgroundPictureInPicture() = backgroundPlay == 2; + @AdvancedField @FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 7) @DropdownFieldOptionsId(R.array.resume_after_preview) var resumeAfterPreview: Int = 1; @@ -464,6 +481,7 @@ class Settings : FragmentedStorageFileJson() { }; } + @AdvancedField @FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9) var useLiveChatWindow: Boolean = true; @@ -497,6 +515,7 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21) var autoplay: Boolean = false; + @AdvancedField @FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22) var deleteFromWatchLaterAuto: Boolean = true; @@ -530,6 +549,7 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0) var recommendationsDefault: Boolean = false; + @AdvancedField @FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0) var hideRecommendations: Boolean = false; @@ -566,10 +586,12 @@ class Settings : FragmentedStorageFileJson() { var preferredAudioQuality: Int = 1; fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0; + @AdvancedField @FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4) @Serializable(with = FlexibleBooleanSerializer::class) var byteRangeDownload: Boolean = true; + @AdvancedField @FormField(R.string.byte_range_concurrency, FieldForm.DROPDOWN, R.string.number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources, 5) @DropdownFieldOptionsId(R.array.thread_count) var byteRangeConcurrency: Int = 3; @@ -599,11 +621,12 @@ class Settings : FragmentedStorageFileJson() { @Serializable(with = FlexibleBooleanSerializer::class) var keepScreenOn: Boolean = true; + @AdvancedField @FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3) @Serializable(with = FlexibleBooleanSerializer::class) var alwaysProxyRequests: Boolean = false; - + @AdvancedField @FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4) @Serializable(with = FlexibleBooleanSerializer::class) var allowIpv6: Boolean = true; @@ -675,9 +698,11 @@ class Settings : FragmentedStorageFileJson() { @Serializable class Plugins { + @AdvancedField @FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1) var checkDisabledPluginsForUpdates: Boolean = false; + @AdvancedField @FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0) var clearCookiesOnLogout: Boolean = true; @@ -896,8 +921,10 @@ class Settings : FragmentedStorageFileJson() { var other = Other(); @Serializable class Other { + @AdvancedField @FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2) var playlistDeleteConfirmation: Boolean = true; + @AdvancedField @FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3) var playlistAllowDups: Boolean = true; 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 c20375f2..6ebf7be6 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt @@ -423,17 +423,25 @@ class StatePlaylists { class PlaylistBackup: ReconstructStore() { override fun toReconstruction(obj: Playlist): String { val items = ArrayList(); - items.add(obj.name); + items.add(obj.name + ":::" + obj.id); items.addAll(obj.videos.map { it.url }); return items.map { it.replace("\n","") }.joinToString("\n"); } override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Playlist { + var idToUse = id; val items = backup.split("\n"); if(items.size <= 0) { throw IllegalStateException("Cannot reconstructor playlist ${id}"); } - val name = items[0]; + var name = items[0]; + if(name.contains(":::")){ + val splitIndex = name.indexOf(":::"); + val foundId = name.substring(splitIndex + 3); + if(!foundId.isNullOrEmpty()) + idToUse = foundId; + name = name.substring(0, splitIndex); + } val videos = items.drop(1).filter { it.isNotEmpty() }.map { try { val videoUrl = it; @@ -465,7 +473,7 @@ class StatePlaylists { throw ReconstructionException(name, "${name}:[${it}] ${ex.message}", ex); } }.filter { it != null }.map { it!! } - return Playlist(id, name, videos); + return Playlist(idToUse, name, videos); } } } \ No newline at end of file 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 fd08165c..3cc52a2d 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt @@ -224,6 +224,11 @@ class StateSync { } } + private val _lockSubscriptions = Any(); + private val _lockSubscriptionGroups = Any(); + private val _lockPlaylists = Any(); + private val _lockWatchlater = Any(); + private fun handleData(session: SyncSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) { val remotePublicKey = session.remotePublicKey when (subOpcode) { @@ -307,7 +312,9 @@ class StateSync { data.get(dataBody); val json = String(dataBody, Charsets.UTF_8); val subPackage = Serializer.json.decodeFromString(json); - handleSyncSubscriptionPackage(session, subPackage); + synchronized(_lockSubscriptions) { + handleSyncSubscriptionPackage(session, subPackage); + } if(subPackage.subscriptions.size > 0) { val newestSub = subPackage.subscriptions.maxOf { it.creationTime }; @@ -327,21 +334,23 @@ class StateSync { val pack = Serializer.json.decodeFromString(json); var lastSubgroupChange = OffsetDateTime.MIN; - for(group in pack.groups){ - if(group.lastChange > lastSubgroupChange) - lastSubgroupChange = group.lastChange; + synchronized(_lockSubscriptionGroups) { + for(group in pack.groups){ + if(group.lastChange > lastSubgroupChange) + lastSubgroupChange = group.lastChange; - val existing = StateSubscriptionGroups.instance.getSubscriptionGroup(group.id); + val existing = StateSubscriptionGroups.instance.getSubscriptionGroup(group.id); - if(existing == null) - StateSubscriptionGroups.instance.updateSubscriptionGroup(group, false, true); - else if(existing.lastChange < group.lastChange) { - existing.name = group.name; - existing.urls = group.urls; - existing.image = group.image; - existing.priority = group.priority; - existing.lastChange = group.lastChange; - StateSubscriptionGroups.instance.updateSubscriptionGroup(existing, false, true); + if(existing == null) + StateSubscriptionGroups.instance.updateSubscriptionGroup(group, false, true); + else if(existing.lastChange < group.lastChange) { + existing.name = group.name; + existing.urls = group.urls; + existing.image = group.image; + existing.priority = group.priority; + existing.lastChange = group.lastChange; + StateSubscriptionGroups.instance.updateSubscriptionGroup(existing, false, true); + } } } for(removal in pack.groupRemovals) { @@ -358,18 +367,20 @@ class StateSync { val json = String(dataBody, Charsets.UTF_8); val pack = Serializer.json.decodeFromString(json); - for(playlist in pack.playlists) { - val existing = StatePlaylists.instance.getPlaylist(playlist.id); + synchronized(_lockPlaylists) { + for(playlist in pack.playlists) { + val existing = StatePlaylists.instance.getPlaylist(playlist.id); - if(existing == null) - StatePlaylists.instance.createOrUpdatePlaylist(playlist, false); - else if(existing.dateUpdate < playlist.dateUpdate) { - existing.dateUpdate = playlist.dateUpdate; - existing.name = playlist.name; - existing.videos = playlist.videos; - existing.dateCreation = playlist.dateCreation; - existing.datePlayed = playlist.datePlayed; - StatePlaylists.instance.createOrUpdatePlaylist(existing, false); + if(existing == null) + StatePlaylists.instance.createOrUpdatePlaylist(playlist, false); + else if(existing.dateUpdate < playlist.dateUpdate) { + existing.dateUpdate = playlist.dateUpdate; + existing.name = playlist.name; + existing.videos = playlist.videos; + existing.dateCreation = playlist.dateCreation; + existing.datePlayed = playlist.datePlayed; + StatePlaylists.instance.createOrUpdatePlaylist(existing, false); + } } } for(removal in pack.playlistRemovals) { @@ -390,14 +401,16 @@ class StateSync { Logger.i(TAG, "SyncWatchLater received ${pack.videos.size} (${pack.videoAdds?.size}, ${pack.videoRemovals?.size})"); val allExisting = StatePlaylists.instance.getWatchLater(); - for(video in pack.videos) { - val existing = allExisting.firstOrNull { it.url == video.url }; - val time = if(pack.videoAdds != null && pack.videoAdds.containsKey(video.url)) (pack.videoAdds[video.url] ?: 0).sToOffsetDateTimeUTC() else OffsetDateTime.MIN; - val removalTime = StatePlaylists.instance.getWatchLaterRemovalTime(video.url) ?: OffsetDateTime.MIN; - if(existing == null && time > removalTime) { - StatePlaylists.instance.addToWatchLater(video, false); - if(time > OffsetDateTime.MIN) - StatePlaylists.instance.setWatchLaterAddTime(video.url, time); + synchronized(_lockWatchlater) { + for(video in pack.videos) { + val existing = allExisting.firstOrNull { it.url == video.url }; + val time = if(pack.videoAdds != null && pack.videoAdds.containsKey(video.url)) (pack.videoAdds[video.url] ?: 0).sToOffsetDateTimeUTC() else OffsetDateTime.MIN; + val removalTime = StatePlaylists.instance.getWatchLaterRemovalTime(video.url) ?: OffsetDateTime.MIN; + if(existing == null && time > removalTime) { + StatePlaylists.instance.addToWatchLater(video, false); + if(time > OffsetDateTime.MIN) + StatePlaylists.instance.setWatchLaterAddTime(video.url, time); + } } } for(removal in pack.videoRemovals) { diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/ButtonField.kt b/app/src/main/java/com/futo/platformplayer/views/fields/ButtonField.kt index 03af1ee8..a7e142f8 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/ButtonField.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/ButtonField.kt @@ -41,6 +41,8 @@ class ButtonField : BigButton, IField { return null; }; + override var isAdvanced: Boolean = false; + //private val _title : TextView; //private val _subtitle : TextView; @@ -89,7 +91,7 @@ class ButtonField : BigButton, IField { return this; } - override fun fromField(obj : Any, field : Field, formField: FormField?) : ButtonField { + override fun fromField(obj : Any, field : Field, formField: FormField?, advanced: Boolean) : ButtonField { throw IllegalStateException("ButtonField should only be used for methods"); } override fun setField() { diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/DropdownField.kt b/app/src/main/java/com/futo/platformplayer/views/fields/DropdownField.kt index 590335d3..4f01d4d5 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/DropdownField.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/DropdownField.kt @@ -40,6 +40,8 @@ class DropdownField : TableRow, IField { override var reference: Any? = null; + override var isAdvanced: Boolean = false; + override val onChanged = Event3(); override val value: Any? get() = _selected; @@ -112,7 +114,7 @@ class DropdownField : TableRow, IField { return this; } - override fun fromField(obj: Any, field: Field, formField: FormField?) : DropdownField { + override fun fromField(obj: Any, field: Field, formField: FormField?, advanced: Boolean) : DropdownField { this._field = field; this._obj = obj; @@ -133,6 +135,9 @@ class DropdownField : TableRow, IField { _description.visibility = View.GONE; } + val advancedFieldAttr = field.getAnnotation(AdvancedField::class.java) + if(advancedFieldAttr != null || advanced) + isAdvanced = true; _options = (field.getAnnotation(DropdownFieldOptions::class.java)?.options ?: field.getAnnotation(DropdownFieldOptionsId::class.java)?.optionsId?.let { resources.getStringArray(it) } ?: diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/Field.kt b/app/src/main/java/com/futo/platformplayer/views/fields/Field.kt index 9439d46a..b11ce641 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/Field.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/Field.kt @@ -4,6 +4,10 @@ import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event3 import java.lang.reflect.Field +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class AdvancedField(); + @Target(AnnotationTarget.FIELD, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) @Retention(AnnotationRetention.RUNTIME) @@ -22,6 +26,8 @@ interface IField { val obj : Any?; val field : Field?; + val isAdvanced: Boolean; + val value: Any?; val onChanged : Event3; @@ -29,7 +35,7 @@ interface IField { val searchContent: String?; - fun fromField(obj : Any, field : Field, formField: FormField? = null) : IField; + fun fromField(obj : Any, field : Field, formField: FormField? = null, advanced: Boolean = false) : IField; fun setField(); fun setValue(value: Any); diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/FieldForm.kt b/app/src/main/java/com/futo/platformplayer/views/fields/FieldForm.kt index 1262e345..566a5024 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/FieldForm.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/FieldForm.kt @@ -37,6 +37,8 @@ class FieldForm : LinearLayout { private var _fields : List = arrayListOf(); + private var _showAdvancedSettings: Boolean = false; + constructor(context : Context, attrs : AttributeSet? = null) : super(context, attrs) { inflate(context, R.layout.field_form, this); _containerSearch = findViewById(R.id.container_search); @@ -58,11 +60,17 @@ class FieldForm : LinearLayout { if(field is GroupField) { updateSettingsVisibility(field); } else if(field is View && field.descriptor != null) { - val txt = field.searchContent?.lowercase(); - if(txt != null) { - val visible = isGroupMatch || txt.contains(query); - field.visibility = if (visible) View.VISIBLE else View.GONE; - groupVisible = groupVisible || visible; + if(field.isAdvanced && !_showAdvancedSettings) + { + field.visibility = View.GONE; + } + else { + val txt = field.searchContent?.lowercase(); + if (txt != null) { + val visible = isGroupMatch || txt.contains(query); + field.visibility = if (visible) View.VISIBLE else View.GONE; + groupVisible = groupVisible || visible; + } } } } @@ -71,6 +79,10 @@ class FieldForm : LinearLayout { } } + fun setShowAdvancedSettings(show: Boolean) { + _showAdvancedSettings = show; + updateSettingsVisibility(); + } fun setSearchQuery(query: String) { _editSearch.setText(query); updateSettingsVisibility(); @@ -92,13 +104,22 @@ class FieldForm : LinearLayout { throw java.lang.IllegalStateException("Only views can be IFields"); } + if(field is ToggleField && field.descriptor?.id == "advancedSettings") { + _showAdvancedSettings = field.value as Boolean; + } + _fieldsContainer.addView(field as View); field.onChanged.subscribe { a1, a2, _ -> + if(field is ToggleField && field.descriptor?.id == "advancedSettings") { + setShowAdvancedSettings((a2 as Boolean)); + } + onChanged.emit(a1, a2); }; } _fields = newFields; + updateSettingsVisibility(); onLoaded?.invoke(); } } @@ -267,10 +288,12 @@ class FieldForm : LinearLayout { for(prop in objFields) { prop.first.javaField!!.isAccessible = true; + val advanced = prop.first.hasAnnotation(); + val field = when(prop.second.type) { GROUP -> GroupField(context).fromField(obj, prop.first.javaField!!, prop.second); - DROPDOWN -> DropdownField(context).fromField(obj, prop.first.javaField!!, prop.second); - TOGGLE -> ToggleField(context).fromField(obj, prop.first.javaField!!, prop.second); + DROPDOWN -> DropdownField(context).fromField(obj, prop.first.javaField!!, prop.second, advanced); + TOGGLE -> ToggleField(context).fromField(obj, prop.first.javaField!!, prop.second, advanced); READONLYTEXT -> ReadOnlyTextField(context).fromField(obj, prop.first.javaField!!, prop.second); else -> throw java.lang.IllegalStateException("Unknown field type ${prop.second.type} for ${prop.second.title}") } diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/GroupField.kt b/app/src/main/java/com/futo/platformplayer/views/fields/GroupField.kt index 133fb788..9621285a 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/GroupField.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/GroupField.kt @@ -34,6 +34,7 @@ class GroupField : LinearLayout, IField { private val _container : LinearLayout; override var reference: Any? = null; + override var isAdvanced: Boolean = false; override val value: Any? = null; @@ -100,7 +101,7 @@ class GroupField : LinearLayout, IField { return this; } - override fun fromField(obj: Any, field: Field, formField: FormField?) : GroupField { + override fun fromField(obj: Any, field: Field, formField: FormField?, advanced: Boolean) : GroupField { this._field = field; this._obj = obj; diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/ReadOnlyTextField.kt b/app/src/main/java/com/futo/platformplayer/views/fields/ReadOnlyTextField.kt index d0cfb7dc..3fb78aeb 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/ReadOnlyTextField.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/ReadOnlyTextField.kt @@ -31,6 +31,7 @@ class ReadOnlyTextField : TableRow, IField { override val onChanged = Event3(); override var reference: Any? = null; + override var isAdvanced: Boolean = false; override val value: Any? = null; @@ -45,7 +46,7 @@ class ReadOnlyTextField : TableRow, IField { override fun setValue(value: Any) {} - override fun fromField(obj : Any, field : Field, formField: FormField?) : ReadOnlyTextField { + override fun fromField(obj : Any, field : Field, formField: FormField?, advanced: Boolean) : ReadOnlyTextField { this._field = field; this._obj = obj; diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt b/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt index 8e1cfbbb..c419092c 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt @@ -33,6 +33,7 @@ class ToggleField : TableRow, IField { private var _lastValue: Boolean = false; override var reference: Any? = null; + override var isAdvanced: Boolean = false; override val onChanged = Event3(); @@ -75,7 +76,7 @@ class ToggleField : TableRow, IField { return this; } - override fun fromField(obj : Any, field : Field, formField: FormField?) : ToggleField { + override fun fromField(obj : Any, field : Field, formField: FormField?, advanced: Boolean) : ToggleField { this._field = field; this._obj = obj; @@ -87,6 +88,12 @@ class ToggleField : TableRow, IField { else _title.text = field.name; + val advancedFieldAttr = field.getAnnotation(AdvancedField::class.java) + if(advancedFieldAttr != null || advanced) { + Logger.w("ToggleField", "Found advanced field: " + field.name); + isAdvanced = true; + } + if(attrField == null || attrField.subtitle == -1) _description.visibility = View.GONE; else { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f81f1642..d382860d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,8 @@ Channel Home Progress Bar + Advanced Settings + If advanced settings should be shown, this exposes additional settings to finetune your experience. If a historical progress bar should be shown Hide hidden from home in search Hide videos and creators hidden from home also in search results From e3e7b0c345ea646667ab2737f7d7a4d9c8a0d4de Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 4 Jun 2025 20:50:10 +0200 Subject: [PATCH 03/87] More advanced settings --- app/src/main/java/com/futo/platformplayer/Settings.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 363eb917..c25efdb0 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -226,6 +226,7 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5) var showHomeFiltersPluginNames: Boolean = false; + @AdvancedField @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) var previewFeedItems: Boolean = true; @@ -258,6 +259,7 @@ class Settings : FragmentedStorageFileJson() { @DropdownFieldOptionsId(R.array.feed_style) var searchFeedStyle: Int = 1; + @AdvancedField @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5) var previewFeedItems: Boolean = true; From ccc686ed50b3fa2cb7253f61d87cebdeedfbd336 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 4 Jun 2025 21:26:41 +0200 Subject: [PATCH 04/87] Downloads size ordering, Subsgroup removal on unsubscribe, multi-key like querying --- .../mainactivity/main/DownloadsFragment.kt | 6 ++++- .../platformplayer/states/StateHistory.kt | 9 ++++++-- .../states/StateSubscriptions.kt | 15 +++++++++++-- .../stores/db/ManagedDBStore.kt | 22 +++++++++++++++++++ app/src/main/res/values/strings.xml | 2 ++ 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt index 536700d8..0e429430 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt @@ -150,7 +150,7 @@ class DownloadsFragment : MainFragment() { spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.downloads_sortby_array)).also { it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple); }; - val options = listOf("nameAsc", "nameDesc", "downloadDateAsc", "downloadDateDesc", "releasedAsc", "releasedDesc"); + val options = listOf("nameAsc", "nameDesc", "downloadDateAsc", "downloadDateDesc", "releasedAsc", "releasedDesc", "sizeAsc", "sizeDesc"); spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) { when(pos) { @@ -160,6 +160,8 @@ class DownloadsFragment : MainFragment() { 3 -> ordering.setAndSave("downloadDateDesc") 4 -> ordering.setAndSave("releasedAsc") 5 -> ordering.setAndSave("releasedDesc") + 6 -> ordering.setAndSave("sizeAsc") + 7 -> ordering.setAndSave("sizeDesc") else -> ordering.setAndSave("") } updateContentFilters() @@ -257,6 +259,8 @@ class DownloadsFragment : MainFragment() { "nameDesc" -> vidsToReturn.sortedByDescending { it.name.lowercase() } "releasedAsc" -> vidsToReturn.sortedBy { it.datetime ?: OffsetDateTime.MAX } "releasedDesc" -> vidsToReturn.sortedByDescending { it.datetime ?: OffsetDateTime.MIN } + "sizeAsc" -> vidsToReturn.sortedBy { it.videoSource.sumOf { it.fileSize } + it.audioSource.sumOf { it.fileSize } } + "sizeDesc" -> vidsToReturn.sortedByDescending { it.videoSource.sumOf { it.fileSize } + it.audioSource.sumOf { it.fileSize } } else -> vidsToReturn } } diff --git a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt index 3047731d..97ab82c1 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt @@ -131,8 +131,13 @@ class StateHistory { fun getHistoryPager(): IPager { return _historyDBStore.getObjectPager(); } - fun getHistorySearchPager(query: String): IPager { - return _historyDBStore.queryLikeObjectPager(DBHistory.Index::name, "%${query}%", 10); + fun getHistorySearchPager(query: String, withAuthor: Boolean = false): IPager { + return if(!withAuthor) + _historyDBStore.queryLikeObjectPager(DBHistory.Index::name, "%${query}%", 10) + else + _historyDBStore.queryLikeObjectPager(DBHistory.Index::name, "%${query}%", 10) + //_historyDBStore.queryLike2ObjectPager(DBHistory.Index::name, DBHistory.Index::auth,"%${query}%", 10) + //TODO: See if we can include author name? } fun getHistoryIndexByUrl(url: String): DBHistory.Index? { return historyIndex[url]; diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt index 1d1acff6..60026ea6 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt @@ -329,8 +329,19 @@ class StateSubscriptions { } } - if(StateSubscriptionGroups.instance.hasSubscriptionGroup(sub.channel.url)) - getSubscriptionOtherOrCreate(sub.channel.url, sub.channel.name, sub.channel.thumbnail); + if(StateSubscriptionGroups.instance.hasSubscriptionGroup(sub.channel.url)) { + val subGroups = StateSubscriptionGroups.instance.getSubscriptionGroups().filter { it.urls.contains(sub.channel.url) }; + for(group in subGroups) { + group.urls.remove(sub.channel.url); + StateSubscriptionGroups.instance.updateSubscriptionGroup(group); + } + /* + getSubscriptionOtherOrCreate( + sub.channel.url, + sub.channel.name, + sub.channel.thumbnail + ); */ + } } return sub; } diff --git a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt index 2e493eef..6b9ed65f 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt @@ -274,10 +274,17 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} LIKE ? ${_orderSQL} LIMIT ? OFFSET ?"; val query = SimpleSQLiteQuery(queryStr, arrayOf(obj, pageSize, page * pageSize)); return deserializeIndexes(dbDaoBase.getMultiple(query)); + }fun queryLike2Page(field: String, field2: String, obj: String, page: Int, pageSize: Int): List { + val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} LIKE ? OR ${field2} LIKE ? ${_orderSQL} LIMIT ? OFFSET ?"; + val query = SimpleSQLiteQuery(queryStr, arrayOf(obj, obj, pageSize, page * pageSize)); + return deserializeIndexes(dbDaoBase.getMultiple(query)); } fun queryLikeObjectPage(field: String, obj: String, page: Int, pageSize: Int): List { return convertObjects(queryLikePage(field, obj, page, pageSize)); } + fun queryLike2ObjectPage(field: String, field2: String, obj: String, page: Int, pageSize: Int): List { + return convertObjects(queryLike2Page(field, field2, obj, page, pageSize)); + } //Query Page Objects @@ -336,6 +343,13 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA queryLikePage(field, obj, it - 1, pageSize); }); } + fun queryLike2Pager(field: KProperty<*>, field2: KProperty<*>, obj: String, pageSize: Int): IPager = queryLike2Pager(validateFieldName(field), validateFieldName(field2), obj, pageSize); + fun queryLike2Pager(field: String, field2: String, obj: String, pageSize: Int): IPager { + return AdhocPager({ + Logger.i("ManagedDBStore", "Next Page [query: ${obj}](${it}) ${pageSize}"); + queryLike2Page(field, field2, obj, it - 1, pageSize); + }); + } fun queryLikeObjectPager(field: KProperty<*>, obj: String, pageSize: Int): IPager = queryLikeObjectPager(validateFieldName(field), obj, pageSize); fun queryLikeObjectPager(field: String, obj: String, pageSize: Int): IPager { return AdhocPager({ @@ -344,6 +358,14 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA }); } + fun queryLike2ObjectPager(field: KProperty<*>, field2: KProperty<*>, obj: String, pageSize: Int): IPager = queryLike2ObjectPager(validateFieldName(field), validateFieldName(field2), obj, pageSize); + fun queryLike2ObjectPager(field: String, field2: String, obj: String, pageSize: Int): IPager { + return AdhocPager({ + Logger.i("ManagedDBStore", "Next Page [query: ${obj}](${it}) ${pageSize}"); + queryLike2ObjectPage(field, field2, obj, it - 1, pageSize); + }); + } + //Query Pager with convert fun queryPager(field: KProperty<*>, obj: Any, pageSize: Int, convert: (I)->X): IPager = queryPager(validateFieldName(field), obj, pageSize, convert); fun queryPager(field: String, obj: Any, pageSize: Int, convert: (I)->X): IPager { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d382860d..9acb16a1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -996,6 +996,8 @@ Download Date (Newest) Release Date (Oldest) Release Date (Newest) + Size (Smallest) + Size (Largest) Name (Ascending) From 919567dbdbb740566951cf13fe81e600f1b0f954 Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 5 Jun 2025 08:52:59 +0200 Subject: [PATCH 05/87] Made sync dialogs more robust. --- .../futo/platformplayer/states/StateSync.kt | 74 +++++++++++-------- app/src/stable/assets/sources/dailymotion | 2 +- app/src/stable/assets/sources/odysee | 2 +- app/src/stable/assets/sources/patreon | 2 +- app/src/stable/assets/sources/peertube | 2 +- app/src/stable/assets/sources/rumble | 2 +- app/src/stable/assets/sources/soundcloud | 2 +- app/src/stable/assets/sources/spotify | 2 +- app/src/stable/assets/sources/youtube | 2 +- .../unstable/assets/sources/apple-podcasts | 2 +- app/src/unstable/assets/sources/bitchute | 2 +- app/src/unstable/assets/sources/dailymotion | 2 +- app/src/unstable/assets/sources/odysee | 2 +- app/src/unstable/assets/sources/patreon | 2 +- app/src/unstable/assets/sources/peertube | 2 +- app/src/unstable/assets/sources/rumble | 2 +- app/src/unstable/assets/sources/soundcloud | 2 +- app/src/unstable/assets/sources/spotify | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 19 files changed, 61 insertions(+), 49 deletions(-) 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 fd08165c..2303e3bc 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt @@ -84,16 +84,20 @@ class StateSync { onUnauthorized = { sess -> StateApp.instance.scope.launch(Dispatchers.Main) { - UIDialogs.showConfirmationDialog( - context, - "Device Unauthorized: ${sess.displayName}", - action = { - Logger.i(TAG, "${sess.remotePublicKey} unauthorized received") - removeAuthorizedDevice(sess.remotePublicKey) - deviceRemoved.emit(sess.remotePublicKey) - }, - cancelAction = {} - ) + try { + UIDialogs.showConfirmationDialog( + context, + "Device Unauthorized: ${sess.displayName}", + action = { + Logger.i(TAG, "${sess.remotePublicKey} unauthorized received") + removeAuthorizedDevice(sess.remotePublicKey) + deviceRemoved.emit(sess.remotePublicKey) + }, + cancelAction = {} + ) + } catch (e: Throwable) { + Logger.e(TAG, "Failed to show unauthorized dialog.", e) + } } } @@ -118,30 +122,38 @@ class StateSync { if (scope != null && activity != null) { scope.launch(Dispatchers.Main) { - UIDialogs.showConfirmationDialog(activity, "Allow connection from $remotePublicKey?", - action = { - scope.launch(Dispatchers.IO) { - try { - callback(true) - Logger.i(TAG, "Connection authorized for $remotePublicKey by confirmation") + try { + UIDialogs.showConfirmationDialog( + activity, "Allow connection from $remotePublicKey?", + action = { + scope.launch(Dispatchers.IO) { + try { + callback(true) + Logger.i( + TAG, + "Connection authorized for $remotePublicKey by confirmation" + ) - activity.finish() - } catch (e: Throwable) { - Logger.e(TAG, "Failed to send authorize", e) + activity.finish() + } catch (e: Throwable) { + Logger.e(TAG, "Failed to send authorize", e) + } + } + }, + cancelAction = { + scope.launch(Dispatchers.IO) { + try { + callback(false) + Logger.i(TAG, "$remotePublicKey unauthorized received") + } catch (e: Throwable) { + Logger.w(TAG, "Failed to send unauthorize", e) + } } } - }, - cancelAction = { - scope.launch(Dispatchers.IO) { - try { - callback(false) - Logger.i(TAG, "$remotePublicKey unauthorized received") - } catch (e: Throwable) { - Logger.w(TAG, "Failed to send unauthorize", e) - } - } - } - ) + ) + } catch (e: Throwable) { + Logger.e(TAG, "Failed to show authorized dialog.", e) + } } } else { callback(false) diff --git a/app/src/stable/assets/sources/dailymotion b/app/src/stable/assets/sources/dailymotion index d1154300..ffd40f20 160000 --- a/app/src/stable/assets/sources/dailymotion +++ b/app/src/stable/assets/sources/dailymotion @@ -1 +1 @@ -Subproject commit d11543001150f96f3383d83fec3341d9321746b8 +Subproject commit ffd40f2006b9048690944e55688951a849f5a13a diff --git a/app/src/stable/assets/sources/odysee b/app/src/stable/assets/sources/odysee index 6ea9fa7e..215cd9bd 160000 --- a/app/src/stable/assets/sources/odysee +++ b/app/src/stable/assets/sources/odysee @@ -1 +1 @@ -Subproject commit 6ea9fa7e4c20ba8c89975ac835ccebdbd1184fc4 +Subproject commit 215cd9bd70d3cc68e25441f7696dcbe5beee2709 diff --git a/app/src/stable/assets/sources/patreon b/app/src/stable/assets/sources/patreon index d98c7f8a..e5dce87c 160000 --- a/app/src/stable/assets/sources/patreon +++ b/app/src/stable/assets/sources/patreon @@ -1 +1 @@ -Subproject commit d98c7f8aee36101d60a0c671d16a0800d5d715d0 +Subproject commit e5dce87c9d8ae7571df40a6fa252404e18b963f1 diff --git a/app/src/stable/assets/sources/peertube b/app/src/stable/assets/sources/peertube index 56bff391..6e7f943b 160000 --- a/app/src/stable/assets/sources/peertube +++ b/app/src/stable/assets/sources/peertube @@ -1 +1 @@ -Subproject commit 56bff391239e676e7d347ad3730df17795938a7b +Subproject commit 6e7f943b0ba56181ee503e1f2cb8349db1351553 diff --git a/app/src/stable/assets/sources/rumble b/app/src/stable/assets/sources/rumble index 3bbce817..932fdf78 160000 --- a/app/src/stable/assets/sources/rumble +++ b/app/src/stable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit 3bbce81794b175410fe79a12c28f9ba966de07da +Subproject commit 932fdf78dec23a132bedc8838185af9911452af5 diff --git a/app/src/stable/assets/sources/soundcloud b/app/src/stable/assets/sources/soundcloud index 54564312..f8234d6a 160000 --- a/app/src/stable/assets/sources/soundcloud +++ b/app/src/stable/assets/sources/soundcloud @@ -1 +1 @@ -Subproject commit 54564312683a0ae06d7085405478f96cade325e3 +Subproject commit f8234d6af8573414d07fd364bc136aa67ad0e379 diff --git a/app/src/stable/assets/sources/spotify b/app/src/stable/assets/sources/spotify index 1d884f50..47e76a96 160000 --- a/app/src/stable/assets/sources/spotify +++ b/app/src/stable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit 1d884f50abf88acf8fec77cefda2f6ffa0d932b5 +Subproject commit 47e76a96e5edcb265b99e2e30f178ba6234a6d2f diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 6d6838e2..a297a0a7 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 6d6838e2a472786e652521bd5fa0dff8f9364dc2 +Subproject commit a297a0a7884ea2cf1aa4c9798d72ee11d0038dce diff --git a/app/src/unstable/assets/sources/apple-podcasts b/app/src/unstable/assets/sources/apple-podcasts index 089987f0..9aa31c5e 160000 --- a/app/src/unstable/assets/sources/apple-podcasts +++ b/app/src/unstable/assets/sources/apple-podcasts @@ -1 +1 @@ -Subproject commit 089987f007319cf22972090a0cb09afd8c008adb +Subproject commit 9aa31c5e87c7957a6e7ef07b6a8f38b775c88d9a diff --git a/app/src/unstable/assets/sources/bitchute b/app/src/unstable/assets/sources/bitchute index b213f91c..b31ced36 160000 --- a/app/src/unstable/assets/sources/bitchute +++ b/app/src/unstable/assets/sources/bitchute @@ -1 +1 @@ -Subproject commit b213f91c0b0e61622dd7e3c0a7b5f6a46973321c +Subproject commit b31ced36b9faaa535fb13a5873cdeb1c89d55859 diff --git a/app/src/unstable/assets/sources/dailymotion b/app/src/unstable/assets/sources/dailymotion index d1154300..ffd40f20 160000 --- a/app/src/unstable/assets/sources/dailymotion +++ b/app/src/unstable/assets/sources/dailymotion @@ -1 +1 @@ -Subproject commit d11543001150f96f3383d83fec3341d9321746b8 +Subproject commit ffd40f2006b9048690944e55688951a849f5a13a diff --git a/app/src/unstable/assets/sources/odysee b/app/src/unstable/assets/sources/odysee index 6ea9fa7e..215cd9bd 160000 --- a/app/src/unstable/assets/sources/odysee +++ b/app/src/unstable/assets/sources/odysee @@ -1 +1 @@ -Subproject commit 6ea9fa7e4c20ba8c89975ac835ccebdbd1184fc4 +Subproject commit 215cd9bd70d3cc68e25441f7696dcbe5beee2709 diff --git a/app/src/unstable/assets/sources/patreon b/app/src/unstable/assets/sources/patreon index d98c7f8a..e5dce87c 160000 --- a/app/src/unstable/assets/sources/patreon +++ b/app/src/unstable/assets/sources/patreon @@ -1 +1 @@ -Subproject commit d98c7f8aee36101d60a0c671d16a0800d5d715d0 +Subproject commit e5dce87c9d8ae7571df40a6fa252404e18b963f1 diff --git a/app/src/unstable/assets/sources/peertube b/app/src/unstable/assets/sources/peertube index 56bff391..6e7f943b 160000 --- a/app/src/unstable/assets/sources/peertube +++ b/app/src/unstable/assets/sources/peertube @@ -1 +1 @@ -Subproject commit 56bff391239e676e7d347ad3730df17795938a7b +Subproject commit 6e7f943b0ba56181ee503e1f2cb8349db1351553 diff --git a/app/src/unstable/assets/sources/rumble b/app/src/unstable/assets/sources/rumble index 3bbce817..932fdf78 160000 --- a/app/src/unstable/assets/sources/rumble +++ b/app/src/unstable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit 3bbce81794b175410fe79a12c28f9ba966de07da +Subproject commit 932fdf78dec23a132bedc8838185af9911452af5 diff --git a/app/src/unstable/assets/sources/soundcloud b/app/src/unstable/assets/sources/soundcloud index 54564312..f8234d6a 160000 --- a/app/src/unstable/assets/sources/soundcloud +++ b/app/src/unstable/assets/sources/soundcloud @@ -1 +1 @@ -Subproject commit 54564312683a0ae06d7085405478f96cade325e3 +Subproject commit f8234d6af8573414d07fd364bc136aa67ad0e379 diff --git a/app/src/unstable/assets/sources/spotify b/app/src/unstable/assets/sources/spotify index 1d884f50..47e76a96 160000 --- a/app/src/unstable/assets/sources/spotify +++ b/app/src/unstable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit 1d884f50abf88acf8fec77cefda2f6ffa0d932b5 +Subproject commit 47e76a96e5edcb265b99e2e30f178ba6234a6d2f diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 6d6838e2..a297a0a7 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 6d6838e2a472786e652521bd5fa0dff8f9364dc2 +Subproject commit a297a0a7884ea2cf1aa4c9798d72ee11d0038dce From cbef605f22a1172892b5c8b2a0398ccb3efd4b2a Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 5 Jun 2025 08:57:33 +0200 Subject: [PATCH 06/87] Updated plugins. --- app/src/stable/assets/sources/crunchyroll | 2 +- app/src/stable/assets/sources/dailymotion | 2 +- app/src/stable/assets/sources/odysee | 2 +- app/src/stable/assets/sources/patreon | 2 +- app/src/stable/assets/sources/peertube | 2 +- app/src/stable/assets/sources/rumble | 2 +- app/src/stable/assets/sources/soundcloud | 2 +- app/src/stable/assets/sources/spotify | 2 +- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/apple-podcasts | 2 +- app/src/unstable/assets/sources/bitchute | 2 +- app/src/unstable/assets/sources/crunchyroll | 2 +- app/src/unstable/assets/sources/dailymotion | 2 +- app/src/unstable/assets/sources/odysee | 2 +- app/src/unstable/assets/sources/patreon | 2 +- app/src/unstable/assets/sources/peertube | 2 +- app/src/unstable/assets/sources/rumble | 2 +- app/src/unstable/assets/sources/soundcloud | 2 +- app/src/unstable/assets/sources/spotify | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/src/stable/assets/sources/crunchyroll b/app/src/stable/assets/sources/crunchyroll index 1aa91f21..534bded3 160000 --- a/app/src/stable/assets/sources/crunchyroll +++ b/app/src/stable/assets/sources/crunchyroll @@ -1 +1 @@ -Subproject commit 1aa91f216c0a87604aed1669b63b7830e4288630 +Subproject commit 534bded369009134a1da90ac69a2946b82d34344 diff --git a/app/src/stable/assets/sources/dailymotion b/app/src/stable/assets/sources/dailymotion index ffd40f20..d1154300 160000 --- a/app/src/stable/assets/sources/dailymotion +++ b/app/src/stable/assets/sources/dailymotion @@ -1 +1 @@ -Subproject commit ffd40f2006b9048690944e55688951a849f5a13a +Subproject commit d11543001150f96f3383d83fec3341d9321746b8 diff --git a/app/src/stable/assets/sources/odysee b/app/src/stable/assets/sources/odysee index 215cd9bd..6ea9fa7e 160000 --- a/app/src/stable/assets/sources/odysee +++ b/app/src/stable/assets/sources/odysee @@ -1 +1 @@ -Subproject commit 215cd9bd70d3cc68e25441f7696dcbe5beee2709 +Subproject commit 6ea9fa7e4c20ba8c89975ac835ccebdbd1184fc4 diff --git a/app/src/stable/assets/sources/patreon b/app/src/stable/assets/sources/patreon index e5dce87c..d98c7f8a 160000 --- a/app/src/stable/assets/sources/patreon +++ b/app/src/stable/assets/sources/patreon @@ -1 +1 @@ -Subproject commit e5dce87c9d8ae7571df40a6fa252404e18b963f1 +Subproject commit d98c7f8aee36101d60a0c671d16a0800d5d715d0 diff --git a/app/src/stable/assets/sources/peertube b/app/src/stable/assets/sources/peertube index 6e7f943b..56bff391 160000 --- a/app/src/stable/assets/sources/peertube +++ b/app/src/stable/assets/sources/peertube @@ -1 +1 @@ -Subproject commit 6e7f943b0ba56181ee503e1f2cb8349db1351553 +Subproject commit 56bff391239e676e7d347ad3730df17795938a7b diff --git a/app/src/stable/assets/sources/rumble b/app/src/stable/assets/sources/rumble index 932fdf78..3bbce817 160000 --- a/app/src/stable/assets/sources/rumble +++ b/app/src/stable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit 932fdf78dec23a132bedc8838185af9911452af5 +Subproject commit 3bbce81794b175410fe79a12c28f9ba966de07da diff --git a/app/src/stable/assets/sources/soundcloud b/app/src/stable/assets/sources/soundcloud index f8234d6a..54564312 160000 --- a/app/src/stable/assets/sources/soundcloud +++ b/app/src/stable/assets/sources/soundcloud @@ -1 +1 @@ -Subproject commit f8234d6af8573414d07fd364bc136aa67ad0e379 +Subproject commit 54564312683a0ae06d7085405478f96cade325e3 diff --git a/app/src/stable/assets/sources/spotify b/app/src/stable/assets/sources/spotify index 47e76a96..1d884f50 160000 --- a/app/src/stable/assets/sources/spotify +++ b/app/src/stable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit 47e76a96e5edcb265b99e2e30f178ba6234a6d2f +Subproject commit 1d884f50abf88acf8fec77cefda2f6ffa0d932b5 diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index a297a0a7..6d6838e2 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit a297a0a7884ea2cf1aa4c9798d72ee11d0038dce +Subproject commit 6d6838e2a472786e652521bd5fa0dff8f9364dc2 diff --git a/app/src/unstable/assets/sources/apple-podcasts b/app/src/unstable/assets/sources/apple-podcasts index 9aa31c5e..089987f0 160000 --- a/app/src/unstable/assets/sources/apple-podcasts +++ b/app/src/unstable/assets/sources/apple-podcasts @@ -1 +1 @@ -Subproject commit 9aa31c5e87c7957a6e7ef07b6a8f38b775c88d9a +Subproject commit 089987f007319cf22972090a0cb09afd8c008adb diff --git a/app/src/unstable/assets/sources/bitchute b/app/src/unstable/assets/sources/bitchute index b31ced36..b213f91c 160000 --- a/app/src/unstable/assets/sources/bitchute +++ b/app/src/unstable/assets/sources/bitchute @@ -1 +1 @@ -Subproject commit b31ced36b9faaa535fb13a5873cdeb1c89d55859 +Subproject commit b213f91c0b0e61622dd7e3c0a7b5f6a46973321c diff --git a/app/src/unstable/assets/sources/crunchyroll b/app/src/unstable/assets/sources/crunchyroll index 1aa91f21..534bded3 160000 --- a/app/src/unstable/assets/sources/crunchyroll +++ b/app/src/unstable/assets/sources/crunchyroll @@ -1 +1 @@ -Subproject commit 1aa91f216c0a87604aed1669b63b7830e4288630 +Subproject commit 534bded369009134a1da90ac69a2946b82d34344 diff --git a/app/src/unstable/assets/sources/dailymotion b/app/src/unstable/assets/sources/dailymotion index ffd40f20..d1154300 160000 --- a/app/src/unstable/assets/sources/dailymotion +++ b/app/src/unstable/assets/sources/dailymotion @@ -1 +1 @@ -Subproject commit ffd40f2006b9048690944e55688951a849f5a13a +Subproject commit d11543001150f96f3383d83fec3341d9321746b8 diff --git a/app/src/unstable/assets/sources/odysee b/app/src/unstable/assets/sources/odysee index 215cd9bd..6ea9fa7e 160000 --- a/app/src/unstable/assets/sources/odysee +++ b/app/src/unstable/assets/sources/odysee @@ -1 +1 @@ -Subproject commit 215cd9bd70d3cc68e25441f7696dcbe5beee2709 +Subproject commit 6ea9fa7e4c20ba8c89975ac835ccebdbd1184fc4 diff --git a/app/src/unstable/assets/sources/patreon b/app/src/unstable/assets/sources/patreon index e5dce87c..d98c7f8a 160000 --- a/app/src/unstable/assets/sources/patreon +++ b/app/src/unstable/assets/sources/patreon @@ -1 +1 @@ -Subproject commit e5dce87c9d8ae7571df40a6fa252404e18b963f1 +Subproject commit d98c7f8aee36101d60a0c671d16a0800d5d715d0 diff --git a/app/src/unstable/assets/sources/peertube b/app/src/unstable/assets/sources/peertube index 6e7f943b..56bff391 160000 --- a/app/src/unstable/assets/sources/peertube +++ b/app/src/unstable/assets/sources/peertube @@ -1 +1 @@ -Subproject commit 6e7f943b0ba56181ee503e1f2cb8349db1351553 +Subproject commit 56bff391239e676e7d347ad3730df17795938a7b diff --git a/app/src/unstable/assets/sources/rumble b/app/src/unstable/assets/sources/rumble index 932fdf78..3bbce817 160000 --- a/app/src/unstable/assets/sources/rumble +++ b/app/src/unstable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit 932fdf78dec23a132bedc8838185af9911452af5 +Subproject commit 3bbce81794b175410fe79a12c28f9ba966de07da diff --git a/app/src/unstable/assets/sources/soundcloud b/app/src/unstable/assets/sources/soundcloud index f8234d6a..54564312 160000 --- a/app/src/unstable/assets/sources/soundcloud +++ b/app/src/unstable/assets/sources/soundcloud @@ -1 +1 @@ -Subproject commit f8234d6af8573414d07fd364bc136aa67ad0e379 +Subproject commit 54564312683a0ae06d7085405478f96cade325e3 diff --git a/app/src/unstable/assets/sources/spotify b/app/src/unstable/assets/sources/spotify index 47e76a96..1d884f50 160000 --- a/app/src/unstable/assets/sources/spotify +++ b/app/src/unstable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit 47e76a96e5edcb265b99e2e30f178ba6234a6d2f +Subproject commit 1d884f50abf88acf8fec77cefda2f6ffa0d932b5 diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index a297a0a7..6d6838e2 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit a297a0a7884ea2cf1aa4c9798d72ee11d0038dce +Subproject commit 6d6838e2a472786e652521bd5fa0dff8f9364dc2 From fa8d17510149f9ed25173e044034c57c117220da Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 5 Jun 2025 09:58:10 +0200 Subject: [PATCH 07/87] Fixed issue in base64 encoding. --- .../platformplayer/sync/internal/Channel.kt | 6 ++-- .../sync/internal/SyncService.kt | 7 ++-- .../sync/internal/SyncSocketSession.kt | 34 +++++++++---------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/Channel.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/Channel.kt index e17b6309..4e9b2653 100644 --- a/app/src/main/java/com/futo/platformplayer/sync/internal/Channel.kt +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/Channel.kt @@ -75,7 +75,7 @@ class ChannelRelayed( private var handshakeState: HandshakeState? = if (initiator) { HandshakeState(SyncService.protocolName, HandshakeState.INITIATOR).apply { localKeyPair.copyFrom(this@ChannelRelayed.localKeyPair) - remotePublicKey.setPublicKey(Base64.getDecoder().decode(publicKey), 0) + remotePublicKey.setPublicKey(publicKey.base64ToByteArray(), 0) } } else { HandshakeState(SyncService.protocolName, HandshakeState.RESPONDER).apply { @@ -177,7 +177,7 @@ class ChannelRelayed( this.remoteVersion = remoteVersion val remoteKeyBytes = ByteArray(handshakeState!!.remotePublicKey.publicKeyLength) handshakeState!!.remotePublicKey.getPublicKey(remoteKeyBytes, 0) - this.remotePublicKey = Base64.getEncoder().encodeToString(remoteKeyBytes) + this.remotePublicKey = remoteKeyBytes.toBase64() handshakeState?.destroy() handshakeState = null this.transport = transport @@ -316,7 +316,7 @@ class ChannelRelayed( val channelMessage = ByteArray(1024) val channelBytesWritten = handshakeState!!.writeMessage(channelMessage, 0, null, 0, 0) - val publicKeyBytes = Base64.getDecoder().decode(publicKey) + val publicKeyBytes = publicKey.base64ToByteArray() if (publicKeyBytes.size != 32) throw IllegalArgumentException("Public key must be 32 bytes") val (pairingMessageLength, pairingMessage) = if (pairingCode != null) { diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt index 5e5ad7de..eb77c02d 100644 --- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt @@ -13,6 +13,7 @@ import com.futo.platformplayer.noise.protocol.DHState import com.futo.platformplayer.noise.protocol.Noise import com.futo.platformplayer.states.StateSync import com.futo.polycentric.core.base64ToByteArray +import com.futo.polycentric.core.base64UrlToByteArray import com.futo.polycentric.core.toBase64 import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -98,7 +99,7 @@ class SyncService( override fun onServiceLost(service: NsdServiceInfo) { Log.e(TAG, "service lost: $service") val urlSafePkey = service.attributes["pk"]?.decodeToString() ?: return - val pkey = Base64.getDecoder().decode(urlSafePkey.replace('-', '+').replace('_', '/')).toBase64() + val pkey = urlSafePkey.base64UrlToByteArray().toBase64() synchronized(_mdnsCache) { _mdnsCache.remove(pkey) } @@ -128,7 +129,7 @@ class SyncService( } val urlSafePkey = attributes.get("pk")?.decodeToString() ?: return - val pkey = Base64.getDecoder().decode(urlSafePkey.replace('-', '+').replace('_', '/')).toBase64() + val pkey = urlSafePkey.base64UrlToByteArray().toBase64() val syncDeviceInfo = SyncDeviceInfo(pkey, adrs.map { it.hostAddress }.toTypedArray(), port, null) synchronized(_mdnsCache) { @@ -157,7 +158,7 @@ class SyncService( override fun onServiceLost() { Log.v(TAG, "onServiceLost: $service") val urlSafePkey = service.attributes["pk"]?.decodeToString() ?: return - val pkey = Base64.getDecoder().decode(urlSafePkey.replace('-', '+').replace('_', '/')).toBase64() + val pkey = urlSafePkey.base64UrlToByteArray().toBase64() synchronized(_mdnsCache) { _mdnsCache.remove(pkey) } diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt index cb67f934..e3b7fb30 100644 --- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt @@ -123,7 +123,7 @@ class SyncSocketSession { val localPublicKey = ByteArray(localKeyPair.publicKeyLength) localKeyPair.getPublicKey(localPublicKey, 0) - _localPublicKey = Base64.getEncoder().encodeToString(localPublicKey) + _localPublicKey = localPublicKey.toBase64() } fun startAsInitiator(remotePublicKey: String, appId: UInt = 0u, pairingCode: String? = null) { @@ -253,14 +253,14 @@ class SyncSocketSession { val initiator = HandshakeState(SyncService.protocolName, HandshakeState.INITIATOR) initiator.localKeyPair.copyFrom(_localKeyPair) - initiator.remotePublicKey.setPublicKey(Base64.getDecoder().decode(remotePublicKey), 0) + initiator.remotePublicKey.setPublicKey(remotePublicKey.base64ToByteArray(), 0) initiator.start() val pairingMessage: ByteArray val pairingMessageLength: Int if (pairingCode != null) { val pairingHandshake = HandshakeState(SyncSocketSession.nProtocolName, HandshakeState.INITIATOR) - pairingHandshake.remotePublicKey.setPublicKey(Base64.getDecoder().decode(remotePublicKey), 0) + pairingHandshake.remotePublicKey.setPublicKey(remotePublicKey.base64ToByteArray(), 0) pairingHandshake.start() val pairingCodeBytes = pairingCode.toByteArray(Charsets.UTF_8) val pairingBuffer = ByteArray(512) @@ -299,7 +299,7 @@ class SyncSocketSession { _cipherStatePair = initiator.split() val remoteKeyBytes = ByteArray(initiator.remotePublicKey.publicKeyLength) initiator.remotePublicKey.getPublicKey(remoteKeyBytes, 0) - _remotePublicKey = Base64.getEncoder().encodeToString(remoteKeyBytes).base64ToByteArray().toBase64() + _remotePublicKey = remoteKeyBytes.toBase64() } private fun handshakeAsResponder(): Boolean { @@ -516,7 +516,7 @@ class SyncSocketSession { return } val channelHandshakeMessage = ByteArray(channelMessageLength).also { data.get(it) } - val publicKey = Base64.getEncoder().encodeToString(publicKeyBytes) + val publicKey = publicKeyBytes.toBase64() val pairingCode = if (pairingMessageLength > 0) { val pairingProtocol = HandshakeState(SyncSocketSession.nProtocolName, HandshakeState.RESPONDER).apply { localKeyPair.copyFrom(_localKeyPair) @@ -671,7 +671,7 @@ class SyncSocketSession { val records = mutableMapOf>() repeat(recordCount) { val publisherBytes = ByteArray(32).also { data.get(it) } - val publisher = Base64.getEncoder().encodeToString(publisherBytes) + val publisher = publisherBytes.toBase64() val blobLength = data.int val encryptedBlob = ByteArray(blobLength).also { data.get(it) } val timestamp = data.long @@ -712,7 +712,7 @@ class SyncSocketSession { val numResponses = data.get().toInt() val result = mutableMapOf() repeat(numResponses) { - val publicKey = Base64.getEncoder().encodeToString(ByteArray(32).also { data.get(it) }) + val publicKey = ByteArray(32).also { data.get(it) }.toBase64() val status = data.get().toInt() if (status == 0) { val infoSize = data.int @@ -994,7 +994,7 @@ class SyncSocketSession { val deferred = CompletableDeferred() _pendingConnectionInfoRequests[requestId] = deferred try { - val publicKeyBytes = Base64.getDecoder().decode(publicKey) + val publicKeyBytes = publicKey.base64ToByteArray() if (publicKeyBytes.size != 32) throw IllegalArgumentException("Public key must be 32 bytes") val packet = ByteBuffer.allocate(4 + 32).order(ByteOrder.LITTLE_ENDIAN) packet.putInt(requestId) @@ -1017,7 +1017,7 @@ class SyncSocketSession { packet.putInt(requestId) packet.put(publicKeys.size.toByte()) for (pk in publicKeys) { - val pkBytes = Base64.getDecoder().decode(pk) + val pkBytes = pk.base64ToByteArray() if (pkBytes.size != 32) throw IllegalArgumentException("Invalid public key length for $pk") packet.put(pkBytes) } @@ -1125,7 +1125,7 @@ class SyncSocketSession { publishBytes.put(authorizedKeys.size.toByte()) for (key in authorizedKeys) { - val publicKeyBytes = Base64.getDecoder().decode(key) + val publicKeyBytes = key.base64ToByteArray() if (publicKeyBytes.size != 32) throw IllegalArgumentException("Public key must be 32 bytes") val protocol = HandshakeState(SyncSocketSession.nProtocolName, HandshakeState.INITIATOR) @@ -1183,7 +1183,7 @@ class SyncSocketSession { packet.put(consumerPublicKeys.size.toByte()) for (consumer in consumerPublicKeys) { - val consumerBytes = Base64.getDecoder().decode(consumer) + val consumerBytes = consumer.base64ToByteArray() if (consumerBytes.size != 32) throw IllegalArgumentException("Consumer public key must be 32 bytes") packet.put(consumerBytes) val protocol = HandshakeState(SyncSocketSession.nProtocolName, HandshakeState.INITIATOR).apply { @@ -1222,7 +1222,7 @@ class SyncSocketSession { val deferred = CompletableDeferred?>() _pendingGetRecordRequests[requestId] = deferred try { - val publisherBytes = Base64.getDecoder().decode(publisherPublicKey) + val publisherBytes = publisherPublicKey.base64ToByteArray() if (publisherBytes.size != 32) throw IllegalArgumentException("Publisher public key must be 32 bytes") val keyBytes = key.toByteArray(Charsets.UTF_8) val packet = ByteBuffer.allocate(4 + 32 + 1 + keyBytes.size).order(ByteOrder.LITTLE_ENDIAN) @@ -1253,7 +1253,7 @@ class SyncSocketSession { packet.put(keyBytes) packet.put(publisherPublicKeys.size.toByte()) for (publisher in publisherPublicKeys) { - val bytes = Base64.getDecoder().decode(publisher) + val bytes = publisher.base64ToByteArray() if (bytes.size != 32) throw IllegalArgumentException("Publisher public key must be 32 bytes") packet.put(bytes) } @@ -1272,9 +1272,9 @@ class SyncSocketSession { val deferred = CompletableDeferred() _pendingDeleteRequests[requestId] = deferred try { - val publisherBytes = Base64.getDecoder().decode(publisherPublicKey) + val publisherBytes = publisherPublicKey.base64ToByteArray() if (publisherBytes.size != 32) throw IllegalArgumentException("Publisher public key must be 32 bytes") - val consumerBytes = Base64.getDecoder().decode(consumerPublicKey) + val consumerBytes = consumerPublicKey.base64ToByteArray() if (consumerBytes.size != 32) throw IllegalArgumentException("Consumer public key must be 32 bytes") val packetSize = 4 + 32 + 32 + 1 + keys.sumOf { 1 + it.toByteArray(Charsets.UTF_8).size } val packet = ByteBuffer.allocate(packetSize).order(ByteOrder.LITTLE_ENDIAN) @@ -1301,9 +1301,9 @@ class SyncSocketSession { val deferred = CompletableDeferred>>() _pendingListKeysRequests[requestId] = deferred try { - val publisherBytes = Base64.getDecoder().decode(publisherPublicKey) + val publisherBytes = publisherPublicKey.base64ToByteArray() if (publisherBytes.size != 32) throw IllegalArgumentException("Publisher public key must be 32 bytes") - val consumerBytes = Base64.getDecoder().decode(consumerPublicKey) + val consumerBytes = consumerPublicKey.base64ToByteArray() if (consumerBytes.size != 32) throw IllegalArgumentException("Consumer public key must be 32 bytes") val packet = ByteBuffer.allocate(4 + 32 + 32).order(ByteOrder.LITTLE_ENDIAN) packet.putInt(requestId) From 4d170db5e0c37fea19a0cd53b2c25f96afbce7ff Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 5 Jun 2025 10:31:13 +0200 Subject: [PATCH 08/87] Improvements to connection publishing for sync. --- .../java/com/futo/platformplayer/Utility.kt | 27 +++++++++++++++++++ .../sync/internal/SyncService.kt | 20 +++++++++----- .../sync/internal/SyncSocketSession.kt | 22 +++++---------- 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Utility.kt b/app/src/main/java/com/futo/platformplayer/Utility.kt index bfa7925b..e2868ed6 100644 --- a/app/src/main/java/com/futo/platformplayer/Utility.kt +++ b/app/src/main/java/com/futo/platformplayer/Utility.kt @@ -339,6 +339,33 @@ fun ByteArray.fromGzip(): ByteArray { return outputStream.toByteArray() } +fun findCandidateAddresses(): List { + val candidates = NetworkInterface.getNetworkInterfaces() + .toList() + .asSequence() + .filter(::isUsableInterface) + .flatMap { nif -> + nif.interfaceAddresses + .asSequence() + .mapNotNull { ia -> + ia.address.takeIf(::isUsableAddress)?.let { addr -> + nif to ia + } + } + } + .toList() + + return candidates + .sortedWith( + compareBy>( + { addressScore(it.second.address) }, + { interfaceScore(it.first) }, + { -it.second.networkPrefixLength.toInt() }, + { -it.first.mtu } + ) + ).map { it.second.address } +} + fun findPreferredAddress(): InetAddress? { val candidates = NetworkInterface.getNetworkInterfaces() .toList() diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt index eb77c02d..9281f144 100644 --- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt @@ -328,7 +328,7 @@ class SyncService( val now = System.currentTimeMillis() synchronized(_mdnsCache) { for ((pkey, info) in _mdnsCache) { - if (!database.isAuthorized(pkey) || isConnected(pkey)) continue + if (!database.isAuthorized(pkey) || getLinkType(pkey) == LinkType.Direct) continue val last = synchronized(_lastConnectTimesMdns) { _lastConnectTimesMdns[pkey] ?: 0L @@ -360,8 +360,8 @@ class SyncService( while (_started) { val authorizedDevices = database.getAllAuthorizedDevices() ?: arrayOf() val addressesToConnect = authorizedDevices.mapNotNull { - val connected = isConnected(it) - if (connected) { + val connectedDirectly = getLinkType(it) == LinkType.Direct + if (connectedDirectly) { return@mapNotNull null } @@ -468,8 +468,13 @@ class SyncService( while (_started && !socketClosed) { val unconnectedAuthorizedDevices = database.getAllAuthorizedDevices() - ?.filter { !isConnected(it) }?.toTypedArray() - ?: arrayOf() + ?.filter { + if (Settings.instance.synchronization.connectLocalDirectThroughRelay) { + getLinkType(it) != LinkType.Direct + } else { + !isConnected(it) + } + }?.toTypedArray() ?: arrayOf() relaySession.publishConnectionInformation( unconnectedAuthorizedDevices, settings.listenerPort, @@ -497,7 +502,7 @@ class SyncService( val potentialLocalAddresses = connectionInfo.ipv4Addresses .filter { it != connectionInfo.remoteIp } - if (connectionInfo.allowLocalDirect && Settings.instance.synchronization.connectLocalDirectThroughRelay) { + if (getLinkType(targetKey) != LinkType.Direct && connectionInfo.allowLocalDirect && Settings.instance.synchronization.connectLocalDirectThroughRelay) { Thread { try { Log.v( @@ -529,7 +534,7 @@ class SyncService( // TODO: Implement hole punching if needed } - if (connectionInfo.allowRemoteRelayed && Settings.instance.synchronization.connectThroughRelay) { + if (getLinkType(targetKey) == LinkType.None && connectionInfo.allowRemoteRelayed && Settings.instance.synchronization.connectThroughRelay) { try { Logger.v( TAG, @@ -741,6 +746,7 @@ class SyncService( ) } + fun getLinkType(publicKey: String): LinkType = synchronized(_sessions) { _sessions[publicKey]?.linkType ?: LinkType.None } fun isConnected(publicKey: String): Boolean = synchronized(_sessions) { _sessions[publicKey]?.connected ?: false } fun isAuthorized(publicKey: String): Boolean = database.isAuthorized(publicKey) fun getSession(publicKey: String): SyncSession? = synchronized(_sessions) { _sessions[publicKey] } diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt index e3b7fb30..8e75e5de 100644 --- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.sync.internal import android.os.Build import com.futo.platformplayer.ensureNotMainThread +import com.futo.platformplayer.findCandidateAddresses import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.noise.protocol.CipherStatePair import com.futo.platformplayer.noise.protocol.DHState @@ -1078,20 +1079,9 @@ class SyncSocketSession { ) { if (authorizedKeys.size > 255) throw IllegalArgumentException("Number of authorized keys exceeds 255") - val ipv4Addresses = mutableListOf() - val ipv6Addresses = mutableListOf() - for (nic in NetworkInterface.getNetworkInterfaces()) { - if (nic.isUp) { - for (addr in nic.inetAddresses) { - if (!addr.isLoopbackAddress) { - when (addr) { - is Inet4Address -> ipv4Addresses.add(addr.hostAddress) - is Inet6Address -> ipv6Addresses.add(addr.hostAddress) - } - } - } - } - } + val candidateAddresses = findCandidateAddresses() + val ipv4Addresses = candidateAddresses.filterIsInstance() + val ipv6Addresses = candidateAddresses.filterIsInstance() val deviceName = getDeviceName() val nameBytes = getLimitedUtf8Bytes(deviceName, 255) @@ -1103,12 +1093,12 @@ class SyncSocketSession { data.put(nameBytes) data.put(ipv4Addresses.size.toByte()) for (addr in ipv4Addresses) { - val addrBytes = InetAddress.getByName(addr).address + val addrBytes = addr.address data.put(addrBytes) } data.put(ipv6Addresses.size.toByte()) for (addr in ipv6Addresses) { - val addrBytes = InetAddress.getByName(addr).address + val addrBytes = addr.address data.put(addrBytes) } data.put(if (allowLocalDirect) 1 else 0) From 7b53315046d8e866c71f4d0a677138c5cd72d92f Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 5 Jun 2025 10:38:49 +0200 Subject: [PATCH 09/87] Another fix for connection robustness. --- .../java/com/futo/platformplayer/sync/internal/SyncService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt index 9281f144..c330062c 100644 --- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt @@ -534,7 +534,7 @@ class SyncService( // TODO: Implement hole punching if needed } - if (getLinkType(targetKey) == LinkType.None && connectionInfo.allowRemoteRelayed && Settings.instance.synchronization.connectThroughRelay) { + if (!isConnected(targetKey) && connectionInfo.allowRemoteRelayed && Settings.instance.synchronization.connectThroughRelay) { try { Logger.v( TAG, From 401999b5ea8f059ae24bb5ca941f23cc4199013a Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 5 Jun 2025 10:45:36 +0200 Subject: [PATCH 10/87] Fixed exception in sync. --- .../futo/platformplayer/sync/internal/SyncSocketSession.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt index 8e75e5de..35437a7b 100644 --- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt @@ -814,7 +814,7 @@ class SyncSocketSession { return } val decryptedPayload = channel.decrypt(data) - val errorCode = SyncErrorCode.entries.find { it.value == decryptedPayload.int } ?: SyncErrorCode.ConnectionClosed + val errorCode = decryptedPayload.int Logger.e(TAG, "Received relayed error (errorCode = $errorCode) on connectionId $connectionId, closing") channel.close() _channels.remove(connectionId) @@ -825,7 +825,7 @@ class SyncSocketSession { return } val connectionId = data.long - val errorCode = SyncErrorCode.entries.find { it.value == data.int } ?: SyncErrorCode.ConnectionClosed + val errorCode = data.int val channel = _channels[connectionId] ?: run { Logger.e(TAG, "Received error code $errorCode for non-existent channel with connectionId $connectionId") return From f4f1470153b5a3ef4357581ed27bcd861187069d Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 5 Jun 2025 10:58:32 +0200 Subject: [PATCH 11/87] Increased connect timeout. --- .../main/java/com/futo/platformplayer/Extensions_Network.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt index 9fe21ec8..f0137f2d 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt @@ -219,9 +219,7 @@ private fun ByteArray.toInetAddress(): InetAddress { fun getConnectedSocket(attemptAddresses: List, port: Int): Socket? { ensureNotMainThread() - val timeout = 2000 - - + val timeout = 10000 val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance() else attemptAddresses; if(addresses.isEmpty()) throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})"); From 40c195d4a00442533d556b8ac712241bfc9c58ce Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 5 Jun 2025 13:14:57 +0200 Subject: [PATCH 12/87] Crashfix on stopping StateSync #2302 --- .../com/futo/platformplayer/sync/internal/SyncService.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt index c330062c..23ea684b 100644 --- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt @@ -803,8 +803,12 @@ class SyncService( _relaySession = null _serverSocket?.close() _serverSocket = null + + synchronized(_sessions) { + _sessions.values.toList() + }.forEach { it.close() } + synchronized(_sessions) { - _sessions.values.forEach { it.close() } _sessions.clear() } } From c959b973fc3565900251af98ef7a65fc0cfc7f4d Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 5 Jun 2025 13:17:15 +0200 Subject: [PATCH 13/87] Crashfix related to PiP #2041. --- .../mainactivity/main/VideoDetailFragment.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt index fd3319f0..53886862 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt @@ -467,10 +467,14 @@ class VideoDetailFragment() : MainFragment() { activity?.enterPictureInPictureMode(params); } fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, isStop: Boolean, newConfig: Configuration) { - if (isInPictureInPictureMode) { - _viewDetail?.startPictureInPicture(); - } else if (isInPictureInPicture) { - leavePictureInPictureMode(isStop); + try { + if (isInPictureInPictureMode) { + _viewDetail?.startPictureInPicture(); + } else if (isInPictureInPicture) { + leavePictureInPictureMode(isStop); + } + } catch (e: Throwable) { + Logger.e(TAG, "Failed to handle onPictureInPictureModeChanged", e) } } fun leavePictureInPictureMode(isStop: Boolean) { From d3210ec12a30548e4bbfedd0e173ad27b9127b05 Mon Sep 17 00:00:00 2001 From: zvonimir Date: Thu, 5 Jun 2025 14:15:00 +0200 Subject: [PATCH 14/87] Improve android issue templates --- .../{bug_report.yml => 1-bug_report.yml} | 104 +++++++++++++++--- ...ture_request.yml => 2-feature_request.yml} | 7 +- ...on_issue.yml => 3-documentation_issue.yml} | 5 +- 3 files changed, 97 insertions(+), 19 deletions(-) rename .github/ISSUE_TEMPLATE/{bug_report.yml => 1-bug_report.yml} (54%) rename .github/ISSUE_TEMPLATE/{feature_request.yml => 2-feature_request.yml} (91%) rename .github/ISSUE_TEMPLATE/{documentation_issue.yml => 3-documentation_issue.yml} (95%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/1-bug_report.yml similarity index 54% rename from .github/ISSUE_TEMPLATE/bug_report.yml rename to .github/ISSUE_TEMPLATE/1-bug_report.yml index 776ef91f..e2108e3e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1-bug_report.yml @@ -1,6 +1,9 @@ name: Bug Report description: Let us know about an unexpected error, a crash, or an incorrect behavior. -labels: ["Bug"] +labels: ["Bug", "Android"] +title: "Bug: " +type: bug +projects: ["futo-org/19"] body: - type: markdown attributes: @@ -18,11 +21,33 @@ body: * if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown - type: textarea - id: what-happened + id: reproduction-steps attributes: - label: What happened? - description: What did you expect to happen? - placeholder: Tell us what you see! + label: Reproduction steps + description: Please provide us with the steps to reproduce the issue if possible. This step makes a big difference if we are going to be able to fix it so be as precise as possible. + placeholder: | + 0. Play a Youtube video + 1. Press on Download button + 2. Select quality 1440p + 3. Grayjay crashes when attempting to download + validations: + required: true + + - type: textarea + id: actual-result + attributes: + label: Actual result + description: What happend? + placeholder: Tell us what you saw! + validations: + required: true + + - type: textarea + id: expected-result + attributes: + label: Expected result + description: What was suppose to happen? + placeholder: Tell us what you expected to happen! validations: required: true @@ -31,7 +56,7 @@ body: attributes: label: Grayjay Version description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name". - placeholder: "242" + placeholder: "311" validations: required: true @@ -42,19 +67,23 @@ body: multiple: true options: - "All" - - "Youtube" - - "Odysee" - - "Rumble" - - "Kick" - - "Twitch" - - "PeerTube" - - "Patreon" - - "Nebula" + - "Apple Podcasts" - "BiliBili (CN)" - "Bitchute" - - "SoundCloud" + - "Crunchyroll" + - "CuriosityStream" - "Dailymotion" - - "Apple Podcasts" + - "Kick" + - "Nebula" + - "Odysee" + - "Patreon" + - "PeerTube" + - "Rumble" + - "SoundCloud" + - "Spotify" + - "TedTalks" + - "Twitch" + - "Youtube" - "Other" validations: required: true @@ -66,6 +95,30 @@ body: description: In the application, select Sources > [the broken plugin], write down the value under "Version". placeholder: "12" + - type: input + id: android-version + attributes: + label: Which android version are you using? + placeholder: "Android 15" + validations: + required: true + + - type: input + id: phone-model + attributes: + label: Which device are you using? + placeholder: "Google Pixel 9" + validations: + required: true + + - type: input + id: os-version + attributes: + label: Which operating system are you using? + placeholder: "GrapheneOS/CalyxOS/Tizen/HyperOS 2/..." + validations: + required: true + - type: checkboxes id: login attributes: @@ -86,9 +139,28 @@ body: validations: required: true + - type: textarea + id: grayjay-references + attributes: + label: References + description: | + Are there any other GitHub issues, whether open or closed, that are related to the problem you've described above? If so, please create a list below that mentions each of them. For example: + ``` + - #10 + ``` + placeholder: + value: + validations: + required: false + - type: textarea id: logs attributes: label: Relevant log output description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. render: shell + + - type: markdown + attributes: + value: | + **Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/2-feature_request.yml similarity index 91% rename from .github/ISSUE_TEMPLATE/feature_request.yml rename to .github/ISSUE_TEMPLATE/2-feature_request.yml index ebba5241..2058150f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/2-feature_request.yml @@ -1,13 +1,16 @@ name: Feature Request description: Suggest a new feature or other enhancement. -labels: ["Enhancement"] +labels: ["Enhancement", "Android"] +title: "Feature request: " +type: feature +projects: ["futo-org/19"] body: - type: markdown attributes: value: | # Thank you for opening a feature request. - The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application + The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues and feature requests relating to the Grayjay android application For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay) diff --git a/.github/ISSUE_TEMPLATE/documentation_issue.yml b/.github/ISSUE_TEMPLATE/3-documentation_issue.yml similarity index 95% rename from .github/ISSUE_TEMPLATE/documentation_issue.yml rename to .github/ISSUE_TEMPLATE/3-documentation_issue.yml index c416d012..40d245dc 100644 --- a/.github/ISSUE_TEMPLATE/documentation_issue.yml +++ b/.github/ISSUE_TEMPLATE/3-documentation_issue.yml @@ -1,13 +1,16 @@ name: Documentation Issue description: Report an issue or suggest a change in the documentation. labels: ["Documentation"] +title: "Documentation: " +type: task +projects: ["futo-org/19"] body: - type: markdown attributes: value: | # Thank you for opening a documentation change request. - The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app) + The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay android application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app) Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention. For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay) From 8208f928020e602bd9f100949bc6d2adc074ca12 Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 5 Jun 2025 15:18:45 +0200 Subject: [PATCH 15/87] Added view license in settings. --- .../java/com/futo/platformplayer/Settings.kt | 18 ++++++++++- .../platformplayer/activities/MainActivity.kt | 3 +- .../mainactivity/MainActivityFragment.kt | 2 +- .../bottombar/MenuBottomBarFragment.kt | 4 +-- .../fragment/mainactivity/main/BuyFragment.kt | 32 +++++++++++-------- app/src/main/res/values/strings.xml | 2 ++ 6 files changed, 42 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index d3a24fca..560ceb0a 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -900,7 +900,23 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1) val paymentStatus: String get() = SettingsActivity.getActivity()?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown"; - @FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2) + @FormField(R.string.license_status, FieldForm.BUTTON, R.string.view_license_status, 2) + fun viewLicenseStatus() { + SettingsActivity.getActivity()?.let { + try { + if (StatePayment.instance.hasPaid) { + val paymentKey = StatePayment.instance.getPaymentKey() + UIDialogs.showDialogOk(it, R.drawable.ic_paid, "License activated\n" + paymentKey.first) + } else { + UIDialogs.showDialogOk(it, R.drawable.ic_paid, "No license activated") + } + } catch (e: Throwable) { + Logger.e(TAG, "Failed to show license status dialog", e) + } + } + } + + @FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 3) fun clearPayment() { SettingsActivity.getActivity()?.let { context -> UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", { diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index fcb10008..b46b6502 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -184,7 +184,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { lateinit var _fragVideoDetail: VideoDetailFragment; //State - private val _queue: Queue> = LinkedList(); + private val _queue: LinkedList> = LinkedList(); lateinit var fragCurrent: MainFragment private set; private var _parameterCurrent: Any? = null; @@ -1184,7 +1184,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { if (segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory) fragBeforeOverlay = fragCurrent; - fragCurrent = segment; _parameterCurrent = parameter; } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/MainActivityFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/MainActivityFragment.kt index 6244004b..c9b89d82 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/MainActivityFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/MainActivityFragment.kt @@ -23,7 +23,7 @@ open class MainActivityFragment : Fragment() { fun navigate(frag: MainFragment, parameter: Any? = null, withHistory: Boolean = true) { val a = activity if (a is MainActivity) - (activity as MainActivity).navigate(frag, parameter, withHistory) + (activity as MainActivity).navigate(frag, parameter, withHistory, false) else Log.e(TAG, "Failed to navigate due to activity not being a main activity.") } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt index 521fb016..c8f0d62e 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt @@ -330,7 +330,7 @@ class MenuBottomBarFragment : MainActivityFragment() { } if (!StatePayment.instance.hasPaid) { - newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid_filled, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate(withHistory = false) })) + newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid_filled, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate(withHistory = true) })) } //Add conditional buttons here, when you add a conditional button, be sure to add the register and unregister events for when the button needs to be updated @@ -396,7 +396,7 @@ class MenuBottomBarFragment : MainActivityFragment() { ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate(withHistory = false) }), ButtonDefinition(10, R.drawable.ic_help_square, R.drawable.ic_help_square_fill, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate(withHistory = false) }), ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, { - val c = it.context ?: return@ButtonDefinition; + val c = it.context ?: return@ButtonDefinition; Logger.i(TAG, "settings preventPictureInPicture()"); it.requireFragment().preventPictureInPicture(); val intent = Intent(c, SettingsActivity::class.java); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt index e79e0c13..eaa84e3e 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt @@ -1,6 +1,8 @@ package com.futo.platformplayer.fragment.mainactivity.main import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -66,8 +68,7 @@ class BuyFragment : MainFragment() { _paymentManager = PaymentManager(StatePayment.instance, fragment, _overlayPaying) { success, _, exception -> if(success) { - UIDialogs.showDialog(context, R.drawable.ic_check, context.getString(R.string.payment_succeeded), context.getString(R.string.thanks_for_your_purchase_a_key_will_be_sent_to_your_email_after_your_payment_has_been_received), null, 0, - UIDialogs.Action("Ok", {}, UIDialogs.ActionStyle.PRIMARY)); + UIDialogs.showDialog(context, R.drawable.ic_check, context.getString(R.string.payment_succeeded), context.getString(R.string.thanks_for_your_purchase_a_key_will_be_sent_to_your_email_after_your_payment_has_been_received), null, 0, UIDialogs.Action("Ok", {}, UIDialogs.ActionStyle.PRIMARY)); _fragment.close(true); } else { @@ -115,11 +116,14 @@ class BuyFragment : MainFragment() { val licenseInput = SlideUpMenuTextInput(context, context.getString(R.string.license)); val productLicenseDialog = SlideUpMenuOverlay(context, findViewById(R.id.overlay_paid), context.getString(R.string.enter_license_key), context.getString(R.string.ok), true, licenseInput); productLicenseDialog.onOK.subscribe { + licenseInput.deactivate(); val licenseText = licenseInput.text; if (licenseText.isNullOrEmpty()) { UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.invalid_license_key)); return@subscribe; } + licenseInput.clear(); + productLicenseDialog.hide(true); _fragment.lifecycleScope.launch(Dispatchers.IO) { @@ -127,17 +131,18 @@ class BuyFragment : MainFragment() { val activationResult = StatePayment.instance.setPaymentLicense(licenseText); withContext(Dispatchers.Main) { - if(activationResult) { - licenseInput.deactivate(); - licenseInput.clear(); - productLicenseDialog.hide(true); - - UIDialogs.showDialogOk(context, R.drawable.ic_check, context.getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required)); - _fragment.close(true); - } - else - { - UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.invalid_license_key)); + try { + if(activationResult) { + UIDialogs.showDialogOk(context, R.drawable.ic_check, context.getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required)) { + _fragment.close(true) + } + } + else + { + UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.invalid_license_key)); + } + } catch (e: Throwable) { + Logger.e(TAG, "Failed to update UI after buy complete", e) } } } @@ -158,5 +163,6 @@ class BuyFragment : MainFragment() { companion object { fun newInstance() = BuyFragment().apply {} + private val TAG = "BuyFragment" } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c245ccac..98f542c0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -442,6 +442,8 @@ If home filters should show full plugin names or just icons Log Level Logging + License status + View license status Sync Grayjay Sync your data across multiple devices Manage Polycentric identity From c94c2721d727fb98a9cf7acaf01c03f32bb2ae1d Mon Sep 17 00:00:00 2001 From: Koen Date: Thu, 5 Jun 2025 15:14:31 +0000 Subject: [PATCH 16/87] Revert "prevent the user from needing to tap update on system dialog when self updating" This reverts commit a1d460385d644735a999e7b477a8e62ee240bfe4 --- app/src/main/AndroidManifest.xml | 1 - .../java/com/futo/platformplayer/dialogs/AutoUpdateDialog.kt | 5 ----- 2 files changed, 6 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 176dc044..ea6f3e5b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,6 @@ - = Build.VERSION_CODES.S) { - params.setRequireUserAction(USER_ACTION_NOT_REQUIRED) - } val sessionId = packageInstaller.createSession(params); session = packageInstaller.openSession(sessionId) From c333300906b732356868eb6ddab53172735cb27e Mon Sep 17 00:00:00 2001 From: Kai Date: Thu, 5 Jun 2025 11:08:19 -0500 Subject: [PATCH 17/87] fix graphical glitches with quality selector Changelog: changed --- .../overlays/slideup/SlideUpMenuOverlay.kt | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) 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)); From 84a51035266b5907deb4cd5e370e0a4b8d738b0c Mon Sep 17 00:00:00 2001 From: Kelvin Date: Thu, 5 Jun 2025 19:11:55 +0200 Subject: [PATCH 18/87] Use lifecycle scope instead of root scope --- .../fragment/mainactivity/main/SubscriptionsFeedFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt index 83e39a88..4a3fdf91 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt @@ -191,7 +191,7 @@ class SubscriptionsFeedFragment : MainFragment() { private var _bypassRateLimit = false; private val _lastExceptions: List? = null; - private val _taskGetPager = TaskHandler>({StateApp.instance.scope}, { withRefresh -> + private val _taskGetPager = TaskHandler>({fragment.lifecycleScope}, { withRefresh -> val group = subGroup; if(!_bypassRateLimit) { val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group); @@ -202,7 +202,7 @@ class SubscriptionsFeedFragment : MainFragment() { throw RateLimitException(rateLimitPlugins.map { it.key.id }); } _bypassRateLimit = false; - val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh, group); + val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(fragment.lifecycleScope, withRefresh, group); val feed = StateSubscriptions.instance.getFeed(group?.id); val currentExs = feed?.exceptions ?: listOf(); From fe0aac7c6effeafee5637406df1d59f061bd4c72 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Thu, 5 Jun 2025 22:47:45 +0200 Subject: [PATCH 19/87] WIP playback speed additions --- .../java/com/futo/platformplayer/Settings.kt | 53 +++++++++++++++++++ .../mainactivity/main/VideoDetailView.kt | 29 +++++++--- .../overlays/slideup/SlideUpMenuButtonList.kt | 2 +- .../overlay_slide_up_menu_button_list.xml | 4 +- app/src/main/res/values/strings.xml | 23 ++++++++ 5 files changed, 102 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 560ceb0a..b8e23283 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -531,6 +531,59 @@ class Settings : FragmentedStorageFileJson() { else -> 10_000L; } } + + + @FormField(R.string.min_playback_speed, FieldForm.DROPDOWN, R.string.min_playback_speed_description, 25) + @DropdownFieldOptionsId(R.array.min_playback_speed) + var minimumPlaybackSpeed: Int = 0; + @FormField(R.string.max_playback_speed, FieldForm.DROPDOWN, R.string.max_playback_speed_description, 26) + @DropdownFieldOptionsId(R.array.max_playback_speed) + var maximumPlaybackSpeed: Int = 2; + @FormField(R.string.step_playback_speed, FieldForm.DROPDOWN, R.string.step_playback_speed_description, 26) + @DropdownFieldOptionsId(R.array.step_playback_speed) + var stepPlaybackSpeed: Int = 1; + + fun getPlaybackSpeedStep(): Double { + return when(stepPlaybackSpeed) { + 0 -> 0.05 + 1 -> 0.1 + 2 -> 0.25 + else -> 0.1; + } + } + fun getPlaybackSpeeds(): List { + val playbackSpeeds = mutableListOf(); + playbackSpeeds.add(1.0); + val minSpeed = when(minimumPlaybackSpeed) { + 0 -> 0.25 + 1 -> 0.5 + 2 -> 1.0 + else -> 0.25 + } + val maxSpeed = when(maximumPlaybackSpeed) { + 0 -> 2.0 + 1 -> 2.25 + 2 -> 3.0 + 3 -> 4.0 + 4 -> 5.0 + else -> 2.25; + } + var testSpeed = 1.0; + + while(testSpeed > minSpeed) { + val nextSpeed = (testSpeed - 0.25) as Double; + testSpeed = Math.max(nextSpeed, minSpeed); + playbackSpeeds.add(testSpeed); + } + testSpeed = 1.0; + while(testSpeed < maxSpeed) { + val nextSpeed = (testSpeed + if(testSpeed < 2) 0.25 else 1.0) as Double; + testSpeed = Math.min(nextSpeed, maxSpeed); + playbackSpeeds.add(testSpeed); + } + playbackSpeeds.sort(); + return playbackSpeeds; + } } @FormField(R.string.comments, "group", R.string.comments_description, 6) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 199f5566..8e514ce0 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -2149,23 +2149,40 @@ class VideoDetailView : ConstraintLayout { val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate() + val qualityPlaybackSpeedTitle = if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", currentPlaybackRate)})"); } else null; _overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString( R.string.quality), null, true, - if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate)) } else null, + qualityPlaybackSpeedTitle, if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply { - setButtons(listOf("0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25"), currentPlaybackRate!!.toString()); + val playbackSpeeds = Settings.instance.playback.getPlaybackSpeeds(); + val format = if(playbackSpeeds.size < 20) "%.2f" else "%.1f"; + val playbackLabels = playbackSpeeds.map { String.format(format, it) }.toMutableList(); + playbackLabels.add("+"); + playbackLabels.add(0, "-"); + + setButtons(playbackLabels, String.format(format, currentPlaybackRate)); onClick.subscribe { v -> + val currentPlaybackSpeed = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate(); + var playbackSpeedString = v; + val stepSpeed = Settings.instance.playback.getPlaybackSpeedStep(); + if(v == "+") + playbackSpeedString = String.format("%.2f", (currentPlaybackSpeed?.toDouble() ?: 1.0) + stepSpeed).toString(); + else if(v == "-") + playbackSpeedString = String.format("%.2f", (currentPlaybackSpeed?.toDouble() ?: 1.0) - stepSpeed).toString(); + val newPlaybackSpeed = playbackSpeedString.toDouble(); if (_isCasting) { val ad = StateCasting.instance.activeDevice ?: return@subscribe if (!ad.canSetSpeed) { return@subscribe } - ad.changeSpeed(v.toDouble()) - setSelected(v); + qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", newPlaybackSpeed)})"); + ad.changeSpeed(newPlaybackSpeed) + setSelected(playbackSpeedString); } else { - _player.setPlaybackRate(v.toFloat()); - setSelected(v); + qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", newPlaybackSpeed)})"); + _player.setPlaybackRate(playbackSpeedString.toFloat()); + setSelected(playbackSpeedString); } }; } else null, diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuButtonList.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuButtonList.kt index 78031ec0..d30a4795 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuButtonList.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuButtonList.kt @@ -31,7 +31,7 @@ class SlideUpMenuButtonList : LinearLayout { fun setButtons(texts: List, activeText: String? = null) { _root.removeAllViews(); - val marginLeft = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.0f, resources.displayMetrics).toInt(); + val marginLeft = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.5f, resources.displayMetrics).toInt(); val marginRight = marginLeft; buttons.clear(); diff --git a/app/src/main/res/layout/overlay_slide_up_menu_button_list.xml b/app/src/main/res/layout/overlay_slide_up_menu_button_list.xml index e27f28af..4b37c8f5 100644 --- a/app/src/main/res/layout/overlay_slide_up_menu_button_list.xml +++ b/app/src/main/res/layout/overlay_slide_up_menu_button_list.xml @@ -5,7 +5,7 @@ android:layout_marginTop="10dp" android:id="@+id/root" android:orientation="horizontal" - android:paddingStart="6dp" - android:paddingEnd="6dp"> + android:paddingStart="0dp" + android:paddingEnd="0dp"> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98f542c0..e2b7d730 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -427,6 +427,12 @@ Delete from WatchLater when watched After you leave a video that you mostly watched, it will be removed from watch later. Seek duration + Minimum Playback Speed + Minimum Available Speed + Maximum Playback Speed + Maximum Available Speed + Playback Speed Step Size + The step size of playback speeds, may not affect higher playback speeds. Fast-Forward / Fast-Rewind duration Switch to Audio in Background Groups @@ -1091,6 +1097,23 @@ 30 seconds 60 seconds + + 2.0 + 2.25 + 3.0 + 4.0 + 5.0 + + + 0.25 + 0.5 + 1.0 + + + 0.05 + 0.1 + 0.25 + 15 30 From f86fb0ee44b5b883da3b84f41e795c0fabd3757e Mon Sep 17 00:00:00 2001 From: Kai Date: Thu, 5 Jun 2025 23:13:05 -0500 Subject: [PATCH 20/87] add functionality to copy playlists fix https://github.com/futo-org/grayjay-android/issues/2306 Changelog: added --- .../mainactivity/main/PlaylistFragment.kt | 27 ++++++++++--------- .../futo/platformplayer/models/Playlist.kt | 3 +++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt index c56585b0..ba433563 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt @@ -10,7 +10,6 @@ import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.* import com.futo.platformplayer.activities.IWithResultLauncher import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist -import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.constructs.TaskHandler @@ -165,14 +164,16 @@ class PlaylistFragment : MainFragment() { }; } - private fun copyPlaylist(playlist: Playlist) { + private fun savePlaylist(playlist: Playlist) { StatePlaylists.instance.playlistStore.save(playlist) - _fragment.topBar?.assume()?.setMenuItems( - arrayListOf() - ) UIDialogs.toast("Playlist saved") } + private fun copyPlaylist(playlist: Playlist) { + StatePlaylists.instance.playlistStore.save(playlist.makeCopy()) + UIDialogs.toast("Playlist copied") + } + fun onShown(parameter: Any?) { _taskLoadPlaylist.cancel() @@ -188,12 +189,14 @@ class PlaylistFragment : MainFragment() { setButtonExportVisible(false) setButtonEditVisible(true) - if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) { - _fragment.topBar?.assume() - ?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) { + _fragment.topBar?.assume() + ?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) { + if (StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) { copyPlaylist(parameter) - })) - } + } else { + savePlaylist(parameter) + } + })) } else { setName(null) setVideos(null, false) @@ -259,7 +262,7 @@ class PlaylistFragment : MainFragment() { val playlist = _playlist ?: return if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) { UIDialogs.showConfirmationDialog(context, "Playlist must be saved to download", { - copyPlaylist(playlist) + savePlaylist(playlist) download() }) return @@ -292,7 +295,7 @@ class PlaylistFragment : MainFragment() { val playlist = _playlist ?: return if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) { UIDialogs.showConfirmationDialog(context, "Playlist must be saved to edit the name", { - copyPlaylist(playlist) + savePlaylist(playlist) onEditClick() }) return 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 d7b1035f..d6bca755 100644 --- a/app/src/main/java/com/futo/platformplayer/models/Playlist.kt +++ b/app/src/main/java/com/futo/platformplayer/models/Playlist.kt @@ -35,6 +35,9 @@ class Playlist { this.videos = ArrayList(list); } + fun makeCopy(): Playlist { + return Playlist("$name (Copy)", videos) + } companion object { fun fromV8(config: SourcePluginConfig, obj: V8ValueObject?): Playlist? { From 8e70f1b865fa9dceab0e4341be367337ea757e7d Mon Sep 17 00:00:00 2001 From: Kai Date: Thu, 5 Jun 2025 23:14:03 -0500 Subject: [PATCH 21/87] add long tap to copy playing video title Changelog: added --- .../fragment/mainactivity/main/VideoDetailView.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 199f5566..073d67b0 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -2,6 +2,8 @@ package com.futo.platformplayer.fragment.mainactivity.main import android.app.PictureInPictureParams import android.app.RemoteAction +import android.content.ClipData +import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.content.res.Configuration @@ -408,6 +410,14 @@ class VideoDetailView : ConstraintLayout { showChaptersUI(); }; + _title.setOnLongClickListener { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager; + val clip = ClipData.newPlainText("Video Title", (it as TextView).text); + clipboard.setPrimaryClip(clip); + UIDialogs.toast(context, "Copied", false) + // let other interactions happen based on the touch + false + } _buttonSubscribe.onSubscribed.subscribe { _slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer); From 70f36e69e606264a51f28c4be67dd506ea3dbf19 Mon Sep 17 00:00:00 2001 From: Koen J Date: Fri, 6 Jun 2025 10:15:15 +0200 Subject: [PATCH 22/87] Freeze fix when clicking link in description. --- .../others/PlatformLinkMovementMethod.kt | 47 ++++++++++++------- .../views/behavior/NonScrollingTextView.kt | 36 +++++++++----- 2 files changed, 54 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt b/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt index d524b7cf..76652236 100644 --- a/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt +++ b/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt @@ -8,11 +8,14 @@ import android.text.method.LinkMovementMethod import android.text.style.URLSpan import android.view.MotionEvent import android.widget.TextView +import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.receivers.MediaControlReceiver import com.futo.platformplayer.timestampRegex -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMethod() { @@ -60,31 +63,39 @@ class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMe val dx = event.x - downX val dy = event.y - downY if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(widget, event)) { - runBlocking { - for (link in pressedLinks!!) { - Logger.i(TAG) { "Link clicked '${link.url}'." } + for (link in pressedLinks!!) { + Logger.i(TAG) { "Link clicked '${link.url}'." } - if (_context is MainActivity) { - if (_context.handleUrl(link.url)) continue - if (timestampRegex.matches(link.url)) { - val tokens = link.url.split(':') - var time_s = -1L - when (tokens.size) { - 2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong() - 3 -> time_s = tokens[0].toLong() * 3600 + - tokens[1].toLong() * 60 + - tokens[2].toLong() - } + val c = _context + if (c is MainActivity) { + c.lifecycleScope.launch(Dispatchers.IO) { + if (c.handleUrl(link.url)) { + return@launch + } + if (timestampRegex.matches(link.url)) { + val tokens = link.url.split(':') + var time_s = -1L + when (tokens.size) { + 2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong() + 3 -> time_s = tokens[0].toLong() * 3600 + + tokens[1].toLong() * 60 + + tokens[2].toLong() + } - if (time_s != -1L) { + if (time_s != -1L) { + withContext(Dispatchers.Main) { MediaControlReceiver.onSeekToReceived.emit(time_s * 1000) - continue } + return@launch } } - _context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))) + + withContext(Dispatchers.Main) { + c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))) + } } } + } pressedLinks = null linkPressed = false return true diff --git a/app/src/main/java/com/futo/platformplayer/views/behavior/NonScrollingTextView.kt b/app/src/main/java/com/futo/platformplayer/views/behavior/NonScrollingTextView.kt index cf599176..6e3a8860 100644 --- a/app/src/main/java/com/futo/platformplayer/views/behavior/NonScrollingTextView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/behavior/NonScrollingTextView.kt @@ -8,12 +8,16 @@ import android.text.Spannable import android.text.style.URLSpan import android.util.AttributeSet import android.view.MotionEvent +import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.others.PlatformLinkMovementMethod import com.futo.platformplayer.receivers.MediaControlReceiver +import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.timestampRegex -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView { private var _lastTouchedLinks: Array? = null @@ -77,12 +81,14 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView { val dx = event.x - downX val dy = event.y - downY if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(event)) { - runBlocking { - for (link in _lastTouchedLinks!!) { - Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." } - val c = context - if (c is MainActivity) { - if (c.handleUrl(link.url)) continue + for (link in _lastTouchedLinks!!) { + Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." } + val c = context + if (c is MainActivity) { + c.lifecycleScope.launch(Dispatchers.IO) { + if (c.handleUrl(link.url)) { + return@launch + } if (timestampRegex.matches(link.url)) { val tokens = link.url.split(':') var time_s = -1L @@ -92,13 +98,21 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView { tokens[1].toLong() * 60 + tokens[2].toLong() } + if (time_s != -1L) { - MediaControlReceiver.onSeekToReceived.emit(time_s * 1000) - continue + withContext(Dispatchers.Main) { + MediaControlReceiver.onSeekToReceived.emit(time_s * 1000) + } + return@launch } } - c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))) - } else { + + withContext(Dispatchers.Main) { + c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))) + } + } + } else { + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))) } } From 5b50ac926ecb6a4e25155f30170e70d6cb499650 Mon Sep 17 00:00:00 2001 From: Koen J Date: Fri, 6 Jun 2025 10:15:15 +0200 Subject: [PATCH 23/87] Freeze fix when clicking link in description. --- .../others/PlatformLinkMovementMethod.kt | 47 ++++++++++++------- .../views/behavior/NonScrollingTextView.kt | 36 +++++++++----- 2 files changed, 54 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt b/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt index d524b7cf..76652236 100644 --- a/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt +++ b/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt @@ -8,11 +8,14 @@ import android.text.method.LinkMovementMethod import android.text.style.URLSpan import android.view.MotionEvent import android.widget.TextView +import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.receivers.MediaControlReceiver import com.futo.platformplayer.timestampRegex -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMethod() { @@ -60,31 +63,39 @@ class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMe val dx = event.x - downX val dy = event.y - downY if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(widget, event)) { - runBlocking { - for (link in pressedLinks!!) { - Logger.i(TAG) { "Link clicked '${link.url}'." } + for (link in pressedLinks!!) { + Logger.i(TAG) { "Link clicked '${link.url}'." } - if (_context is MainActivity) { - if (_context.handleUrl(link.url)) continue - if (timestampRegex.matches(link.url)) { - val tokens = link.url.split(':') - var time_s = -1L - when (tokens.size) { - 2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong() - 3 -> time_s = tokens[0].toLong() * 3600 + - tokens[1].toLong() * 60 + - tokens[2].toLong() - } + val c = _context + if (c is MainActivity) { + c.lifecycleScope.launch(Dispatchers.IO) { + if (c.handleUrl(link.url)) { + return@launch + } + if (timestampRegex.matches(link.url)) { + val tokens = link.url.split(':') + var time_s = -1L + when (tokens.size) { + 2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong() + 3 -> time_s = tokens[0].toLong() * 3600 + + tokens[1].toLong() * 60 + + tokens[2].toLong() + } - if (time_s != -1L) { + if (time_s != -1L) { + withContext(Dispatchers.Main) { MediaControlReceiver.onSeekToReceived.emit(time_s * 1000) - continue } + return@launch } } - _context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))) + + withContext(Dispatchers.Main) { + c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))) + } } } + } pressedLinks = null linkPressed = false return true diff --git a/app/src/main/java/com/futo/platformplayer/views/behavior/NonScrollingTextView.kt b/app/src/main/java/com/futo/platformplayer/views/behavior/NonScrollingTextView.kt index cf599176..6e3a8860 100644 --- a/app/src/main/java/com/futo/platformplayer/views/behavior/NonScrollingTextView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/behavior/NonScrollingTextView.kt @@ -8,12 +8,16 @@ import android.text.Spannable import android.text.style.URLSpan import android.util.AttributeSet import android.view.MotionEvent +import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.others.PlatformLinkMovementMethod import com.futo.platformplayer.receivers.MediaControlReceiver +import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.timestampRegex -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView { private var _lastTouchedLinks: Array? = null @@ -77,12 +81,14 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView { val dx = event.x - downX val dy = event.y - downY if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(event)) { - runBlocking { - for (link in _lastTouchedLinks!!) { - Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." } - val c = context - if (c is MainActivity) { - if (c.handleUrl(link.url)) continue + for (link in _lastTouchedLinks!!) { + Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." } + val c = context + if (c is MainActivity) { + c.lifecycleScope.launch(Dispatchers.IO) { + if (c.handleUrl(link.url)) { + return@launch + } if (timestampRegex.matches(link.url)) { val tokens = link.url.split(':') var time_s = -1L @@ -92,13 +98,21 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView { tokens[1].toLong() * 60 + tokens[2].toLong() } + if (time_s != -1L) { - MediaControlReceiver.onSeekToReceived.emit(time_s * 1000) - continue + withContext(Dispatchers.Main) { + MediaControlReceiver.onSeekToReceived.emit(time_s * 1000) + } + return@launch } } - c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))) - } else { + + withContext(Dispatchers.Main) { + c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))) + } + } + } else { + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))) } } From d08dffd9e2001d3445b05342f69a380250c159ff Mon Sep 17 00:00:00 2001 From: Koen J Date: Fri, 6 Jun 2025 11:12:31 +0200 Subject: [PATCH 24/87] Added potential fix for having to restart app to get casting devices to show. Added persistent ordering for creators. --- .../java/com/futo/platformplayer/casting/StateCasting.kt | 5 +++-- .../futo/platformplayer/dialogs/ConnectCastingDialog.kt | 2 -- .../fragment/mainactivity/main/CreatorsFragment.kt | 7 ++++++- .../platformplayer/views/adapters/SubscriptionAdapter.kt | 3 ++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index 58bd772c..b2c2156c 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -166,10 +166,11 @@ class StateCasting { Logger.i(TAG, "CastingService started."); _nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager + startDiscovering() } @Synchronized - fun startDiscovering() { + private fun startDiscovering() { _nsdManager?.apply { _discoveryListeners.forEach { discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value) @@ -178,7 +179,7 @@ class StateCasting { } @Synchronized - fun stopDiscovering() { + private fun stopDiscovering() { _nsdManager?.apply { _discoveryListeners.forEach { try { diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt index f00bd191..87375779 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt @@ -103,7 +103,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) { super.show(); Logger.i(TAG, "Dialog shown."); - StateCasting.instance.startDiscovering() (_imageLoader.drawable as Animatable?)?.start(); synchronized(StateCasting.instance.devices) { @@ -148,7 +147,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) { override fun dismiss() { super.dismiss() (_imageLoader.drawable as Animatable?)?.stop() - StateCasting.instance.stopDiscovering() StateCasting.instance.onDeviceAdded.remove(this) StateCasting.instance.onDeviceChanged.remove(this) StateCasting.instance.onDeviceRemoved.remove(this) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CreatorsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CreatorsFragment.kt index 54649ebf..345d577e 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CreatorsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CreatorsFragment.kt @@ -16,6 +16,8 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.futo.platformplayer.R import com.futo.platformplayer.UISlideOverlays +import com.futo.platformplayer.stores.FragmentedStorage +import com.futo.platformplayer.stores.StringStorage import com.futo.platformplayer.views.adapters.SubscriptionAdapter class CreatorsFragment : MainFragment() { @@ -29,6 +31,8 @@ class CreatorsFragment : MainFragment() { private var _editSearch: EditText? = null; private var _textMeta: TextView? = null; private var _buttonClearSearch: ImageButton? = null + private var _ordering = FragmentedStorage.get("creators_ordering") + override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { val view = inflater.inflate(R.layout.fragment_creators, container, false); @@ -44,7 +48,7 @@ class CreatorsFragment : MainFragment() { _buttonClearSearch?.visibility = View.INVISIBLE; } - val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)) { subs -> + val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription), _ordering?.value?.toIntOrNull() ?: 5) { subs -> _textMeta?.let { it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}"; } @@ -61,6 +65,7 @@ class CreatorsFragment : MainFragment() { spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) { adapter.sortBy = pos; + _ordering.setAndSave(pos.toString()) } override fun onNothingSelected(parent: AdapterView<*>?) = Unit }; diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt index 33783e67..fb28c2e4 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt @@ -31,10 +31,11 @@ class SubscriptionAdapter : RecyclerView.Adapter { updateDataset(); } - constructor(inflater: LayoutInflater, confirmationMessage: String, onDatasetChanged: ((List)->Unit)? = null) : super() { + constructor(inflater: LayoutInflater, confirmationMessage: String, sortByDefault: Int, onDatasetChanged: ((List)->Unit)? = null) : super() { _inflater = inflater; _confirmationMessage = confirmationMessage; _onDatasetChanged = onDatasetChanged; + sortBy = sortByDefault StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper()) StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { updateDataset() } From 2c454a0ec55ec8b80bd469fefdb23f3ce5761f69 Mon Sep 17 00:00:00 2001 From: Koen J Date: Fri, 6 Jun 2025 11:20:04 +0200 Subject: [PATCH 25/87] Added boolean setting to allow link local casting over ipv4. --- .../main/java/com/futo/platformplayer/Settings.kt | 5 +++++ .../com/futo/platformplayer/casting/StateCasting.kt | 13 ++++++++++--- app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index b8e23283..e8f4f70a 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -681,6 +681,11 @@ class Settings : FragmentedStorageFileJson() { @Serializable(with = FlexibleBooleanSerializer::class) var allowIpv6: Boolean = true; + @AdvancedField + @FormField(R.string.allow_ipv4, FieldForm.TOGGLE, R.string.allow_ipv4_description, 5) + @Serializable(with = FlexibleBooleanSerializer::class) + var allowLinkLocalIpv4: Boolean = false; + /*TODO: Should we have a different casting quality? @FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3) @DropdownFieldOptionsId(R.array.preferred_quality_array) diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index b2c2156c..1e8e1830 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -1221,9 +1221,16 @@ class StateCasting { private fun getLocalUrl(ad: CastingDevice): String { var address = ad.localAddress!! - if (address.isLinkLocalAddress) { - address = findPreferredAddress() ?: address - Logger.i(TAG, "Selected casting address: $address") + if (Settings.instance.casting.allowLinkLocalIpv4) { + if (address.isLinkLocalAddress && address is Inet6Address) { + address = findPreferredAddress() ?: address + Logger.i(TAG, "Selected casting address: $address") + } + } else { + if (address.isLinkLocalAddress) { + address = findPreferredAddress() ?: address + Logger.i(TAG, "Selected casting address: $address") + } } return "http://${address.toUrlAddress().trim('/')}:${_castServer.port}"; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e2b7d730..b2513e21 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -76,6 +76,8 @@ Always proxy requests when casting data through the device. Allow IPV6 If casting over IPV6 is allowed, can cause issues on some networks + Allow Link Local IPV4 + If casting over IPV4 link local is allowed, can cause issues on some networks Discover Find new video sources to add These sources have been disabled From a810f82ce2a03c93962e39e92c44efa0c17eac5e Mon Sep 17 00:00:00 2001 From: Koen J Date: Fri, 6 Jun 2025 11:20:04 +0200 Subject: [PATCH 26/87] Added boolean setting to allow link local casting over ipv4. --- .../main/java/com/futo/platformplayer/Settings.kt | 5 +++++ .../com/futo/platformplayer/casting/StateCasting.kt | 13 ++++++++++--- app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 560ceb0a..281523c6 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -628,6 +628,11 @@ class Settings : FragmentedStorageFileJson() { @Serializable(with = FlexibleBooleanSerializer::class) var allowIpv6: Boolean = true; + @AdvancedField + @FormField(R.string.allow_ipv4, FieldForm.TOGGLE, R.string.allow_ipv4_description, 5) + @Serializable(with = FlexibleBooleanSerializer::class) + var allowLinkLocalIpv4: Boolean = false; + /*TODO: Should we have a different casting quality? @FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3) @DropdownFieldOptionsId(R.array.preferred_quality_array) diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index b2c2156c..1e8e1830 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -1221,9 +1221,16 @@ class StateCasting { private fun getLocalUrl(ad: CastingDevice): String { var address = ad.localAddress!! - if (address.isLinkLocalAddress) { - address = findPreferredAddress() ?: address - Logger.i(TAG, "Selected casting address: $address") + if (Settings.instance.casting.allowLinkLocalIpv4) { + if (address.isLinkLocalAddress && address is Inet6Address) { + address = findPreferredAddress() ?: address + Logger.i(TAG, "Selected casting address: $address") + } + } else { + if (address.isLinkLocalAddress) { + address = findPreferredAddress() ?: address + Logger.i(TAG, "Selected casting address: $address") + } } return "http://${address.toUrlAddress().trim('/')}:${_castServer.port}"; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98f542c0..cd96fd44 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -76,6 +76,8 @@ Always proxy requests when casting data through the device. Allow IPV6 If casting over IPV6 is allowed, can cause issues on some networks + Allow Link Local IPV4 + If casting over IPV4 link local is allowed, can cause issues on some networks Discover Find new video sources to add These sources have been disabled From 10753eb87937adb2d9caca6d75ac504c8883dc2a Mon Sep 17 00:00:00 2001 From: Koen J Date: Fri, 6 Jun 2025 12:25:25 +0200 Subject: [PATCH 27/87] Sort to prefer ipv4 over ipv6. --- .../java/com/futo/platformplayer/Extensions_Network.kt | 7 +++++-- app/src/main/java/com/futo/platformplayer/Utility.kt | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt index f0137f2d..fca7deda 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_Network.kt @@ -241,8 +241,11 @@ fun getConnectedSocket(attemptAddresses: List, port: Int): Socket? return null; } + val sortedAddresses: List = addresses + .sortedBy { addr -> addressScore(addr) } + val sockets: ArrayList = arrayListOf(); - for (i in addresses.indices) { + for (i in sortedAddresses.indices) { sockets.add(Socket()); } @@ -250,7 +253,7 @@ fun getConnectedSocket(attemptAddresses: List, port: Int): Socket? var connectedSocket: Socket? = null; val threads: ArrayList = arrayListOf(); for (i in 0 until sockets.size) { - val address = addresses[i]; + val address = sortedAddresses[i]; val socket = sockets[i]; val thread = Thread { try { diff --git a/app/src/main/java/com/futo/platformplayer/Utility.kt b/app/src/main/java/com/futo/platformplayer/Utility.kt index e2868ed6..0875aadb 100644 --- a/app/src/main/java/com/futo/platformplayer/Utility.kt +++ b/app/src/main/java/com/futo/platformplayer/Utility.kt @@ -434,7 +434,7 @@ private fun interfaceScore(nif: NetworkInterface): Int { } } -private fun addressScore(addr: InetAddress): Int { +fun addressScore(addr: InetAddress): Int { return when (addr) { is Inet4Address -> { val octets = addr.address.map { it.toInt() and 0xFF } From 0d9e1cd3c5e680082a107e23038aa5c08118f8b7 Mon Sep 17 00:00:00 2001 From: zvonimir Date: Fri, 6 Jun 2025 15:40:43 +0200 Subject: [PATCH 28/87] fix: Scope getting removed when switching between settings 'Kelvin approved' --- .../platformplayer/activities/MainActivity.kt | 17 ++++++++++------- .../com/futo/platformplayer/states/StateApp.kt | 14 +++++++++++--- .../platformplayer/views/fields/ToggleField.kt | 2 +- app/src/unstable/AndroidManifest.xml | 2 +- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index b46b6502..829b7857 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -115,6 +115,7 @@ import java.io.StringWriter import java.lang.reflect.InvocationTargetException import java.util.LinkedList import java.util.Queue +import java.util.UUID import java.util.concurrent.ConcurrentLinkedQueue @@ -218,6 +219,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { } } + val mainId = UUID.randomUUID().toString().substring(0, 5) + constructor() : super() { if (BuildConfig.DEBUG) { StrictMode.setVmPolicy( @@ -269,8 +272,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { @UnstableApi override fun onCreate(savedInstanceState: Bundle?) { - Logger.i(TAG, "MainActivity Starting"); - StateApp.instance.setGlobalContext(this, lifecycleScope); + Logger.w(TAG, "MainActivity Starting [$mainId]"); + StateApp.instance.setGlobalContext(this, lifecycleScope, mainId); StateApp.instance.mainAppStarting(this); super.onCreate(savedInstanceState); @@ -671,13 +674,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { override fun onResume() { super.onResume(); - Logger.v(TAG, "onResume") + Logger.w(TAG, "onResume [$mainId]") _isVisible = true; } override fun onPause() { super.onPause(); - Logger.v(TAG, "onPause") + Logger.w(TAG, "onPause [$mainId]") _isVisible = false; _qrCodeLoadingDialog?.dismiss() @@ -686,7 +689,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { override fun onStop() { super.onStop() - Logger.v(TAG, "_wasStopped = true"); + Logger.w(TAG, "onStop [$mainId]"); _wasStopped = true; } @@ -1103,8 +1106,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { override fun onDestroy() { super.onDestroy(); - Logger.v(TAG, "onDestroy") - StateApp.instance.mainAppDestroyed(this); + Logger.w(TAG, "onDestroy [$mainId]") + StateApp.instance.mainAppDestroyed(this, mainId); } inline fun isFragmentActive(): Boolean { diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt index e2155e8b..9757f005 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -156,6 +156,8 @@ class StateApp { return thisContext; } + private var _mainId: String? = null; + //Files private var _tempDirectory: File? = null; private var _cacheDirectory: File? = null; @@ -295,9 +297,12 @@ class StateApp { } //Lifecycle - fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null) { + fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null, mainId: String? = null) { + _mainId = mainId; _context = context; _scope = coroutineScope + Logger.w(TAG, "Scope initialized ${(coroutineScope != null)}\n ${Log.getStackTraceString(Throwable())}") + } fun initializeFiles(force: Boolean = false) { @@ -719,7 +724,9 @@ class StateApp { migrateStores(context, managedStores, index + 1); } - fun mainAppDestroyed(context: Context) { + fun mainAppDestroyed(context: Context, mainId: String? = null) { + if (mainId != null && (_mainId != mainId || _mainId == null)) + return Logger.i(TAG, "App ended"); _receiverBecomingNoisy?.let { _receiverBecomingNoisy = null; @@ -743,7 +750,8 @@ class StateApp { fun dispose(){ _context = null; - _scope = null; + // _scope = null; + Logger.w(TAG, "StateApp disposed: ${Log.getStackTraceString(Throwable())}") } private val _connectivityEvents = object : ConnectivityManager.NetworkCallback() { diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt b/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt index c419092c..7675a791 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt @@ -90,7 +90,7 @@ class ToggleField : TableRow, IField { val advancedFieldAttr = field.getAnnotation(AdvancedField::class.java) if(advancedFieldAttr != null || advanced) { - Logger.w("ToggleField", "Found advanced field: " + field.name); + // Logger.w("ToggleField", "Found advanced field: " + field.name); isAdvanced = true; } diff --git a/app/src/unstable/AndroidManifest.xml b/app/src/unstable/AndroidManifest.xml index 7c47d7bd..de471f2d 100644 --- a/app/src/unstable/AndroidManifest.xml +++ b/app/src/unstable/AndroidManifest.xml @@ -7,7 +7,7 @@ - + From dd1c04bea1461460444bd19756e0954ccd80def4 Mon Sep 17 00:00:00 2001 From: Kai Date: Fri, 6 Jun 2025 09:39:09 -0500 Subject: [PATCH 29/87] make the copied playlist name unique Changelog: changed --- .../fragment/mainactivity/main/PlaylistFragment.kt | 9 ++++++++- .../main/java/com/futo/platformplayer/models/Playlist.kt | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt index ba433563..c4b55e6f 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt @@ -170,7 +170,14 @@ class PlaylistFragment : MainFragment() { } private fun copyPlaylist(playlist: Playlist) { - StatePlaylists.instance.playlistStore.save(playlist.makeCopy()) + var copyNumber = 1 + var newName = "${playlist.name} (Copy)" + val playlists = StatePlaylists.instance.playlistStore.getItems() + while (playlists.any { it.name == newName }) { + copyNumber += 1 + newName = "${playlist.name} (Copy $copyNumber)" + } + StatePlaylists.instance.playlistStore.save(playlist.makeCopy(newName)) UIDialogs.toast("Playlist copied") } 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 d6bca755..9862e675 100644 --- a/app/src/main/java/com/futo/platformplayer/models/Playlist.kt +++ b/app/src/main/java/com/futo/platformplayer/models/Playlist.kt @@ -35,8 +35,8 @@ class Playlist { this.videos = ArrayList(list); } - fun makeCopy(): Playlist { - return Playlist("$name (Copy)", videos) + fun makeCopy(newName: String? = null): Playlist { + return Playlist(newName ?: name, videos) } companion object { From 19861fe8129d2a38b784e0bf43a65bddf87e131b Mon Sep 17 00:00:00 2001 From: Kai Date: Fri, 6 Jun 2025 13:40:20 -0500 Subject: [PATCH 30/87] fix https://github.com/futo-org/grayjay-android/issues/2316 Changelog: changed --- app/src/main/res/layout/fragment_sources.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/fragment_sources.xml b/app/src/main/res/layout/fragment_sources.xml index 80940400..ccd87712 100644 --- a/app/src/main/res/layout/fragment_sources.xml +++ b/app/src/main/res/layout/fragment_sources.xml @@ -8,7 +8,7 @@ android:orientation="vertical" android:paddingTop="10dp" android:animateLayoutChanges="true"> - - + From 623c47fa2e6a2165443b0d996d1fc66e8bcb66aa Mon Sep 17 00:00:00 2001 From: Kai Date: Fri, 6 Jun 2025 15:25:46 -0500 Subject: [PATCH 31/87] fix https://github.com/futo-org/grayjay-android/issues/2210 Changelog: changed --- .../fragment/mainactivity/main/VideoDetailFragment.kt | 7 +------ app/src/main/res/values/dimensions.xml | 1 + 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt index 53886862..3404de15 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt @@ -101,7 +101,7 @@ class VideoDetailFragment() : MainFragment() { } private fun isSmallWindow(): Boolean { - return resources.configuration.smallestScreenWidthDp < resources.getInteger(R.integer.column_width_dp) * 2 + return resources.configuration.smallestScreenWidthDp < resources.getInteger(R.integer.smallest_width_dp) } private fun isAutoRotateEnabled(): Boolean { @@ -627,11 +627,6 @@ class VideoDetailFragment() : MainFragment() { showSystemUI() } - // temporarily force the device to portrait if auto-rotate is disabled to prevent landscape when exiting full screen on a small device -// @SuppressLint("SourceLockedOrientationActivity") -// if (!isFullscreen && isSmallWindow() && !isAutoRotateEnabled() && !isMinimizingFromFullScreen) { -// activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT -// } updateOrientation(); _view?.allowMotion = !fullscreen; } 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 From 389798457b29f758bf9ca2ea488b2371e1db76ea Mon Sep 17 00:00:00 2001 From: Kai Date: Fri, 6 Jun 2025 15:57:09 -0500 Subject: [PATCH 32/87] navigate to playlist screen after copying Changelog: changed --- .../fragment/mainactivity/main/PlaylistFragment.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt index c4b55e6f..1e90e36e 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt @@ -178,6 +178,7 @@ class PlaylistFragment : MainFragment() { newName = "${playlist.name} (Copy $copyNumber)" } StatePlaylists.instance.playlistStore.save(playlist.makeCopy(newName)) + _fragment.navigate(withHistory = false) UIDialogs.toast("Playlist copied") } From 6598dff6dfba1f232ebc69b809c9b6d3aa52c055 Mon Sep 17 00:00:00 2001 From: Kai Date: Fri, 6 Jun 2025 23:35:59 -0500 Subject: [PATCH 33/87] add add to watch later setting add https://github.com/futo-org/grayjay-android/issues/2173 Changelog: added --- .../java/com/futo/platformplayer/Settings.kt | 7 +++-- .../futo/platformplayer/UISlideOverlays.kt | 2 ++ .../main/ArticleDetailFragment.kt | 2 ++ .../mainactivity/main/ChannelFragment.kt | 2 ++ .../mainactivity/main/ContentFeedView.kt | 2 ++ .../mainactivity/main/VideoDetailView.kt | 2 ++ .../platformplayer/states/StatePlaylists.kt | 27 +++++++++---------- app/src/main/res/values/strings.xml | 3 +++ 8 files changed, 31 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index e8f4f70a..c265fc40 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -999,10 +999,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.add_to_beginning_of_watch_later, FieldForm.TOGGLE, R.string.add_to_beginning_description, 4) + var addToBeginning: 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/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..74116069 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 @@ -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 : FeedViewShow confirmation dialog when deleting media from a playlist Allow duplicate playlist videos Allow adding duplicate videos to playlists + Save new videos to start 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 From 8f28653b288597fd75f83b09a71de97b6805da20 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Sat, 7 Jun 2025 16:44:20 +0200 Subject: [PATCH 34/87] Fix edgecases for new playback speed control --- .../fragment/mainactivity/main/VideoDetailView.kt | 4 ++-- .../java/com/futo/platformplayer/views/fields/ToggleField.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 8e514ce0..45ccf042 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -2166,9 +2166,9 @@ class VideoDetailView : ConstraintLayout { var playbackSpeedString = v; val stepSpeed = Settings.instance.playback.getPlaybackSpeedStep(); if(v == "+") - playbackSpeedString = String.format("%.2f", (currentPlaybackSpeed?.toDouble() ?: 1.0) + stepSpeed).toString(); + playbackSpeedString = String.format("%.2f", Math.min((currentPlaybackSpeed?.toDouble() ?: 1.0) + stepSpeed, 5.0)).toString(); else if(v == "-") - playbackSpeedString = String.format("%.2f", (currentPlaybackSpeed?.toDouble() ?: 1.0) - stepSpeed).toString(); + playbackSpeedString = String.format("%.2f", Math.max(0.1, (currentPlaybackSpeed?.toDouble() ?: 1.0) - stepSpeed)).toString(); val newPlaybackSpeed = playbackSpeedString.toDouble(); if (_isCasting) { val ad = StateCasting.instance.activeDevice ?: return@subscribe diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt b/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt index c419092c..f9b483b3 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt @@ -90,7 +90,7 @@ class ToggleField : TableRow, IField { val advancedFieldAttr = field.getAnnotation(AdvancedField::class.java) if(advancedFieldAttr != null || advanced) { - Logger.w("ToggleField", "Found advanced field: " + field.name); + Logger.w("ToggleField", "Found cccadvanced field: " + field.name); isAdvanced = true; } From 99dc50894c849239d560ad1d3f5e8c04bb6adf58 Mon Sep 17 00:00:00 2001 From: Kai Date: Mon, 9 Jun 2025 16:54:24 -0500 Subject: [PATCH 35/87] update text Changelog: changed --- app/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 03a21f85..de5148c2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -466,8 +466,8 @@ Show confirmation dialog when deleting media from a playlist Allow duplicate playlist videos Allow adding duplicate videos to playlists - Save new videos to start of watch later - When adding videos to watch later add them to the beginning of the list instead of the end + 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 From 9944842a2f9a16c167e22a08196b7c07a6b3116e Mon Sep 17 00:00:00 2001 From: Kai Date: Mon, 9 Jun 2025 17:02:55 -0500 Subject: [PATCH 36/87] Change adaptive streaming (HLS and Dash) quality to sort in descending quality to align with YouTube and the rest of Grayjay Changelog: changed --- .../fragment/mainactivity/main/VideoDetailView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 45ccf042..1a446b4a 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -1897,8 +1897,8 @@ class VideoDetailView : ConstraintLayout { } updateQualityFormatsOverlay( - videoTrackFormats.distinctBy { it.height }.sortedBy { it.height }, - audioTrackFormats.distinctBy { it.bitrate }.sortedBy { it.bitrate }); + videoTrackFormats.distinctBy { it.height }.sortedByDescending { it.height }, + audioTrackFormats.distinctBy { it.bitrate }.sortedByDescending { it.bitrate }); } } From fb12073a82ce85feef982a742e93098562ac9001 Mon Sep 17 00:00:00 2001 From: Kai Date: Tue, 10 Jun 2025 09:18:28 -0500 Subject: [PATCH 37/87] Only save brightness on resume fullscreen if use system brightness is enabled Changelog: changed --- .../fragment/mainactivity/main/VideoDetailView.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 45ccf042..b705fbb4 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -2539,7 +2539,9 @@ class VideoDetailView : ConstraintLayout { } fun saveBrightness() { - _player.gestureControl.saveBrightness() + if (Settings.instance.gestureControls.useSystemBrightness) { + _player.gestureControl.saveBrightness() + } } fun restoreBrightness() { _player.gestureControl.restoreBrightness() From 5e25a5054f64bb5db0a317b40f31f5bf68b85ab3 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Tue, 10 Jun 2025 17:33:14 +0200 Subject: [PATCH 38/87] Increase max comment length, Fix raw dash downloads ending too early, Fix playback tracker not working for downloaded videos --- .../platformplayer/downloads/VideoDownload.kt | 2 +- .../mainactivity/main/VideoDetailView.kt | 28 ++++++++++++++++++- app/src/main/res/layout/list_comment.xml | 2 +- 3 files changed, 29 insertions(+), 3 deletions(-) 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 f5cf534a..c0d9d0bf 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.toString()); + val url = foundTemplateUrl.replace("\$Number\$", (indexCounter + 1).toString()); val data = if(executor != null) executor.executeRequest("GET", url, null, mapOf()); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 45ccf042..fc24c707 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -1399,8 +1399,8 @@ class VideoDetailView : ConstraintLayout { onVideoChanged.emit(0, 0) } + val me = this; if (video is JSVideoDetails) { - val me = this; fragment.lifecycleScope.launch(Dispatchers.IO) { try { //TODO: Implement video.getContentChapters() @@ -1457,6 +1457,32 @@ class VideoDetailView : ConstraintLayout { } }; } + else { + fragment.lifecycleScope.launch(Dispatchers.IO) { + try { + if (!StateApp.instance.privateMode) { + val stopwatch = com.futo.platformplayer.debug.Stopwatch() + var tracker = video.getPlaybackTracker() + Logger.i(TAG, "video.getPlaybackTracker took ${stopwatch.elapsedMs}ms") + + if (tracker == null) { + stopwatch.reset() + tracker = StatePlatform.instance.getPlaybackTracker(video.url); + Logger.i( + TAG, + "StatePlatform.instance.getPlaybackTracker took ${stopwatch.elapsedMs}ms" + ) + } + + if (me.video == video) + me._playbackTracker = tracker; + } else if (me.video == video) + me._playbackTracker = null; + } catch (ex: Throwable) { + Logger.e(TAG, "Playback tracker failed", ex); + } + } + } val ref = Models.referenceFromBuffer(video.url.toByteArray()) val extraBytesRef = video.id.value?.let { if (it.isNotEmpty()) it.toByteArray() else null } diff --git a/app/src/main/res/layout/list_comment.xml b/app/src/main/res/layout/list_comment.xml index 91a1d0a9..b39af456 100644 --- a/app/src/main/res/layout/list_comment.xml +++ b/app/src/main/res/layout/list_comment.xml @@ -80,7 +80,7 @@ android:isScrollContainer="false" android:textColor="#CCCCCC" android:textSize="13sp" - android:maxLines="100" + android:maxLines="150" app:layout_constraintTop_toBottomOf="@id/text_metadata" app:layout_constraintLeft_toRightOf="@id/image_thumbnail" app:layout_constraintRight_toRightOf="parent" From 2825db88a5c762836d04f9764787a106ceb85de9 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Tue, 10 Jun 2025 18:56:19 +0200 Subject: [PATCH 39/87] Minor playback tracker fix, submodules --- .../fragment/mainactivity/main/VideoDetailView.kt | 2 +- app/src/stable/assets/sources/nebula | 2 +- app/src/stable/assets/sources/rumble | 2 +- app/src/stable/assets/sources/spotify | 2 +- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/nebula | 2 +- app/src/unstable/assets/sources/rumble | 2 +- app/src/unstable/assets/sources/spotify | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 53c1c6fa..9011c1e5 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -1484,7 +1484,7 @@ class VideoDetailView : ConstraintLayout { ) } - if (me.video == video) + if (me.video.url == video.url && !me.video.url.isNullOrBlank()) me._playbackTracker = tracker; } else if (me.video == video) me._playbackTracker = null; diff --git a/app/src/stable/assets/sources/nebula b/app/src/stable/assets/sources/nebula index 97a5ad5a..880da6a0 160000 --- a/app/src/stable/assets/sources/nebula +++ b/app/src/stable/assets/sources/nebula @@ -1 +1 @@ -Subproject commit 97a5ad5a37c40ed68cccbab05ba16926a0aaee41 +Subproject commit 880da6a015fa8e109cd475d1d87866b8394de0a5 diff --git a/app/src/stable/assets/sources/rumble b/app/src/stable/assets/sources/rumble index 3bbce817..401274b1 160000 --- a/app/src/stable/assets/sources/rumble +++ b/app/src/stable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit 3bbce81794b175410fe79a12c28f9ba966de07da +Subproject commit 401274b1ec2806b3a61f877080ff023b4bf5dc0d diff --git a/app/src/stable/assets/sources/spotify b/app/src/stable/assets/sources/spotify index 1d884f50..d0258043 160000 --- a/app/src/stable/assets/sources/spotify +++ b/app/src/stable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit 1d884f50abf88acf8fec77cefda2f6ffa0d932b5 +Subproject commit d025804364c4dd283be8845a35208c674841911a diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 6d6838e2..2e258294 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 6d6838e2a472786e652521bd5fa0dff8f9364dc2 +Subproject commit 2e25829494addebbf9292b7c68b2d6130e1fc461 diff --git a/app/src/unstable/assets/sources/nebula b/app/src/unstable/assets/sources/nebula index 97a5ad5a..880da6a0 160000 --- a/app/src/unstable/assets/sources/nebula +++ b/app/src/unstable/assets/sources/nebula @@ -1 +1 @@ -Subproject commit 97a5ad5a37c40ed68cccbab05ba16926a0aaee41 +Subproject commit 880da6a015fa8e109cd475d1d87866b8394de0a5 diff --git a/app/src/unstable/assets/sources/rumble b/app/src/unstable/assets/sources/rumble index 3bbce817..401274b1 160000 --- a/app/src/unstable/assets/sources/rumble +++ b/app/src/unstable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit 3bbce81794b175410fe79a12c28f9ba966de07da +Subproject commit 401274b1ec2806b3a61f877080ff023b4bf5dc0d diff --git a/app/src/unstable/assets/sources/spotify b/app/src/unstable/assets/sources/spotify index 1d884f50..d0258043 160000 --- a/app/src/unstable/assets/sources/spotify +++ b/app/src/unstable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit 1d884f50abf88acf8fec77cefda2f6ffa0d932b5 +Subproject commit d025804364c4dd283be8845a35208c674841911a diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 6d6838e2..2e258294 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 6d6838e2a472786e652521bd5fa0dff8f9364dc2 +Subproject commit 2e25829494addebbf9292b7c68b2d6130e1fc461 From a9dc0381903baa8cd52b4e4cba019f9c20f66668 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Tue, 10 Jun 2025 19:20:50 +0200 Subject: [PATCH 40/87] Build fix --- .../fragment/mainactivity/main/VideoDetailView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 9011c1e5..65d81c55 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -1484,7 +1484,7 @@ class VideoDetailView : ConstraintLayout { ) } - if (me.video.url == video.url && !me.video.url.isNullOrBlank()) + if (me.video.url == video.url && !video.url.isNullOrBlank()) me._playbackTracker = tracker; } else if (me.video == video) me._playbackTracker = null; From 4164b1a3f839f16a17d60d97d411edd1d43292d7 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Tue, 10 Jun 2025 19:25:43 +0200 Subject: [PATCH 41/87] Build fix --- .../fragment/mainactivity/main/VideoDetailView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 65d81c55..f5f252d0 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -1484,7 +1484,7 @@ class VideoDetailView : ConstraintLayout { ) } - if (me.video.url == video.url && !video.url.isNullOrBlank()) + if (me.video?.url == video.url && !video.url.isNullOrBlank()) me._playbackTracker = tracker; } else if (me.video == video) me._playbackTracker = null; From 19c84475db7cfde73cc987d49906880d83c75733 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Tue, 10 Jun 2025 23:27:01 +0200 Subject: [PATCH 42/87] Hotfix playback speed for non-dot locales --- .../fragment/mainactivity/main/VideoDetailView.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index f5f252d0..a6241c29 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -174,6 +174,7 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import userpackage.Protocol import java.time.OffsetDateTime +import java.util.Locale import kotlin.math.abs import kotlin.math.roundToLong @@ -2192,19 +2193,19 @@ class VideoDetailView : ConstraintLayout { if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply { val playbackSpeeds = Settings.instance.playback.getPlaybackSpeeds(); val format = if(playbackSpeeds.size < 20) "%.2f" else "%.1f"; - val playbackLabels = playbackSpeeds.map { String.format(format, it) }.toMutableList(); + val playbackLabels = playbackSpeeds.map { String.format(Locale.US, format, it) }.toMutableList(); playbackLabels.add("+"); playbackLabels.add(0, "-"); - setButtons(playbackLabels, String.format(format, currentPlaybackRate)); + setButtons(playbackLabels, String.format(Locale.US, format, currentPlaybackRate)); onClick.subscribe { v -> val currentPlaybackSpeed = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate(); var playbackSpeedString = v; val stepSpeed = Settings.instance.playback.getPlaybackSpeedStep(); if(v == "+") - playbackSpeedString = String.format("%.2f", Math.min((currentPlaybackSpeed?.toDouble() ?: 1.0) + stepSpeed, 5.0)).toString(); + playbackSpeedString = String.format(Locale.US, "%.2f", Math.min((currentPlaybackSpeed?.toDouble() ?: 1.0) + stepSpeed, 5.0)).toString(); else if(v == "-") - playbackSpeedString = String.format("%.2f", Math.max(0.1, (currentPlaybackSpeed?.toDouble() ?: 1.0) - stepSpeed)).toString(); + playbackSpeedString = String.format(Locale.US, "%.2f", Math.max(0.1, (currentPlaybackSpeed?.toDouble() ?: 1.0) - stepSpeed)).toString(); val newPlaybackSpeed = playbackSpeedString.toDouble(); if (_isCasting) { val ad = StateCasting.instance.activeDevice ?: return@subscribe @@ -2212,11 +2213,11 @@ class VideoDetailView : ConstraintLayout { return@subscribe } - qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", newPlaybackSpeed)})"); + qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})"); ad.changeSpeed(newPlaybackSpeed) setSelected(playbackSpeedString); } else { - qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", newPlaybackSpeed)})"); + qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})"); _player.setPlaybackRate(playbackSpeedString.toFloat()); setSelected(playbackSpeedString); } From 9e7b936663ed3eab47b8547e6ab8fa6b68f4071d Mon Sep 17 00:00:00 2001 From: Koen J Date: Wed, 11 Jun 2025 17:03:53 +0200 Subject: [PATCH 43/87] Implemented hold to play video at 2x speed gesture. --- .../views/behavior/GestureControlView.kt | 22 ++++++++++++++++++- .../views/video/FutoVideoPlayer.kt | 17 ++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) 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..bae761fb 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 @@ -92,6 +92,7 @@ class GestureControlView : LinearLayout { private var _surfaceView: View? = null private var _layoutIndicatorFill: FrameLayout; private var _layoutIndicatorFit: FrameLayout; + private var _speedHolding = false private val _gestureController: GestureDetectorCompat; @@ -103,6 +104,8 @@ class GestureControlView : LinearLayout { val onZoom = Event1(); val onSoundAdjusted = Event1(); val onToggleFullscreen = Event0(); + val onSpeedHoldStart = Event0() + val onSpeedHoldEnd = Event0() var fullScreenGestureEnabled = true @@ -216,7 +219,19 @@ 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) { + _speedHolding = true + onSpeedHoldStart.emit() + } + } }); _gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener { @@ -309,6 +324,11 @@ 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 + onSpeedHoldEnd.emit() + } + cancelHideJob(); if (_skipping) { 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..4be38cd3 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(2f) + 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) { From 9bea1563ca53b811dc4771f39734a26233042f82 Mon Sep 17 00:00:00 2001 From: zvonimir Date: Wed, 11 Jun 2025 18:36:05 +0200 Subject: [PATCH 44/87] Revert downloads patch which broke downloads --- .../java/com/futo/platformplayer/downloads/VideoDownload.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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()); From 522704139857ada51c65e22b9006344973c878fd Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 12 Jun 2025 10:33:05 +0200 Subject: [PATCH 45/87] Added setting for hold playback speed increase. Implemented chromecast playback rate adjustment in range [1, 2]. Implemented hold playback speed increase pill. --- .../futo/platformplayer/SyncServerTests.kt | 4 +-- .../java/com/futo/platformplayer/SyncTests.kt | 4 +-- .../java/com/futo/platformplayer/Settings.kt | 17 +++++++++ .../casting/ChomecastCastingDevice.kt | 19 +++++++++- .../views/behavior/GestureControlView.kt | 26 ++++++++++++++ .../platformplayer/views/casting/CastView.kt | 17 +++++++++ .../views/video/FutoVideoPlayer.kt | 2 +- .../main/res/layout/view_gesture_controls.xml | 35 +++++++++++++++++++ app/src/main/res/values/strings.xml | 12 +++++++ 9 files changed, 130 insertions(+), 6 deletions(-) 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/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index e8f4f70a..372501e7 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -584,6 +584,23 @@ 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 = 3; + + fun getHoldPlaybackSpeed(): Double { + return when(holdPlaybackSpeed) { + 0 -> 1.25 + 1 -> 1.5 + 2 -> 1.75 + 3 -> 2.0 + 4 -> 2.25 + 5 -> 2.5 + 6 -> 2.75 + else -> 3.0 + } + } } @FormField(R.string.comments, "group", R.string.comments_description, 6) 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/views/behavior/GestureControlView.kt b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt index bae761fb..4bb864da 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 @@ -94,6 +100,10 @@ class GestureControlView : LinearLayout { 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; val isUserGesturing get() = _rewinding || _skipping || _adjustingBrightness || _adjustingSound || _adjustingFullscreenUp || _adjustingFullscreenDown || _isPanning || _isZooming; @@ -127,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 { @@ -229,6 +242,7 @@ class GestureControlView : LinearLayout { && !_isPanning && !_isZooming) { _speedHolding = true + showHoldSpeedControls() onSpeedHoldStart.emit() } } @@ -316,6 +330,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; @@ -326,6 +351,7 @@ class GestureControlView : LinearLayout { if (ev.action == MotionEvent.ACTION_UP && _speedHolding) { _speedHolding = false + hideHoldSpeedControls() onSpeedHoldEnd.emit() } 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/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt index 4be38cd3..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 @@ -261,7 +261,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase { exoPlayer?.player?.let { player -> _speedHoldWasPlaying = player.isPlaying _speedHoldPrevRate = getPlaybackRate() - setPlaybackRate(2f) + setPlaybackRate(Settings.instance.playback.getHoldPlaybackSpeed().toFloat()) player.play() } } diff --git a/app/src/main/res/layout/view_gesture_controls.xml b/app/src/main/res/layout/view_gesture_controls.xml index ea175d31..ccf8efcd 100644 --- a/app/src/main/res/layout/view_gesture_controls.xml +++ b/app/src/main/res/layout/view_gesture_controls.xml @@ -195,4 +195,39 @@ app:layout_constraintEnd_toEndOf="parent" android:visibility="gone"/> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b2513e21..f4a51bd0 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 @@ -1106,6 +1108,16 @@ 4.0 5.0 + + 1.25 + 1.5 + 1.75 + 2.0 + 2.25 + 2.5 + 2.75 + 3.0 + 0.25 0.5 From 13100dc38d9ee70dc8c6caf865054f12d87cc13b Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 12 Jun 2025 11:21:00 +0200 Subject: [PATCH 46/87] Minor fix in playback speed setting. --- app/src/main/java/com/futo/platformplayer/Settings.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 372501e7..421c37be 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -598,7 +598,8 @@ class Settings : FragmentedStorageFileJson() { 4 -> 2.25 5 -> 2.5 6 -> 2.75 - else -> 3.0 + 7 -> 3.0 + else -> 2.0 } } } From 47027877845161663e129aa9938f431ad532d06f Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Fri, 13 Jun 2025 17:47:22 +0200 Subject: [PATCH 47/87] WIP --- .../platforms/js/internal/JSHttpClient.kt | 2 +- .../engine/packages/PackageBridge.kt | 4 ++++ .../engine/packages/PackageHttp.kt | 22 ++++++++++++++----- .../SubscriptionsTaskFetchAlgorithm.kt | 2 +- 4 files changed, 23 insertions(+), 7 deletions(-) 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..eec4414a 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 @@ -69,7 +69,7 @@ class JSHttpClient : ManagedHttpClient { override fun clone(): ManagedHttpClient { val newClient = JSHttpClient(_jsClient, _auth); - newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) }) + //newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) }) return newClient; } 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..a5f18705 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 @@ -141,6 +141,10 @@ class PackageBridge : V8Package { timeoutMap.remove(id); } } + @V8Function + fun sleep(length: Int) { + Thread.sleep(length.toLong()); + } @V8Function fun toast(str: String) { 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..aa78a184 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 @@ -429,8 +429,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 +467,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 { 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) { From 58c9aeb1a2ee64ffec1c255fafc2d29b1bfd33fe Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Sat, 14 Jun 2025 15:51:31 +0200 Subject: [PATCH 48/87] WIP: V8 update, package http fixes, ReloadRequiredException support, other fixes. Currently broken in situations where setTimeout is used --- app/build.gradle | 3 +- app/src/main/assets/scripts/source.js | 6 +++ .../api/media/platforms/js/JSClient.kt | 20 ++++++- .../sources/JSDashManifestRawAudioSource.kt | 8 ++- .../models/sources/JSDashManifestRawSource.kt | 8 ++- .../platforms/js/models/sources/JSSource.kt | 2 + .../futo/platformplayer/engine/V8Plugin.kt | 52 ++++++++++++------- .../ScriptReloadRequiredException.kt | 20 +++++++ .../engine/packages/PackageBridge.kt | 7 +++ .../engine/packages/PackageHttp.kt | 38 +++++++++----- .../mainactivity/main/VideoDetailView.kt | 10 ++++ .../platformplayer/states/StatePlatform.kt | 36 ++++++++++++- .../views/video/FutoVideoPlayerBase.kt | 39 +++++++++++--- 13 files changed, 205 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptReloadRequiredException.kt diff --git a/app/build.gradle b/app/build.gradle index fcbd422c..8c30e58d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -179,7 +179,8 @@ dependencies { implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject) //JS - implementation("com.caoccao.javet:javet-android:3.0.2") + //implementation("com.caoccao.javet:javet-android:3.0.2") + implementation 'com.caoccao.javet:javet-v8-android:4.1.4' //Exoplayer implementation 'androidx.media3:media3-exoplayer:1.2.1' 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/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt index 476bad8a..6ec1eae2 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 @@ -62,6 +62,7 @@ import com.futo.platformplayer.states.StatePlugins 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 @@ -106,6 +107,8 @@ open class JSClient : IPlatformClient { return _busyAction; } + val declareOnEnable = HashMap(); + val settings: HashMap get() = descriptor.settings; val flags: Array; @@ -213,6 +216,10 @@ open class JSClient : IPlatformClient { return plugin.httpClientOthers[id]; } + fun setReloadData(data: String?) { + declareOnEnable.put("__reloadData", data ?: ""); + } + override fun initialize() { if (_initialized) return @@ -263,7 +270,13 @@ open class JSClient : IPlatformClient { fun 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")) + declareOnEnable.remove("__reloadData"); _enabled = true; } @JSDocs(1, "source.saveState()", "Provide a string that is passed to enable for quicker startup of multiple instances") @@ -735,8 +748,12 @@ open class JSClient : IPlatformClient { } - private fun isBusyWith(actionName: String, handle: ()->T): T { + + fun isBusyWith(actionName: String, handle: ()->T): T { + val busyId = kotlin.random.Random.nextInt(9999); try { + + Logger.v(TAG, "Busy with [${actionName}] (${busyId})") synchronized(_busyLock) { _busyCounter++; } @@ -748,6 +765,7 @@ open class JSClient : IPlatformClient { synchronized(_busyLock) { _busyCounter--; } + Logger.v(TAG, "Busy done [${actionName}] (${busyId})") } } private fun isBusyWith(handle: ()->T): T { 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..2c6d4b35 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,12 +62,16 @@ 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){ 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..a9c070f7 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 @@ -67,13 +67,17 @@ 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){ 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..ee586083 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 @@ -75,9 +75,11 @@ abstract class JSSource { if (!hasRequestExecutor || _obj.isClosed) return null; + Logger.v("JSSource", "Request executor for [${type}] requesting"); val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") { _obj.invoke("getRequestExecutor", arrayOf()); }; + Logger.v("JSSource", "Request executor for [${type}] received"); if (result !is V8ValueObject) return null; 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..96593f48 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -15,6 +15,7 @@ import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient +import com.futo.platformplayer.assume import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.engine.exceptions.NoInternetException import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException @@ -26,6 +27,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 @@ -186,6 +188,7 @@ class V8Plugin { Logger.i(TAG, "Stopping plugin [${config.name}]"); isStopped = true; whenNotBusy { + Logger.i(TAG, "Plugin stopping"); synchronized(_runtimeLock) { isStopped = true; @@ -200,7 +203,7 @@ class V8Plugin { _runtime = null; if(!it.isClosed && !it.isDead) { try { - it.close(); + it.close(true); } catch(ex: JavetException) { //In case race conditions are going on, already closed runtimes are fine. @@ -211,6 +214,7 @@ class V8Plugin { Logger.i(TAG, "Stopped plugin [${config.name}]"); }; } + Logger.i(TAG, "Plugin stopped"); onStopped.emit(this); } } @@ -327,26 +331,38 @@ 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(); + if(executeEx.scriptingError?.context is V8ValueObject) { + val obj = executeEx.scriptingError?.context as V8ValueObject + if(obj.has("plugin_type") == true) { + val pluginType = obj.get("plugin_type").toString(); - //Captcha - if (pluginType == "CaptchaRequiredException") { - throw ScriptCaptchaRequiredException(config, - executeEx.scriptingError.context["url"]?.toString(), - executeEx.scriptingError.context["body"]?.toString(), - executeEx, executeEx.scriptingError?.stack, codeStripped); + //Captcha + if (pluginType == "CaptchaRequiredException") { + throw ScriptCaptchaRequiredException(config, + obj.get("url")?.toString(), + obj.get("body")?.toString(), + executeEx, executeEx.scriptingError?.stack, codeStripped); + } + + //Reload Required + if (pluginType == "ReloadRequiredException") { + throw ScriptReloadRequiredException(config, + obj.get("message")?.toString(), + obj.get("reloadData")?.toString(), + executeEx, executeEx.scriptingError?.stack, codeStripped); + } + + //Others + throwExceptionFromV8( + config, + pluginType, + (extractJSExceptionMessage(executeEx) ?: ""), + 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); } 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..98c0635d --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptReloadRequiredException.kt @@ -0,0 +1,20 @@ +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.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 { + 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/packages/PackageBridge.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt index a5f18705..5450c5eb 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 @@ -78,6 +78,13 @@ class PackageBridge : V8Package { return "android"; } + @V8Property + fun supportedFeatures(): Array { + return arrayOf( + "ReloadRequiredException" + ); + } + @V8Property fun supportedContent(): Array { return arrayOf( 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 aa78a184..0b049ecb 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; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index a6241c29..08e04730 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -93,6 +93,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptAgeException import com.futo.platformplayer.engine.exceptions.ScriptException 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.ScriptUnavailableException import com.futo.platformplayer.exceptions.UnsupportedCastException import com.futo.platformplayer.fixHtmlLinks @@ -608,6 +609,10 @@ class VideoDetailView : ConstraintLayout { } } + _player.onReloadRequired.subscribe { + fetchVideo(); + } + _player.onPlayChanged.subscribe { if (StateCasting.instance.activeDevice == null) { handlePlayChanged(it); @@ -3025,6 +3030,11 @@ class VideoDetailView : ConstraintLayout { return@TaskHandler result; }) .success { setVideoDetails(it, true) } + .exception { + StatePlatform.instance.handleReloadRequired(it, { + fetchVideo(); + }); + } .exception { Logger.w(TAG, "exception", it) 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..8cf8f080 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(); }; } 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..ab0bf383 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; @@ -585,6 +590,12 @@ abstract class FutoVideoPlayerBase : RelativeLayout { } } } + catch(reloadRequired: ScriptReloadRequiredException) { + Logger.i(TAG, "Reload required detected"); + StatePlatform.instance.handleReloadRequired(reloadRequired, { + onReloadRequired.emit(); + }); + } catch(ex: Throwable) { Logger.e(TAG, "DashRaw generator failed", ex); } @@ -677,15 +688,29 @@ abstract class FutoVideoPlayerBase : RelativeLayout { 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); + try { + 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); + } } } + catch(reloadRequired: ScriptReloadRequiredException) { + Logger.i(TAG, "Reload required detected"); + val plugin = audioSource.getUnderlyingPlugin(); + if(plugin == null) + return@launch; + StatePlatform.instance.reEnableClient(plugin.id, { + onReloadRequired.emit(); + }); + } + catch(ex: Throwable) { + + } } return false; } From bcab3bccbc467d5b48d719ad109e0867c283ad37 Mon Sep 17 00:00:00 2001 From: Koen J Date: Mon, 16 Jun 2025 10:43:57 +0200 Subject: [PATCH 49/87] Fixed crash when signature fields are wrongly populated. --- .../media/platforms/js/SourcePluginConfig.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) 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 From 2fca7e9a01e6c7e42840463015501032892bd9c2 Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Mon, 16 Jun 2025 14:13:47 +0200 Subject: [PATCH 50/87] Locking of most known v8 interactions, fix returning previously returned jvm objects, Related fixes --- .../api/media/platforms/js/JSClient.kt | 36 +++--- .../api/media/platforms/js/models/JSPager.kt | 38 +++--- .../platforms/js/models/JSPlaybackTracker.kt | 66 +++++++---- .../platforms/js/models/JSRequestExecutor.kt | 110 +++++++++--------- .../platforms/js/models/JSVideoDetails.kt | 4 +- .../platforms/js/models/sources/JSSource.kt | 6 +- .../futo/platformplayer/engine/V8Plugin.kt | 99 +++++++++++++--- .../engine/internal/V8BindObject.kt | 4 +- .../engine/packages/PackageBridge.kt | 25 +++- .../engine/packages/PackageHttp.kt | 20 +++- .../mainactivity/main/VideoDetailView.kt | 4 +- .../views/video/FutoVideoPlayerBase.kt | 5 +- 12 files changed, 274 insertions(+), 143 deletions(-) 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 6ec1eae2..144faa2c 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 @@ -99,10 +99,8 @@ 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; } @@ -225,9 +223,12 @@ open class JSClient : IPlatformClient { Logger.i(TAG, "Plugin [${config.name}] initializing"); plugin.start(); + Logger.i(TAG, "Plugin [${config.name}] started"); plugin.execute("plugin.config = ${Json.encodeToString(config)}"); plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})"); + Logger.i(TAG, "Plugin [${config.name}] configs set"); + descriptor.appSettings.loadDefaults(descriptor.config); _initialized = true; @@ -254,6 +255,7 @@ open class JSClient : IPlatformClient { hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false, hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false ); + Logger.i(TAG, "Plugin [${config.name}] capabilities retrieved"); try { if (capabilities.hasGetChannelTemplateByClaimMap) @@ -565,7 +567,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; } @@ -747,25 +749,23 @@ open class JSClient : IPlatformClient { return urls; } - + fun busy(handle: ()->T): T { + return _plugin.busy { + return@busy handle(); + } + } fun isBusyWith(actionName: String, handle: ()->T): T { - val busyId = kotlin.random.Random.nextInt(9999); - try { + //val busyId = kotlin.random.Random.nextInt(9999); + return busy { + try { + _busyAction = actionName; + return@busy handle(); - Logger.v(TAG, "Busy with [${actionName}] (${busyId})") - synchronized(_busyLock) { - _busyCounter++; } - _busyAction = actionName; - return handle(); - } - finally { - _busyAction = ""; - synchronized(_busyLock) { - _busyCounter--; + finally { + _busyAction = ""; } - Logger.v(TAG, "Busy done [${actionName}] (${busyId})") } } private fun isBusyWith(handle: ()->T): T { 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/JSVideoDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt index da495498..799c737f 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)); @@ -86,7 +88,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { val tracker = _content.invoke("getPlaybackTracker", arrayOf()) ?: return@catchScriptErrors null; if(tracker is V8ValueObject) - return@catchScriptErrors JSPlaybackTracker(_pluginConfig, tracker); + return@catchScriptErrors JSPlaybackTracker(_plugin, tracker); else return@catchScriptErrors null; }; 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 ee586083..649c74cf 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 @@ -77,7 +77,11 @@ abstract class JSSource { Logger.v("JSSource", "Request executor for [${type}] requesting"); val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") { - _obj.invoke("getRequestExecutor", arrayOf()); + _plugin.isBusyWith("getRequestExecutor") { + _plugin.getUnderlyingPlugin().busy { + _obj.invoke("getRequestExecutor", arrayOf()); + } + } }; Logger.v("JSSource", "Request executor for [${type}] received"); 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 96593f48..462fb192 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -1,9 +1,11 @@ package com.futo.platformplayer.engine import android.content.Context +import com.caoccao.javet.entities.JavetEntityError import com.caoccao.javet.exceptions.JavetCompilationException import com.caoccao.javet.exceptions.JavetException import com.caoccao.javet.exceptions.JavetExecutionException +import com.caoccao.javet.interfaces.IJavetEntityError import com.caoccao.javet.interop.V8Host import com.caoccao.javet.interop.V8Runtime import com.caoccao.javet.interop.options.V8Flags @@ -42,6 +44,9 @@ 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.Semaphore +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock class V8Plugin { val config: IV8PluginConfig; @@ -70,9 +75,10 @@ class V8Plugin { 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 _busyCounterLock = Object(); + //private var _busyCounter = 0; + private val _busyLock = ReentrantLock()//Semaphore(1); + val isBusy get() = _busyLock.isLocked;//synchronized(_busyCounterLock) { _busyCounter > 0 }; var allowDevSubmit: Boolean = false private set(value) { @@ -146,14 +152,19 @@ class V8Plugin { val host = V8Host.getV8Instance(); val options = host.jsRuntimeType.getRuntimeOptions(); + Logger.i(TAG, "Plugin [${config.name}] start: Creating runtime") + _runtime = host.createV8Runtime(options); if (!host.isIsolateCreated) throw IllegalStateException("Isolate not created"); + Logger.i(TAG, "Plugin [${config.name}] start: Created runtime") + //Setup bridge _runtime?.let { it.converter = V8Converter(); + Logger.i(TAG, "Plugin [${config.name}] start: Loading packages") for (pack in _depsPackages) { if (pack.variableName != null) it.createV8ValueObject().use { v8valueObject -> @@ -166,6 +177,8 @@ class V8Plugin { } } + Logger.i(TAG, "Plugin [${config.name}] start: Loading deps") + //Load deps for (dep in _deps) catchScriptErrors("Dep[${dep.key}]") { @@ -176,20 +189,23 @@ class V8Plugin { if (config.allowEval) it.allowEval(true); + Logger.i(TAG, "Plugin [${config.name}] start: Loading script") //Load plugin catchScriptErrors("Plugin[${config.name}]") { it.getExecutor(script).executeVoid() }; isStopped = false; + Logger.i(TAG, "Plugin [${config.name}] start: Script loaded") } } } 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; //Cleanup http @@ -203,7 +219,7 @@ class V8Plugin { _runtime = null; if(!it.isClosed && !it.isDead) { try { - it.close(true); + it.close(); } catch(ex: JavetException) { //In case race conditions are going on, already closed runtimes are fine. @@ -219,6 +235,12 @@ class V8Plugin { } } + 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); } @@ -227,6 +249,14 @@ class V8Plugin { if(isStopped) throw PluginEngineStoppedException(config, "Instance is stopped", js); + return busy { + + val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet"); + return@busy catchScriptErrors("Plugin[${config.name}]", js) { + runtime.getExecutor(js).execute() + }; + } + /* synchronized(_busyCounterLock) { _busyCounter++; } @@ -249,11 +279,26 @@ class V8Plugin { _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 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 } } + /* + fun whenNotBusyBlocking(handler: (V8Plugin)->T): T { + while(true) { + synchronized(_busyCounterLock) { + if(_busyCounter == 0) + { + return handler(this); + } + } + Thread.sleep(1); + } + } + */ + /* fun whenNotBusy(handler: (V8Plugin)->Unit) { synchronized(_busyCounterLock) { if(_busyCounter == 0) @@ -264,12 +309,25 @@ class V8Plugin { if(it == 0) { Logger.w(TAG, "V8Plugin afterBusy handled"); afterBusy.remove(tag); - handler(this); + + var failed = false; + synchronized(_busyCounterLock) { + if(_busyCounter > 0) { + failed = true; + return@synchronized + } + handler(this); + } + if(failed) + busy { + handler(this); + } } } } } } + */ private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? { //TODO: Auto get all package types? @@ -331,24 +389,29 @@ 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 is V8ValueObject) { - val obj = executeEx.scriptingError?.context as V8ValueObject - if(obj.has("plugin_type") == true) { - val pluginType = obj.get("plugin_type").toString(); + 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(); + //val pluginType = obj.get("plugin_type").toString(); //Captcha if (pluginType == "CaptchaRequiredException") { throw ScriptCaptchaRequiredException(config, - obj.get("url")?.toString(), - obj.get("body")?.toString(), + obj.context["url"]?.toString(), + obj.context["body"]?.toString(), + //obj.get("url")?.toString(), + //obj.get("body")?.toString(), executeEx, executeEx.scriptingError?.stack, codeStripped); } //Reload Required if (pluginType == "ReloadRequiredException") { throw ScriptReloadRequiredException(config, - obj.get("message")?.toString(), - obj.get("reloadData")?.toString(), + obj.context["msg"]?.toString(), + obj.context["reloadData"]?.toString(), + //obj.get("message")?.toString(), + //obj.get("reloadData")?.toString(), executeEx, executeEx.scriptingError?.stack, codeStripped); } 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 5450c5eb..b4cc821d 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 @@ -112,28 +113,42 @@ class PackageBridge : V8Package { @V8Function fun setTimeout(func: V8ValueFunction, timeout: Long): Int { val id = timeoutCounter++; - val funcClone = func.toClone() StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { delay(timeout); + if(_plugin.isStopped) + return@launch; synchronized(timeoutMap) { if(!timeoutMap.contains(id)) { - JavetResourceUtils.safeClose(funcClone); + _plugin.busy { + if(!_plugin.isStopped) + JavetResourceUtils.safeClose(funcClone); + } return@launch; } timeoutMap.remove(id); } try { - _plugin.whenNotBusy { - funcClone.callVoid(null, arrayOf()); + Logger.v(TAG, "Timeout started [${id}]"); + _plugin.busy { + Logger.v(TAG, "Timeout call started [${id}]"); + if(!_plugin.isStopped) + funcClone.callVoid(null, arrayOf()); + Logger.v(TAG, "Timeout call ended [${id}]"); } + Logger.v(TAG, "Timeout resolved [${id}]"); } catch(ex: Throwable) { Logger.e(TAG, "Failed timeout callback", ex); } finally { - JavetResourceUtils.safeClose(funcClone); + _plugin.busy { + if(!_plugin.isStopped) + JavetResourceUtils.safeClose(funcClone); + } + //_plugin.whenNotBusy { + //} } }; synchronized(timeoutMap) { 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 0b049ecb..2930a476 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 @@ -656,7 +656,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); @@ -666,7 +668,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) {} } @@ -675,7 +679,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); @@ -686,7 +692,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); @@ -702,7 +710,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/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 08e04730..452af019 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -2497,7 +2497,9 @@ class VideoDetailView : ConstraintLayout { val url = _url; if (!url.isNullOrBlank()) { - setLoading(true); + fragment.lifecycleScope.launch(Dispatchers.Main) { + setLoading(true); + } _taskLoadVideo.run(url); } } 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 ab0bf383..b0afd83f 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 @@ -570,7 +570,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { 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); @@ -593,6 +593,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { catch(reloadRequired: ScriptReloadRequiredException) { Logger.i(TAG, "Reload required detected"); StatePlatform.instance.handleReloadRequired(reloadRequired, { + Logger.i(TAG, "ReloadRequired started reloading video"); onReloadRequired.emit(); }); } @@ -704,9 +705,11 @@ abstract class FutoVideoPlayerBase : RelativeLayout { val plugin = audioSource.getUnderlyingPlugin(); if(plugin == null) return@launch; + /* StatePlatform.instance.reEnableClient(plugin.id, { onReloadRequired.emit(); }); + */ } catch(ex: Throwable) { From 86bd71b89c2e0f21d893baf87a113cdada86201f Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Mon, 16 Jun 2025 14:19:23 +0200 Subject: [PATCH 51/87] Fix edgecase --- .../main/java/com/futo/platformplayer/engine/V8Plugin.kt | 4 ++++ .../futo/platformplayer/views/video/FutoVideoPlayerBase.kt | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) 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 462fb192..5b6ffd91 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -58,6 +58,8 @@ class V8Plugin { val httpClientAuth: ManagedHttpClient get() = _clientAuth; val httpClientOthers: Map get() = _clientOthers; + var startId: Int = 0; + fun registerHttpClient(client: JSHttpClient) { synchronized(_clientOthers) { _clientOthers.put(client.clientId, client); @@ -148,6 +150,7 @@ class V8Plugin { synchronized(_runtimeLock) { if (_runtime != null) return; + startId + 1; //V8RuntimeOptions.V8_FLAGS.setUseStrict(true); val host = V8Host.getV8Instance(); val options = host.jsRuntimeType.getRuntimeOptions(); @@ -207,6 +210,7 @@ class V8Plugin { if(isStopped) return@busy; isStopped = true; + startId = -1; //Cleanup http for(pack in _depsPackages) { 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 b0afd83f..22650adb 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 @@ -593,7 +593,6 @@ abstract class FutoVideoPlayerBase : RelativeLayout { catch(reloadRequired: ScriptReloadRequiredException) { Logger.i(TAG, "Reload required detected"); StatePlatform.instance.handleReloadRequired(reloadRequired, { - Logger.i(TAG, "ReloadRequired started reloading video"); onReloadRequired.emit(); }); } @@ -689,7 +688,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout { DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); if(audioSource.hasGenerate) { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { + var startId = -1; try { + startId = audioSource.getUnderlyingPlugin()?.getUnderlyingPlugin()?.startId ?: -1; val generated = audioSource.generate(); if(generated != null) { withContext(Dispatchers.Main) { @@ -705,11 +706,11 @@ abstract class FutoVideoPlayerBase : RelativeLayout { val plugin = audioSource.getUnderlyingPlugin(); if(plugin == null) return@launch; - /* + if(startId != -1 && plugin.getUnderlyingPlugin()?.startId != startId) + return@launch; StatePlatform.instance.reEnableClient(plugin.id, { onReloadRequired.emit(); }); - */ } catch(ex: Throwable) { From b3f9de3b832726cd231a9ed6041ae2dea1c6edc6 Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Mon, 16 Jun 2025 14:23:34 +0200 Subject: [PATCH 52/87] edgecase fix --- .../futo/platformplayer/views/video/FutoVideoPlayerBase.kt | 7 +++++++ 1 file changed, 7 insertions(+) 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 22650adb..b5e29075 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 @@ -565,7 +565,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout { if(videoSource.hasGenerate) { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { + var startId = -1; try { + startId = videoSource?.getUnderlyingPlugin()?.getUnderlyingPlugin()?.startId ?: -1; val generated = videoSource.generate(); if (generated != null) { withContext(Dispatchers.Main) { @@ -592,6 +594,11 @@ 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()?.startId != startId) + return@launch; StatePlatform.instance.handleReloadRequired(reloadRequired, { onReloadRequired.emit(); }); From ff531b5e77b3ef12d80862ce2a5ce17ad3b6e0b3 Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Mon, 16 Jun 2025 17:46:00 +0200 Subject: [PATCH 53/87] Cleanup, fixes, clearCookies support on httpClients --- .../api/media/platforms/js/DevJSClient.kt | 1 + .../api/media/platforms/js/JSClient.kt | 27 ++++- .../platforms/js/internal/JSHttpClient.kt | 21 +++- .../platforms/js/models/JSRequestModifier.kt | 25 +++-- .../platforms/js/models/sources/JSSource.kt | 21 ++-- .../futo/platformplayer/engine/V8Plugin.kt | 105 +----------------- .../engine/packages/PackageBridge.kt | 9 +- .../engine/packages/PackageHttp.kt | 11 ++ .../views/video/FutoVideoPlayerBase.kt | 20 ++-- 9 files changed, 99 insertions(+), 141 deletions(-) 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 144faa2c..4422dd28 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 @@ -84,6 +84,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; @@ -198,6 +200,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 @@ -215,19 +218,29 @@ open class JSClient : IPlatformClient { } fun setReloadData(data: String?) { - declareOnEnable.put("__reloadData", data ?: ""); + 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(); - Logger.i(TAG, "Plugin [${config.name}] started"); + plugin.execute("plugin.config = ${Json.encodeToString(config)}"); plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})"); - Logger.i(TAG, "Plugin [${config.name}] configs set"); descriptor.appSettings.loadDefaults(descriptor.config); @@ -255,7 +268,6 @@ open class JSClient : IPlatformClient { hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false, hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false ); - Logger.i(TAG, "Plugin [${config.name}] capabilities retrieved"); try { if (capabilities.hasGetChannelTemplateByClaimMap) @@ -277,8 +289,11 @@ open class JSClient : IPlatformClient { } plugin.execute("source.enable(${Json.encodeToString(config)}, parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())}), ${Json.encodeToString(_injectedSaveState)})"); - if(declareOnEnable.containsKey("__reloadData")) + 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") 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 eec4414a..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,9 +67,28 @@ 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)) }) + newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) }) return newClient; } 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/sources/JSSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSSource.kt index 649c74cf..00906239 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 @@ -62,9 +62,11 @@ abstract class JSSource { if (!hasRequestModifier || _obj.isClosed) return null; - val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") { - _obj.invoke("getRequestModifier", arrayOf()); - }; + val result = _plugin.isBusyWith("getRequestModifier") { + V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") { + _obj.invoke("getRequestModifier", arrayOf()); + }; + } if (result !is V8ValueObject) return null; @@ -76,13 +78,12 @@ abstract class JSSource { return null; Logger.v("JSSource", "Request executor for [${type}] requesting"); - val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") { - _plugin.isBusyWith("getRequestExecutor") { - _plugin.getUnderlyingPlugin().busy { - _obj.invoke("getRequestExecutor", arrayOf()); - } - } - }; + val result =_plugin.isBusyWith("getRequestExecutor") { + V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") { + _obj.invoke("getRequestExecutor", arrayOf()); + }; + } + Logger.v("JSSource", "Request executor for [${type}] received"); if (result !is V8ValueObject) 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 5b6ffd91..323aa5e1 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -1,15 +1,12 @@ package com.futo.platformplayer.engine import android.content.Context -import com.caoccao.javet.entities.JavetEntityError import com.caoccao.javet.exceptions.JavetCompilationException import com.caoccao.javet.exceptions.JavetException import com.caoccao.javet.exceptions.JavetExecutionException import com.caoccao.javet.interfaces.IJavetEntityError 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 @@ -17,7 +14,6 @@ import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient -import com.futo.platformplayer.assume import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.engine.exceptions.NoInternetException import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException @@ -44,7 +40,6 @@ 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.Semaphore import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -58,7 +53,7 @@ class V8Plugin { val httpClientAuth: ManagedHttpClient get() = _clientAuth; val httpClientOthers: Map get() = _clientOthers; - var startId: Int = 0; + var runtimeId: Int = 0; fun registerHttpClient(client: JSHttpClient) { synchronized(_clientOthers) { @@ -76,11 +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; - private val _busyLock = ReentrantLock()//Semaphore(1); - val isBusy get() = _busyLock.isLocked;//synchronized(_busyCounterLock) { _busyCounter > 0 }; + private val _busyLock = ReentrantLock() + val isBusy get() = _busyLock.isLocked; var allowDevSubmit: Boolean = false private set(value) { @@ -150,24 +142,19 @@ class V8Plugin { synchronized(_runtimeLock) { if (_runtime != null) return; - startId + 1; + runtimeId = runtimeId + 1; //V8RuntimeOptions.V8_FLAGS.setUseStrict(true); val host = V8Host.getV8Instance(); val options = host.jsRuntimeType.getRuntimeOptions(); - Logger.i(TAG, "Plugin [${config.name}] start: Creating runtime") - _runtime = host.createV8Runtime(options); if (!host.isIsolateCreated) throw IllegalStateException("Isolate not created"); - Logger.i(TAG, "Plugin [${config.name}] start: Created runtime") - //Setup bridge _runtime?.let { it.converter = V8Converter(); - Logger.i(TAG, "Plugin [${config.name}] start: Loading packages") for (pack in _depsPackages) { if (pack.variableName != null) it.createV8ValueObject().use { v8valueObject -> @@ -180,8 +167,6 @@ class V8Plugin { } } - Logger.i(TAG, "Plugin [${config.name}] start: Loading deps") - //Load deps for (dep in _deps) catchScriptErrors("Dep[${dep.key}]") { @@ -192,13 +177,11 @@ class V8Plugin { if (config.allowEval) it.allowEval(true); - Logger.i(TAG, "Plugin [${config.name}] start: Loading script") //Load plugin catchScriptErrors("Plugin[${config.name}]") { it.getExecutor(script).executeVoid() }; isStopped = false; - Logger.i(TAG, "Plugin [${config.name}] start: Script loaded") } } } @@ -210,7 +193,7 @@ class V8Plugin { if(isStopped) return@busy; isStopped = true; - startId = -1; + runtimeId = runtimeId + 1; //Cleanup http for(pack in _depsPackages) { @@ -260,79 +243,11 @@ class V8Plugin { runtime.getExecutor(js).execute() }; } - /* - synchronized(_busyCounterLock) { - _busyCounter++; - } - - val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet"); - try { - return 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? = 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 } } - /* - fun whenNotBusyBlocking(handler: (V8Plugin)->T): T { - while(true) { - synchronized(_busyCounterLock) { - if(_busyCounter == 0) - { - return handler(this); - } - } - Thread.sleep(1); - } - } - */ - /* - 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); - - var failed = false; - synchronized(_busyCounterLock) { - if(_busyCounter > 0) { - failed = true; - return@synchronized - } - handler(this); - } - if(failed) - busy { - handler(this); - } - } - } - } - } - } - */ - private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? { //TODO: Auto get all package types? return when(packageName) { @@ -397,15 +312,12 @@ class V8Plugin { val obj = executeEx.scriptingError?.context as IJavetEntityError if(obj.context.containsKey("plugin_type") == true) { val pluginType = obj.context["plugin_type"].toString(); - //val pluginType = obj.get("plugin_type").toString(); //Captcha if (pluginType == "CaptchaRequiredException") { throw ScriptCaptchaRequiredException(config, obj.context["url"]?.toString(), obj.context["body"]?.toString(), - //obj.get("url")?.toString(), - //obj.get("body")?.toString(), executeEx, executeEx.scriptingError?.stack, codeStripped); } @@ -414,8 +326,6 @@ class V8Plugin { throw ScriptReloadRequiredException(config, obj.context["msg"]?.toString(), obj.context["reloadData"]?.toString(), - //obj.get("message")?.toString(), - //obj.get("reloadData")?.toString(), executeEx, executeEx.scriptingError?.stack, codeStripped); } @@ -481,9 +391,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/packages/PackageBridge.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt index b4cc821d..858b020b 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 @@ -82,7 +82,8 @@ class PackageBridge : V8Package { @V8Property fun supportedFeatures(): Array { return arrayOf( - "ReloadRequiredException" + "ReloadRequiredException", + "HttpBatchClient" ); } @@ -130,14 +131,10 @@ class PackageBridge : V8Package { timeoutMap.remove(id); } try { - Logger.v(TAG, "Timeout started [${id}]"); _plugin.busy { - Logger.v(TAG, "Timeout call started [${id}]"); if(!_plugin.isStopped) funcClone.callVoid(null, arrayOf()); - Logger.v(TAG, "Timeout call ended [${id}]"); } - Logger.v(TAG, "Timeout resolved [${id}]"); } catch(ex: Throwable) { Logger.e(TAG, "Failed timeout callback", ex); @@ -173,7 +170,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 2930a476..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 @@ -347,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) 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 b5e29075..7f361105 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt @@ -567,7 +567,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { var startId = -1; try { - startId = videoSource?.getUnderlyingPlugin()?.getUnderlyingPlugin()?.startId ?: -1; + startId = videoSource?.getUnderlyingPlugin()?.getUnderlyingPlugin()?.runtimeId ?: -1; val generated = videoSource.generate(); if (generated != null) { withContext(Dispatchers.Main) { @@ -597,7 +597,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { val plugin = videoSource.getUnderlyingPlugin(); if(plugin == null) return@launch; - if(startId != -1 && plugin.getUnderlyingPlugin()?.startId != startId) + if(startId != -1 && plugin.getUnderlyingPlugin()?.runtimeId != startId) return@launch; StatePlatform.instance.handleReloadRequired(reloadRequired, { onReloadRequired.emit(); @@ -689,17 +689,17 @@ 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) { var startId = -1; try { - startId = audioSource.getUnderlyingPlugin()?.getUnderlyingPlugin()?.startId ?: -1; + 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), @@ -713,7 +713,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { val plugin = audioSource.getUnderlyingPlugin(); if(plugin == null) return@launch; - if(startId != -1 && plugin.getUnderlyingPlugin()?.startId != startId) + if(startId != -1 && plugin.getUnderlyingPlugin()?.runtimeId != startId) return@launch; StatePlatform.instance.reEnableClient(plugin.id, { onReloadRequired.emit(); @@ -726,6 +726,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout { 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( From 6ba9ec8bc26752684cdcba72b3760f6241f9e4bc Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Mon, 16 Jun 2025 17:56:04 +0200 Subject: [PATCH 54/87] Clearer name setting --- app/src/main/java/com/futo/platformplayer/Settings.kt | 4 ++-- .../java/com/futo/platformplayer/states/StatePlaylists.kt | 7 +------ app/src/main/res/values/strings.xml | 4 ++-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 6cb27e3f..b000d586 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -1017,8 +1017,8 @@ 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.add_to_beginning_of_watch_later, FieldForm.TOGGLE, R.string.add_to_beginning_description, 4) - var addToBeginning: Boolean = true; + @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; 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 996c3f9a..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 @@ -185,7 +180,7 @@ class StatePlaylists { } _watchlistStore.saveAsync(video) - if (Settings.instance.other.addToBeginning) { + if (Settings.instance.other.watchLaterAddStart) { _watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray()) } else { _watchlistOrderStore.set(*(_watchlistOrderStore.values + listOf(video.url)).toTypedArray()) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b2831cec..4dd158e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -468,8 +468,8 @@ 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 + 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 From 7e83793586e2f556b347b1b6ce283e8c2d04d1a0 Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Mon, 16 Jun 2025 18:34:37 +0200 Subject: [PATCH 55/87] Submods --- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 2e258294..0167dfb4 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 2e25829494addebbf9292b7c68b2d6130e1fc461 +Subproject commit 0167dfb471e6f90ab08e997ac7151072576c42db diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 2e258294..0167dfb4 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 2e25829494addebbf9292b7c68b2d6130e1fc461 +Subproject commit 0167dfb471e6f90ab08e997ac7151072576c42db From 33d3d9a29c22beabd05e2808d56a7b0c7ff2275a Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Mon, 16 Jun 2025 19:30:52 +0200 Subject: [PATCH 56/87] Improved locking --- .../api/media/platforms/js/JSClient.kt | 32 +++++++++-------- .../platforms/js/models/JSVideoDetails.kt | 34 +++++++++++-------- 2 files changed, 37 insertions(+), 29 deletions(-) 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 4422dd28..6beb6894 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 @@ -281,7 +281,7 @@ 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) { @@ -297,12 +297,12 @@ open class JSClient : IPlatformClient { _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") @@ -405,14 +405,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") @@ -543,14 +543,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") @@ -652,17 +652,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 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 799c737f..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 @@ -84,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(_plugin, 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? { @@ -108,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? { @@ -125,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 From c14378b534150673c3e52a038fc5a94d4b9b1bba Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Tue, 17 Jun 2025 11:45:02 +0200 Subject: [PATCH 57/87] Improved V8 locking, comment section on diff thread than video, global mapping of v8runtimes to plugins --- .../com/futo/platformplayer/Extensions_V8.kt | 20 +++++++++++ .../api/media/platforms/js/JSClient.kt | 10 ++++++ .../models/sources/JSDashManifestRawSource.kt | 2 +- .../platforms/js/models/sources/JSSource.kt | 34 ++++++++----------- .../futo/platformplayer/engine/V8Plugin.kt | 13 +++++++ .../platformplayer/states/StatePlatform.kt | 4 +-- .../views/video/FutoVideoPlayerBase.kt | 11 +++--- 7 files changed, 68 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt index e31d3dac..b350d153 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}"); @@ -90,6 +96,20 @@ inline fun V8ValueArray.expectV8Variants(config: IV8PluginConfig, co } inline fun V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T { + if(false) + { + this?.getSourcePlugin()?.let { + if (!it.isThreadAlreadyBusy()) { + 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() + ) + } + } + } 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/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt index 6beb6894..7dd027f7 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,6 +59,9 @@ 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 @@ -771,6 +774,13 @@ open class JSClient : IPlatformClient { return@busy handle(); } } + 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); 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 a9c070f7..7f0de0af 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; 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 00906239..320a918f 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 @@ -53,43 +53,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 = _plugin.isBusyWith("getRequestModifier") { - V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") { - _obj.invoke("getRequestModifier", arrayOf()); - }; - } + 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 =_plugin.isBusyWith("getRequestExecutor") { - V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") { - _obj.invoke("getRequestExecutor", arrayOf()); - }; - } + val result = V8Plugin.catchScriptErrors(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") { + _obj.invoke("getRequestExecutor", arrayOf()); + }; Logger.v("JSSource", "Request executor for [${type}] received"); if (result !is V8ValueObject) - return null; + return@isBusyWith null; - return JSRequestExecutor(_plugin, result) + return@isBusyWith JSRequestExecutor(_plugin, result) } fun getUnderlyingPlugin(): JSClient? { 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 323aa5e1..170c2d56 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -49,6 +49,7 @@ 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; @@ -151,6 +152,8 @@ class V8Plugin { if (!host.isIsolateCreated) throw IllegalStateException("Isolate not created"); + _runtimeMap.put(_runtime!!, this); + //Setup bridge _runtime?.let { it.converter = V8Converter(); @@ -203,6 +206,7 @@ class V8Plugin { } _runtime?.let { + _runtimeMap.remove(it); _runtime = null; if(!it.isClosed && !it.isDead) { try { @@ -222,6 +226,9 @@ class V8Plugin { } } + 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()); @@ -273,8 +280,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 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 8cf8f080..9376644f 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt @@ -958,7 +958,7 @@ class StatePlatform { //Comments fun getComments(content: IPlatformContentDetails): IPager { val client = getContentClient(content.url); - val pager = content.getComments(client); + val pager = null;//content.getComments(client); return pager ?: getComments(content.url); } @@ -969,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/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index 7f361105..6bd66ccb 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 @@ -353,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); @@ -567,8 +569,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout { findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) { var startId = -1; try { - startId = videoSource?.getUnderlyingPlugin()?.getUnderlyingPlugin()?.runtimeId ?: -1; - 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)) From b953ff21e7c2604a599de37a064da78b0686bb4f Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Tue, 17 Jun 2025 11:52:26 +0200 Subject: [PATCH 58/87] Lock on subtitle fetch --- .../api/media/platforms/js/models/JSSubtitleSource.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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? { From c0bbe5d4914e81061e4ec85021f545212f860b11 Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Tue, 17 Jun 2025 15:21:46 +0200 Subject: [PATCH 59/87] Additional locking --- .../api/media/platforms/js/JSClient.kt | 19 ++++++++++++------- .../platforms/js/models/JSLiveEventPager.kt | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) 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 7dd027f7..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 @@ -346,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); @@ -375,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") @@ -433,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); 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"); } From ab07288ba037d22872eebd9215a4c4ece1550af5 Mon Sep 17 00:00:00 2001 From: zvonimir Date: Tue, 17 Jun 2025 17:25:34 +0200 Subject: [PATCH 60/87] fix: timeoutMap being deadlocked --- .../engine/packages/PackageBridge.kt | 39 ++++++++----------- .../views/video/FutoVideoPlayerBase.kt | 2 +- 2 files changed, 17 insertions(+), 24 deletions(-) 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 858b020b..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 @@ -27,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 @@ -110,7 +111,7 @@ class PackageBridge : V8Package { } var timeoutCounter = 0; - var timeoutMap = HashSet(); + var timeoutMap = ConcurrentHashMap(); @V8Function fun setTimeout(func: V8ValueFunction, timeout: Long): Int { val id = timeoutCounter++; @@ -118,47 +119,39 @@ class PackageBridge : V8Package { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { delay(timeout); - if(_plugin.isStopped) + if (_plugin.isStopped) return@launch; - synchronized(timeoutMap) { - if(!timeoutMap.contains(id)) { - _plugin.busy { - if(!_plugin.isStopped) - JavetResourceUtils.safeClose(funcClone); - } - return@launch; + if (!timeoutMap.containsKey(id)) { + _plugin.busy { + if (!_plugin.isStopped) + JavetResourceUtils.safeClose(funcClone); } - timeoutMap.remove(id); + return@launch; } + timeoutMap.remove(id); try { _plugin.busy { - if(!_plugin.isStopped) + if (!_plugin.isStopped) funcClone.callVoid(null, arrayOf()); } - } - catch(ex: Throwable) { + } catch (ex: Throwable) { Logger.e(TAG, "Failed timeout callback", ex); - } - finally { + } finally { _plugin.busy { - if(!_plugin.isStopped) + 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) { 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 6bd66ccb..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 @@ -580,7 +580,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { 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( From 48a96140a74f7fb121a6b6ef06ef57f9ff1fd66e Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Tue, 17 Jun 2025 17:28:10 +0200 Subject: [PATCH 61/87] isBusy checks and locking improvements --- .../com/futo/platformplayer/Extensions_V8.kt | 36 ++++++++++++------- .../platformplayer/api/media/PlatformID.kt | 2 ++ .../api/media/models/PlatformAuthorLink.kt | 2 ++ .../models/PlatformAuthorMembershipLink.kt | 2 ++ .../api/media/models/ResultCapabilities.kt | 4 +++ .../api/media/models/Thumbnails.kt | 2 ++ .../media/models/live/IPlatformLiveEvent.kt | 2 ++ .../api/media/models/live/LiveEventComment.kt | 3 ++ .../media/models/live/LiveEventDonation.kt | 2 ++ .../api/media/models/live/LiveEventEmojis.kt | 2 ++ .../api/media/models/live/LiveEventRaid.kt | 2 ++ .../media/models/live/LiveEventViewCount.kt | 2 ++ .../api/media/models/ratings/IRating.kt | 7 +++- .../models/ratings/RatingLikeDislikes.kt | 2 ++ .../api/media/models/ratings/RatingLikes.kt | 2 ++ .../api/media/models/ratings/RatingScaler.kt | 2 ++ .../media/platforms/js/models/IJSContent.kt | 2 ++ .../platforms/js/models/IJSContentDetails.kt | 2 ++ .../sources/JSHLSManifestAudioSource.kt | 11 ++++-- .../platforms/js/models/sources/JSSource.kt | 28 ++++++++++++--- .../models/sources/JSVideoSourceDescriptor.kt | 2 ++ .../engine/exceptions/NoInternetException.kt | 2 ++ .../engine/exceptions/ScriptAgeException.kt | 2 ++ .../ScriptCaptchaRequiredException.kt | 2 ++ .../exceptions/ScriptCompilationException.kt | 2 ++ .../exceptions/ScriptCriticalException.kt | 2 ++ .../engine/exceptions/ScriptException.kt | 2 ++ .../exceptions/ScriptExecutionException.kt | 2 ++ .../ScriptImplementationException.kt | 2 ++ .../ScriptLoginRequiredException.kt | 2 ++ .../ScriptReloadRequiredException.kt | 2 ++ .../exceptions/ScriptTimeoutException.kt | 2 ++ .../exceptions/ScriptUnavailableException.kt | 2 ++ .../futo/platformplayer/models/Playlist.kt | 2 ++ 34 files changed, 124 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt index b350d153..7a43f078 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt @@ -95,21 +95,31 @@ inline fun V8ValueArray.expectV8Variants(config: IV8PluginConfig, co .map { kv-> kv.second.orNull { it.expectV8Variant(config, contextName + "[${kv.first}]", ) } as T }; } -inline fun V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T { - if(false) - { - this?.getSourcePlugin()?.let { - if (!it.isThreadAlreadyBusy()) { - 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 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(true) + 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/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/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/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 320a918f..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 @@ -108,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); @@ -126,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/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 index 98c0635d..6c792a32 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptReloadRequiredException.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/exceptions/ScriptReloadRequiredException.kt @@ -4,6 +4,7 @@ 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 @@ -11,6 +12,7 @@ class ScriptReloadRequiredException(config: IV8PluginConfig, val msg: String?, v companion object { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException { + obj.ensureIsBusy(); val contextName = "ScriptReloadRequiredException"; return ScriptReloadRequiredException(config, obj.getOrThrow(config, "message", contextName), 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/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"; From a464ae9df5a50d7766b47d60c1e329bf2ec8540a Mon Sep 17 00:00:00 2001 From: Koen J Date: Wed, 18 Jun 2025 10:07:02 +0200 Subject: [PATCH 62/87] Added missing loader causing crash. --- app/src/main/res/layout/list_locked_preview.xml | 6 +++++- app/src/main/res/layout/list_locked_thumbnail.xml | 8 +++++++- app/src/main/res/layout/list_video_preview_nested.xml | 8 +++++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/layout/list_locked_preview.xml b/app/src/main/res/layout/list_locked_preview.xml index 2413c98c..e814c2da 100644 --- a/app/src/main/res/layout/list_locked_preview.xml +++ b/app/src/main/res/layout/list_locked_preview.xml @@ -116,9 +116,13 @@ android:layout_marginBottom="6dp" android:background="#DD000000" android:visibility="gone" + android:gravity="center" android:orientation="vertical"> - + + android:gravity="center" + android:orientation="vertical"> + + - - - + Date: Wed, 18 Jun 2025 10:29:12 +0200 Subject: [PATCH 63/87] Reverted changes. --- app/src/main/res/layout/list_locked_preview.xml | 4 ---- app/src/main/res/layout/list_locked_thumbnail.xml | 5 +---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/app/src/main/res/layout/list_locked_preview.xml b/app/src/main/res/layout/list_locked_preview.xml index e814c2da..1aefd5b8 100644 --- a/app/src/main/res/layout/list_locked_preview.xml +++ b/app/src/main/res/layout/list_locked_preview.xml @@ -119,10 +119,6 @@ android:gravity="center" android:orientation="vertical"> - - + Date: Wed, 18 Jun 2025 12:40:25 +0200 Subject: [PATCH 64/87] Downgrade v8, revert comments on diff thread --- app/build.gradle | 4 +-- .../com/futo/platformplayer/Extensions_V8.kt | 2 +- .../futo/platformplayer/engine/V8Plugin.kt | 33 ++++++++++++++++++- .../platformplayer/states/StatePlatform.kt | 2 +- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8c30e58d..278e8b0f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -179,8 +179,8 @@ dependencies { implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject) //JS - //implementation("com.caoccao.javet:javet-android:3.0.2") - implementation 'com.caoccao.javet:javet-v8-android:4.1.4' + 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/main/java/com/futo/platformplayer/Extensions_V8.kt b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt index 7a43f078..aa51393f 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt @@ -118,7 +118,7 @@ inline fun V8Value.ensureIsBusy() { } inline fun V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T { - if(true) + if(false) ensureIsBusy(); return when(T::class) { String::class -> this.expectOrThrow(config, contextName).value as T; 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 170c2d56..0cb2f196 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -4,7 +4,6 @@ import android.content.Context import com.caoccao.javet.exceptions.JavetCompilationException import com.caoccao.javet.exceptions.JavetException import com.caoccao.javet.exceptions.JavetExecutionException -import com.caoccao.javet.interfaces.IJavetEntityError import com.caoccao.javet.interop.V8Host import com.caoccao.javet.interop.V8Runtime import com.caoccao.javet.values.V8Value @@ -321,6 +320,37 @@ 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) { + 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, + 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); + } + + //Others + throwExceptionFromV8( + config, + pluginType, + (extractJSExceptionMessage(executeEx) ?: ""), + executeEx, + executeEx.scriptingError?.stack, + 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) { @@ -354,6 +384,7 @@ class V8Plugin { } } + */ throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped); } catch(ex: Exception) { 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 9376644f..1f1e5625 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt @@ -958,7 +958,7 @@ class StatePlatform { //Comments fun getComments(content: IPlatformContentDetails): IPager { val client = getContentClient(content.url); - val pager = null;//content.getComments(client); + val pager = content.getComments(client); return pager ?: getComments(content.url); } From 7922aa6f80d4ef4db9ec68e0a7d37c88e34f4dfc Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Wed, 18 Jun 2025 12:41:21 +0200 Subject: [PATCH 65/87] Log on busy on main --- app/src/main/java/com/futo/platformplayer/Extensions_V8.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt index aa51393f..240a6cfc 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt @@ -98,16 +98,14 @@ inline fun V8ValueArray.expectV8Variants(config: IV8PluginConfig, co inline fun V8Plugin.ensureIsBusy() { this.let { if (!it.isThreadAlreadyBusy()) { - throw IllegalStateException("Tried to access V8Plugin without busy"); - /* + //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() - ) - */ + ); } } } From 15d771f7fc01a284aadc7c4652687b6dfdab5ea1 Mon Sep 17 00:00:00 2001 From: Koen J Date: Wed, 18 Jun 2025 13:43:50 +0200 Subject: [PATCH 66/87] Fixed channel loader not being animated. --- .../fragment/mainactivity/main/ChannelFragment.kt | 2 +- app/src/main/res/layout/fragment_channel.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 74116069..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) 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"> Date: Wed, 18 Jun 2025 14:27:20 +0200 Subject: [PATCH 67/87] Hide duration if unknown --- .../views/adapters/VideoListEditorViewHolder.kt | 8 +++++++- .../views/adapters/feedtypes/PreviewVideoView.kt | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) 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); From e0e90c5f7462ff136e225af36caf2f0fbc14dd61 Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Wed, 18 Jun 2025 14:33:07 +0200 Subject: [PATCH 68/87] submodules --- app/src/stable/assets/sources/kick | 2 +- app/src/stable/assets/sources/patreon | 2 +- app/src/stable/assets/sources/soundcloud | 2 +- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/kick | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) 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 0167dfb4..71285696 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 0167dfb471e6f90ab08e997ac7151072576c42db +Subproject commit 712856962838dc229bf1b91aa99d22e101957f15 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 From a2986a72bd945f4a5a1c926f4c3d789daa0de99d Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Wed, 18 Jun 2025 14:43:20 +0200 Subject: [PATCH 69/87] Refs --- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 71285696..568d5605 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 712856962838dc229bf1b91aa99d22e101957f15 +Subproject commit 568d560520d6eff77d710aeac66057c76aedd9c0 diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 0167dfb4..568d5605 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 0167dfb471e6f90ab08e997ac7151072576c42db +Subproject commit 568d560520d6eff77d710aeac66057c76aedd9c0 From c6100ede70e7f9287913845d83829430b4f3f8ca Mon Sep 17 00:00:00 2001 From: Koen J Date: Wed, 18 Jun 2025 15:43:12 +0200 Subject: [PATCH 70/87] Added disable for hold playback rate increase. --- .../java/com/futo/platformplayer/Settings.kt | 19 ++++++++++--------- .../views/behavior/GestureControlView.kt | 3 ++- app/src/main/res/values/strings.xml | 1 + 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index b000d586..da414d8f 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -587,18 +587,19 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.hold_playback_speed, FieldForm.DROPDOWN, R.string.hold_playback_speed_description, 27) @DropdownFieldOptionsId(R.array.hold_playback_speeds) - var holdPlaybackSpeed: Int = 3; + var holdPlaybackSpeed: Int = 4; fun getHoldPlaybackSpeed(): Double { return when(holdPlaybackSpeed) { - 0 -> 1.25 - 1 -> 1.5 - 2 -> 1.75 - 3 -> 2.0 - 4 -> 2.25 - 5 -> 2.5 - 6 -> 2.75 - 7 -> 3.0 + 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 } } 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 4bb864da..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 @@ -240,7 +240,8 @@ class GestureControlView : LinearLayout { && !_adjustingFullscreenUp && !_adjustingFullscreenDown && !_isPanning - && !_isZooming) { + && !_isZooming + && Settings.instance.playback.getHoldPlaybackSpeed() > 1.0) { _speedHolding = true showHoldSpeedControls() onSpeedHoldStart.emit() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4dd158e0..39cf819a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1112,6 +1112,7 @@ 5.0 + Disabled 1.25 1.5 1.75 From 11319e0ec5d4632180a8e52003aa51991a0b95f6 Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Wed, 18 Jun 2025 16:22:28 +0200 Subject: [PATCH 71/87] Refs --- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/youtube | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 568d5605..97480075 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 568d560520d6eff77d710aeac66057c76aedd9c0 +Subproject commit 97480075fd8a50fa7eb601b5ed4bc1e531207dd0 diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 568d5605..97480075 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 568d560520d6eff77d710aeac66057c76aedd9c0 +Subproject commit 97480075fd8a50fa7eb601b5ed4bc1e531207dd0 From 49ddecdea4d5b275286b2eec0f758fd198bb1a67 Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 19 Jun 2025 11:21:46 +0200 Subject: [PATCH 72/87] Potential crashfix #2382. --- .../java/com/futo/platformplayer/services/DownloadService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"); From 3a8167644772cdc34fafc53b57ffa19bfc70e383 Mon Sep 17 00:00:00 2001 From: Koen J Date: Fri, 20 Jun 2025 10:47:10 +0200 Subject: [PATCH 73/87] Fixed crash #2389. --- .../main/java/com/futo/platformplayer/states/StateSync.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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) + } + } } } From edb9eda0a933098e2fb171480ebd56cfee24387e Mon Sep 17 00:00:00 2001 From: Kelvin K Date: Fri, 20 Jun 2025 15:35:02 +0200 Subject: [PATCH 74/87] Improved locking --- .../models/sources/JSDashManifestRawAudioSource.kt | 14 ++++++++------ .../js/models/sources/JSDashManifestRawSource.kt | 14 ++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) 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 2c6d4b35..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 @@ -75,12 +75,14 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS } 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 7f0de0af..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 @@ -81,12 +81,14 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo }); 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; From 4d720b1d81c809a04604867f3ef74b6a1d92c0c8 Mon Sep 17 00:00:00 2001 From: Koen J Date: Tue, 24 Jun 2025 11:43:40 +0200 Subject: [PATCH 75/87] Fixed app freezing when exporting Polycentric Identity #2405 --- .../activities/PolycentricBackupActivity.kt | 55 ++++++++++++++----- .../layout/activity_polycentric_backup.xml | 11 ++++ 2 files changed, 53 insertions(+), 13 deletions(-) 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/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 From 548752e2401007d9f8715656b23ce2a0698a13b8 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Thu, 26 Jun 2025 14:35:00 +0200 Subject: [PATCH 76/87] missing lock --- .../com/futo/platformplayer/api/media/PlatformClientPool.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientPool.kt b/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientPool.kt index 211f83a6..ce3a720e 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientPool.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientPool.kt @@ -34,8 +34,10 @@ class PlatformClientPool { isDead = true; onDead.emit(parentClient, this); - for(clientPair in _pool) { - clientPair.key.disable(); + synchronized(_pool) { + for (clientPair in _pool) { + clientPair.key.disable(); + } } }; } From d0644d39daae1f16dcedf0c553097cc410dfb5cc Mon Sep 17 00:00:00 2001 From: Kelvin Date: Thu, 26 Jun 2025 15:01:18 +0200 Subject: [PATCH 77/87] Theoretical fix for networked file import --- .../dialogs/ImportOptionsDialog.kt | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/ImportOptionsDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/ImportOptionsDialog.kt index 2e79b0b4..a71ee36e 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/ImportOptionsDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/ImportOptionsDialog.kt @@ -12,6 +12,9 @@ import com.futo.platformplayer.readBytes import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.views.buttons.BigButton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class ImportOptionsDialog: AlertDialog { private val _context: MainActivity; @@ -41,8 +44,12 @@ class ImportOptionsDialog: AlertDialog { _button_import_zip.onClick.subscribe { dismiss(); StateApp.instance.requestFileReadAccess(_context, null, "application/zip") { - val zipBytes = it?.readBytes(context) ?: return@requestFileReadAccess; - StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes); + StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { + val zipBytes = it?.readBytes(context) ?: return@launch; + withContext(Dispatchers.Main) { + StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes); + } + } }; } _button_import_ezip.setOnClickListener { @@ -51,17 +58,25 @@ class ImportOptionsDialog: AlertDialog { _button_import_txt.onClick.subscribe { dismiss(); StateApp.instance.requestFileReadAccess(_context, null, "text/plain") { - val txtBytes = it?.readBytes(context) ?: return@requestFileReadAccess; - val txt = String(txtBytes); - StateBackup.importTxt(_context, txt); + StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { + val txtBytes = it?.readBytes(context) ?: return@launch; + val txt = String(txtBytes); + withContext(Dispatchers.Main) { + StateBackup.importTxt(_context, txt); + } + } }; } _button_import_newpipe_subs.onClick.subscribe { dismiss(); StateApp.instance.requestFileReadAccess(_context, null, "application/json") { - val jsonBytes = it?.readBytes(context) ?: return@requestFileReadAccess; - val json = String(jsonBytes); - StateBackup.importNewPipeSubs(_context, json); + StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { + val jsonBytes = it?.readBytes(context) ?: return@launch; + val json = String(jsonBytes); + withContext(Dispatchers.Main) { + StateBackup.importNewPipeSubs(_context, json); + } + } }; }; _button_import_platform.onClick.subscribe { From bdcb94055a217fe6c482ffbcea6d78353d6531d5 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Thu, 26 Jun 2025 15:13:41 +0200 Subject: [PATCH 78/87] Refs --- app/src/stable/assets/sources/nebula | 2 +- app/src/stable/assets/sources/spotify | 2 +- app/src/stable/assets/sources/youtube | 2 +- app/src/unstable/assets/sources/nebula | 2 +- app/src/unstable/assets/sources/patreon | 2 +- app/src/unstable/assets/sources/soundcloud | 2 +- app/src/unstable/assets/sources/spotify | 2 +- app/src/unstable/assets/sources/youtube | 2 +- dep/futopay | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/stable/assets/sources/nebula b/app/src/stable/assets/sources/nebula index 880da6a0..090cd76d 160000 --- a/app/src/stable/assets/sources/nebula +++ b/app/src/stable/assets/sources/nebula @@ -1 +1 @@ -Subproject commit 880da6a015fa8e109cd475d1d87866b8394de0a5 +Subproject commit 090cd76dfa08a43b7181c2682c97da9055210bc6 diff --git a/app/src/stable/assets/sources/spotify b/app/src/stable/assets/sources/spotify index d0258043..214ac1df 160000 --- a/app/src/stable/assets/sources/spotify +++ b/app/src/stable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit d025804364c4dd283be8845a35208c674841911a +Subproject commit 214ac1dfcc985f533d9db7d128a8315bc55fa854 diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 97480075..48d98c1f 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 97480075fd8a50fa7eb601b5ed4bc1e531207dd0 +Subproject commit 48d98c1f0cd80e9e569280423ae404e56047c883 diff --git a/app/src/unstable/assets/sources/nebula b/app/src/unstable/assets/sources/nebula index 880da6a0..090cd76d 160000 --- a/app/src/unstable/assets/sources/nebula +++ b/app/src/unstable/assets/sources/nebula @@ -1 +1 @@ -Subproject commit 880da6a015fa8e109cd475d1d87866b8394de0a5 +Subproject commit 090cd76dfa08a43b7181c2682c97da9055210bc6 diff --git a/app/src/unstable/assets/sources/patreon b/app/src/unstable/assets/sources/patreon index d98c7f8a..b811f8bd 160000 --- a/app/src/unstable/assets/sources/patreon +++ b/app/src/unstable/assets/sources/patreon @@ -1 +1 @@ -Subproject commit d98c7f8aee36101d60a0c671d16a0800d5d715d0 +Subproject commit b811f8bdfbbff73cf0d7581c9d7596911cb132b6 diff --git a/app/src/unstable/assets/sources/soundcloud b/app/src/unstable/assets/sources/soundcloud index 54564312..048acef1 160000 --- a/app/src/unstable/assets/sources/soundcloud +++ b/app/src/unstable/assets/sources/soundcloud @@ -1 +1 @@ -Subproject commit 54564312683a0ae06d7085405478f96cade325e3 +Subproject commit 048acef152823d2621da177d3b4e1535cf4ca8ac diff --git a/app/src/unstable/assets/sources/spotify b/app/src/unstable/assets/sources/spotify index d0258043..214ac1df 160000 --- a/app/src/unstable/assets/sources/spotify +++ b/app/src/unstable/assets/sources/spotify @@ -1 +1 @@ -Subproject commit d025804364c4dd283be8845a35208c674841911a +Subproject commit 214ac1dfcc985f533d9db7d128a8315bc55fa854 diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 97480075..48d98c1f 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 97480075fd8a50fa7eb601b5ed4bc1e531207dd0 +Subproject commit 48d98c1f0cd80e9e569280423ae404e56047c883 diff --git a/dep/futopay b/dep/futopay index 3e99ed52..224d6976 160000 --- a/dep/futopay +++ b/dep/futopay @@ -1 +1 @@ -Subproject commit 3e99ed522a16300874e931bbcb86899aaebf1013 +Subproject commit 224d69764c238c80cc21280be03b283eebbf6757 From d22e918273056b06bf27feffd80efdb1ea1b3344 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Thu, 26 Jun 2025 16:17:17 +0200 Subject: [PATCH 79/87] Missing catches --- .../dialogs/ImportOptionsDialog.kt | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/ImportOptionsDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/ImportOptionsDialog.kt index a71ee36e..4c0ccb7a 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/ImportOptionsDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/ImportOptionsDialog.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.widget.Button import com.futo.platformplayer.R +import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment import com.futo.platformplayer.readBytes @@ -47,7 +48,12 @@ class ImportOptionsDialog: AlertDialog { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { val zipBytes = it?.readBytes(context) ?: return@launch; withContext(Dispatchers.Main) { - StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes); + try { + StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes); + } + catch(ex: Throwable) { + UIDialogs.toast("Failed to import, invalid format?\n" + ex.message); + } } } }; @@ -62,7 +68,12 @@ class ImportOptionsDialog: AlertDialog { val txtBytes = it?.readBytes(context) ?: return@launch; val txt = String(txtBytes); withContext(Dispatchers.Main) { - StateBackup.importTxt(_context, txt); + try { + StateBackup.importTxt(_context, txt); + } + catch(ex: Throwable) { + UIDialogs.toast("Failed to import, invalid format?\n" + ex.message); + } } } }; @@ -74,7 +85,12 @@ class ImportOptionsDialog: AlertDialog { val jsonBytes = it?.readBytes(context) ?: return@launch; val json = String(jsonBytes); withContext(Dispatchers.Main) { - StateBackup.importNewPipeSubs(_context, json); + try { + StateBackup.importNewPipeSubs(_context, json); + } + catch(ex: Throwable) { + UIDialogs.toast("Failed to import, invalid format?\n" + ex.message); + } } } }; From a9605118fb2ae06b45397e949fab2870ea67340b Mon Sep 17 00:00:00 2001 From: Koen J Date: Tue, 1 Jul 2025 10:02:06 +0200 Subject: [PATCH 80/87] Clip to outline does not make sense for a ShapeableImageView. --- .../futo/platformplayer/views/adapters/HistoryListViewHolder.kt | 1 - .../platformplayer/views/adapters/VideoListEditorViewHolder.kt | 1 - .../views/adapters/VideoListHorizontalViewHolder.kt | 1 - 3 files changed, 3 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/HistoryListViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/HistoryListViewHolder.kt index 2fb9dd32..a31dd14b 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/HistoryListViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/HistoryListViewHolder.kt @@ -43,7 +43,6 @@ class HistoryListViewHolder : ViewHolder { constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_history, viewGroup, false)) { _root = itemView.findViewById(R.id.root); _imageThumbnail = itemView.findViewById(R.id.image_video_thumbnail); - _imageThumbnail.clipToOutline = true; _textName = itemView.findViewById(R.id.text_video_name); _textAuthor = itemView.findViewById(R.id.text_author); _textMetadata = itemView.findViewById(R.id.text_video_metadata); 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 b059d2c9..469ce702 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 @@ -51,7 +51,6 @@ class VideoListEditorViewHolder : ViewHolder { constructor(view: View, touchHelper: ItemTouchHelper? = null) : super(view) { _root = view.findViewById(R.id.root); _imageThumbnail = view.findViewById(R.id.image_video_thumbnail); - _imageThumbnail?.clipToOutline = true; _textName = view.findViewById(R.id.text_video_name); _textAuthor = view.findViewById(R.id.text_author); _textMetadata = view.findViewById(R.id.text_video_metadata); diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListHorizontalViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListHorizontalViewHolder.kt index 001779f1..e2ba8e5e 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListHorizontalViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/VideoListHorizontalViewHolder.kt @@ -29,7 +29,6 @@ class VideoListHorizontalViewHolder : ViewHolder { constructor(view: View) : super(view) { _root = view.findViewById(R.id.root); _imageThumbnail = view.findViewById(R.id.image_video_thumbnail); - _imageThumbnail?.clipToOutline = true; _textName = view.findViewById(R.id.text_video_name); _textAuthor = view.findViewById(R.id.text_author); _textVideoDuration = view.findViewById(R.id.thumbnail_duration); From 1f3e306a593b42b6edc64ff21888cf3596e584c9 Mon Sep 17 00:00:00 2001 From: Koen J Date: Wed, 2 Jul 2025 17:59:13 +0200 Subject: [PATCH 81/87] RGBA colors. --- .../com/futo/platformplayer/CSSColorTests.kt | 38 +++ .../java/com/futo/platformplayer/CSSColor.kt | 319 ++++++++++++++++++ .../api/media/models/live/LiveEventEmojis.kt | 3 +- .../livechat/LiveChatDonationListItem.kt | 8 +- .../views/livechat/LiveChatDonationPill.kt | 7 +- .../views/livechat/LiveChatMessageListItem.kt | 3 +- .../views/overlays/LiveChatOverlay.kt | 7 +- .../com/futo/platformplayer/CSSColorTests.kt | 257 ++++++++++++++ .../futo/platformplayer/NoiseProtocolTests.kt | 4 +- 9 files changed, 632 insertions(+), 14 deletions(-) create mode 100644 app/src/androidTest/java/com/futo/platformplayer/CSSColorTests.kt create mode 100644 app/src/main/java/com/futo/platformplayer/CSSColor.kt create mode 100644 app/src/test/java/com/futo/platformplayer/CSSColorTests.kt diff --git a/app/src/androidTest/java/com/futo/platformplayer/CSSColorTests.kt b/app/src/androidTest/java/com/futo/platformplayer/CSSColorTests.kt new file mode 100644 index 00000000..66686260 --- /dev/null +++ b/app/src/androidTest/java/com/futo/platformplayer/CSSColorTests.kt @@ -0,0 +1,38 @@ +package com.futo.platformplayer + +import android.graphics.Color +import org.junit.Assert.assertEquals +import org.junit.Test +import toAndroidColor + +class CSSColorTests { + @Test + fun test1() { + val androidHex = "#80336699" + val androidColorInt = Color.parseColor(androidHex) + + val cssHex = "#33669980" + val cssColor = CSSColor.parseColor(cssHex) + + assertEquals( + "CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)", + androidColorInt, + cssColor.toAndroidColor(), + ) + } + + @Test + fun test2() { + val androidHex = "#123ABC" + val androidColorInt = Color.parseColor(androidHex) + + val cssHex = "#123ABCFF" + val cssColor = CSSColor.parseColor(cssHex) + + assertEquals( + "CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)", + androidColorInt, + cssColor.toAndroidColor() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/CSSColor.kt b/app/src/main/java/com/futo/platformplayer/CSSColor.kt new file mode 100644 index 00000000..73b50413 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/CSSColor.kt @@ -0,0 +1,319 @@ +import kotlin.math.* + +class CSSColor(r: Float, g: Float, b: Float, a: Float = 1f) { + init { + require(r in 0f..1f && g in 0f..1f && b in 0f..1f && a in 0f..1f) { + "RGBA channels must be in [0,1]" + } + } + + // -- RGB(A) channels stored 0–1 -- + var r: Float = r.coerceIn(0f, 1f) + set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true } + var g: Float = g.coerceIn(0f, 1f) + set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true } + var b: Float = b.coerceIn(0f, 1f) + set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true } + var a: Float = a.coerceIn(0f, 1f) + set(v) { field = v.coerceIn(0f, 1f) } + + // -- Int views of RGBA 0–255 -- + var red: Int + get() = (r * 255).roundToInt() + set(v) { r = (v.coerceIn(0, 255) / 255f) } + var green: Int + get() = (g * 255).roundToInt() + set(v) { g = (v.coerceIn(0, 255) / 255f) } + var blue: Int + get() = (b * 255).roundToInt() + set(v) { b = (v.coerceIn(0, 255) / 255f) } + var alpha: Int + get() = (a * 255).roundToInt() + set(v) { a = (v.coerceIn(0, 255) / 255f) } + + // -- HSLA storage & lazy recompute flags -- + private var _h: Float = 0f + private var _s: Float = 0f + private var _l: Float = 0f + private var _hslDirty = true + + /** Hue [0...360) */ + var hue: Float + get() { computeHslIfNeeded(); return _h } + set(v) { setHsl(v, saturation, lightness) } + + /** Saturation [0...1] */ + var saturation: Float + get() { computeHslIfNeeded(); return _s } + set(v) { setHsl(hue, v, lightness) } + + /** Lightness [0...1] */ + var lightness: Float + get() { computeHslIfNeeded(); return _l } + set(v) { setHsl(hue, saturation, v) } + + private fun computeHslIfNeeded() { + if (!_hslDirty) return + val max = max(max(r, g), b) + val min = min(min(r, g), b) + val d = max - min + _l = (max + min) / 2f + _s = if (d == 0f) 0f else d / (1f - abs(2f * _l - 1f)) + _h = when { + d == 0f -> 0f + max == r -> ((g - b) / d % 6f) * 60f + max == g -> (((b - r) / d) + 2f) * 60f + else -> (((r - g) / d) + 4f) * 60f + }.let { if (it < 0f) it + 360f else it } + _hslDirty = false + } + + /** + * Set all three HSL channels at once. + * Hue in degrees [0...360), s/l [0...1]. + */ + fun setHsl(h: Float, s: Float, l: Float) { + val hh = ((h % 360f) + 360f) % 360f + val cc = (1f - abs(2f * l - 1f)) * s + val x = cc * (1f - abs((hh / 60f) % 2f - 1f)) + val m = l - cc / 2f + + val (rp, gp, bp) = when { + hh < 60f -> Triple(cc, x, 0f) + hh < 120f -> Triple(x, cc, 0f) + hh < 180f -> Triple(0f, cc, x) + hh < 240f -> Triple(0f, x, cc) + hh < 300f -> Triple(x, 0f, cc) + else -> Triple(cc, 0f, x) + } + + r = rp + m; g = gp + m; b = bp + m + _h = hh; _s = s; _l = l; _hslDirty = false + } + + /** Return 0xRRGGBBAA int */ + fun toRgbaInt(): Int { + val ai = (a * 255).roundToInt() and 0xFF + val ri = (r * 255).roundToInt() and 0xFF + val gi = (g * 255).roundToInt() and 0xFF + val bi = (b * 255).roundToInt() and 0xFF + return (ri shl 24) or (gi shl 16) or (bi shl 8) or ai + } + + /** Return 0xAARRGGBB int */ + fun toArgbInt(): Int { + val ai = (a * 255).roundToInt() and 0xFF + val ri = (r * 255).roundToInt() and 0xFF + val gi = (g * 255).roundToInt() and 0xFF + val bi = (b * 255).roundToInt() and 0xFF + return (ai shl 24) or (ri shl 16) or (gi shl 8) or bi + } + + // — Convenience modifiers (chainable) — + + /** Lighten by fraction [0...1] */ + fun lighten(fraction: Float): CSSColor = apply { + lightness = (lightness + fraction).coerceIn(0f, 1f) + } + + /** Darken by fraction [0...1] */ + fun darken(fraction: Float): CSSColor = apply { + lightness = (lightness - fraction).coerceIn(0f, 1f) + } + + /** Increase saturation by fraction [0...1] */ + fun saturate(fraction: Float): CSSColor = apply { + saturation = (saturation + fraction).coerceIn(0f, 1f) + } + + /** Decrease saturation by fraction [0...1] */ + fun desaturate(fraction: Float): CSSColor = apply { + saturation = (saturation - fraction).coerceIn(0f, 1f) + } + + /** Rotate hue by degrees (can be negative) */ + fun rotateHue(degrees: Float): CSSColor = apply { + hue = (hue + degrees) % 360f + } + + companion object { + /** Create from Android 0xAARRGGBB */ + @JvmStatic fun fromArgb(color: Int): CSSColor { + val a = ((color ushr 24) and 0xFF) / 255f + val r = ((color ushr 16) and 0xFF) / 255f + val g = ((color ushr 8) and 0xFF) / 255f + val b = ( color and 0xFF) / 255f + return CSSColor(r, g, b, a) + } + + /** Create from Android 0xRRGGBBAA */ + @JvmStatic fun fromRgba(color: Int): CSSColor { + val r = ((color ushr 24) and 0xFF) / 255f + val g = ((color ushr 16) and 0xFF) / 255f + val b = ((color ushr 8) and 0xFF) / 255f + val a = ( color and 0xFF) / 255f + return CSSColor(r, g, b, a) + } + + @JvmStatic fun fromAndroidColor(color: Int): CSSColor { + return fromArgb(color) + } + + private val NAMED_HEX = mapOf( + "aliceblue" to "F0F8FF", "antiquewhite" to "FAEBD7", "aqua" to "00FFFF", + "aquamarine" to "7FFFD4", "azure" to "F0FFFF", "beige" to "F5F5DC", + "bisque" to "FFE4C4", "black" to "000000", "blanchedalmond" to "FFEBCD", + "blue" to "0000FF", "blueviolet" to "8A2BE2", "brown" to "A52A2A", + "burlywood" to "DEB887", "cadetblue" to "5F9EA0", "chartreuse" to "7FFF00", + "chocolate" to "D2691E", "coral" to "FF7F50", "cornflowerblue" to "6495ED", + "cornsilk" to "FFF8DC", "crimson" to "DC143C", "cyan" to "00FFFF", + "darkblue" to "00008B", "darkcyan" to "008B8B", "darkgoldenrod" to "B8860B", + "darkgray" to "A9A9A9", "darkgreen" to "006400", "darkgrey" to "A9A9A9", + "darkkhaki" to "BDB76B", "darkmagenta" to "8B008B", "darkolivegreen" to "556B2F", + "darkorange" to "FF8C00", "darkorchid" to "9932CC", "darkred" to "8B0000", + "darksalmon" to "E9967A", "darkseagreen" to "8FBC8F", "darkslateblue" to "483D8B", + "darkslategray" to "2F4F4F", "darkslategrey" to "2F4F4F", "darkturquoise" to "00CED1", + "darkviolet" to "9400D3", "deeppink" to "FF1493", "deepskyblue" to "00BFFF", + "dimgray" to "696969", "dimgrey" to "696969", "dodgerblue" to "1E90FF", + "firebrick" to "B22222", "floralwhite" to "FFFAF0", "forestgreen" to "228B22", + "fuchsia" to "FF00FF", "gainsboro" to "DCDCDC", "ghostwhite" to "F8F8FF", + "gold" to "FFD700", "goldenrod" to "DAA520", "gray" to "808080", + "green" to "008000", "greenyellow" to "ADFF2F", "grey" to "808080", + "honeydew" to "F0FFF0", "hotpink" to "FF69B4", "indianred" to "CD5C5C", + "indigo" to "4B0082", "ivory" to "FFFFF0", "khaki" to "F0E68C", + "lavender" to "E6E6FA", "lavenderblush" to "FFF0F5", "lawngreen" to "7CFC00", + "lemonchiffon" to "FFFACD", "lightblue" to "ADD8E6", "lightcoral" to "F08080", + "lightcyan" to "E0FFFF", "lightgoldenrodyellow" to "FAFAD2", "lightgray" to "D3D3D3", + "lightgreen" to "90EE90", "lightgrey" to "D3D3D3", "lightpink" to "FFB6C1", + "lightsalmon" to "FFA07A", "lightseagreen" to "20B2AA", "lightskyblue" to "87CEFA", + "lightslategray" to "778899", "lightslategrey" to "778899", "lightsteelblue" to "B0C4DE", + "lightyellow" to "FFFFE0", "lime" to "00FF00", "limegreen" to "32CD32", + "linen" to "FAF0E6", "magenta" to "FF00FF", "maroon" to "800000", + "mediumaquamarine" to "66CDAA", "mediumblue" to "0000CD", "mediumorchid" to "BA55D3", + "mediumpurple" to "9370DB", "mediumseagreen" to "3CB371", "mediumslateblue" to "7B68EE", + "mediumspringgreen" to "00FA9A", "mediumturquoise" to "48D1CC", "mediumvioletred" to "C71585", + "midnightblue" to "191970", "mintcream" to "F5FFFA", "mistyrose" to "FFE4E1", + "moccasin" to "FFE4B5", "navajowhite" to "FFDEAD", "navy" to "000080", + "oldlace" to "FDF5E6", "olive" to "808000", "olivedrab" to "6B8E23", + "orange" to "FFA500", "orangered" to "FF4500", "orchid" to "DA70D6", + "palegoldenrod" to "EEE8AA", "palegreen" to "98FB98", "paleturquoise" to "AFEEEE", + "palevioletred" to "DB7093", "papayawhip" to "FFEFD5", "peachpuff" to "FFDAB9", + "peru" to "CD853F", "pink" to "FFC0CB", "plum" to "DDA0DD", + "powderblue" to "B0E0E6", "purple" to "800080", "rebeccapurple" to "663399", + "red" to "FF0000", "rosybrown" to "BC8F8F", "royalblue" to "4169E1", + "saddlebrown" to "8B4513", "salmon" to "FA8072", "sandybrown" to "F4A460", + "seagreen" to "2E8B57", "seashell" to "FFF5EE", "sienna" to "A0522D", + "silver" to "C0C0C0", "skyblue" to "87CEEB", "slateblue" to "6A5ACD", + "slategray" to "708090", "slategrey" to "708090", "snow" to "FFFAFA", + "springgreen" to "00FF7F", "steelblue" to "4682B4", "tan" to "D2B48C", + "teal" to "008080", "thistle" to "D8BFD8", "tomato" to "FF6347", + "turquoise" to "40E0D0", "violet" to "EE82EE", "wheat" to "F5DEB3", + "white" to "FFFFFF", "whitesmoke" to "F5F5F5", "yellow" to "FFFF00", + "yellowgreen" to "9ACD32" + ) + private val NAMED: Map = NAMED_HEX + .mapValues { (_, hexRgb) -> + // parse hexRgb ("RRGGBB") to Int, then OR in 0xFF000000 for full opacity + val rgb = hexRgb.toInt(16) + (rgb shl 8) or 0xFF + } + ("transparent" to 0x00000000) + + private val HEX_REGEX = Regex("^#([0-9a-fA-F]{3,8})$", RegexOption.IGNORE_CASE) + private val RGB_REGEX = Regex("^rgba?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE) + private val HSL_REGEX = Regex("^hsla?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE) + + @JvmStatic + fun parseColor(s: String): CSSColor { + val str = s.trim() + // named + NAMED[str.lowercase()]?.let { return it.RGBAtoCSSColor() } + + // hex + HEX_REGEX.matchEntire(str)?.groupValues?.get(1)?.let { part -> + return parseHexPart(part) + } + + // rgb/rgba + RGB_REGEX.matchEntire(str)?.groupValues?.get(1)?.let { + return parseRgbParts(it.split(',').map(String::trim)) + } + + // hsl/hsla + HSL_REGEX.matchEntire(str)?.groupValues?.get(1)?.let { + return parseHslParts(it.split(',').map(String::trim)) + } + + error("Cannot parse color: \"$s\"") + } + + private fun parseHexPart(p: String): CSSColor { + // expand shorthand like "RGB" or "RGBA" to full 8-chars "RRGGBBAA" + val hex = when (p.length) { + 3 -> p.map { "$it$it" }.joinToString("") + "FF" + 4 -> p.map { "$it$it" }.joinToString("") + 6 -> p + "FF" + 8 -> p + else -> error("Invalid hex color: #$p") + } + + val parsed = hex.toLong(16).toInt() + val alpha = (parsed and 0xFF) shl 24 + val rgbOnly = (parsed ushr 8) and 0x00FFFFFF + val argb = alpha or rgbOnly + return fromArgb(argb) + } + + private fun parseRgbParts(parts: List): CSSColor { + require(parts.size == 3 || parts.size == 4) { "rgb/rgba needs 3 or 4 parts" } + + // r/g/b: "128" → 128/255, "50%" → 0.5 + fun channel(ch: String): Float = + if (ch.endsWith("%")) ch.removeSuffix("%").toFloat() / 100f + else ch.toFloat().coerceIn(0f, 255f) / 255f + + // alpha: "0.5" → 0.5, "50%" → 0.5 + fun alpha(a: String): Float = + if (a.endsWith("%")) a.removeSuffix("%").toFloat() / 100f + else a.toFloat().coerceIn(0f, 1f) + + val r = channel(parts[0]) + val g = channel(parts[1]) + val b = channel(parts[2]) + val a = if (parts.size == 4) alpha(parts[3]) else 1f + + return CSSColor(r, g, b, a) + } + + private fun parseHslParts(parts: List): CSSColor { + require(parts.size == 3 || parts.size == 4) { "hsl/hsla needs 3 or 4 parts" } + + fun hueOf(h: String): Float = when { + h.endsWith("deg") -> h.removeSuffix("deg").toFloat() + h.endsWith("grad") -> h.removeSuffix("grad").toFloat() * 0.9f + h.endsWith("rad") -> h.removeSuffix("rad").toFloat() * (180f / PI.toFloat()) + h.endsWith("turn") -> h.removeSuffix("turn").toFloat() * 360f + else -> h.toFloat() + } + + // for s and l you only ever see percentages + fun pct(p: String): Float = + p.removeSuffix("%").toFloat().coerceIn(0f, 100f) / 100f + + // alpha: "0.5" → 0.5, "50%" → 0.5 + fun alpha(a: String): Float = + if (a.endsWith("%")) pct(a) + else a.toFloat().coerceIn(0f, 1f) + + val h = hueOf(parts[0]) + val s = pct(parts[1]) + val l = pct(parts[2]) + val a = if (parts.size == 4) alpha(parts[3]) else 1f + + return CSSColor(0f, 0f, 0f, a).apply { setHsl(h, s, l) } + } + } +} + +fun Int.RGBAtoCSSColor(): CSSColor = CSSColor.fromRgba(this) +fun Int.ARGBtoCSSColor(): CSSColor = CSSColor.fromArgb(this) +fun CSSColor.toAndroidColor(): Int = toArgbInt() 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 7028d59d..ebd75b44 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 @@ -18,8 +18,7 @@ class LiveEventEmojis: IPlatformLiveEvent { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis { obj.ensureIsBusy(); val contextName = "LiveEventEmojis" - return LiveEventEmojis( - obj.getOrThrow(config, "emojis", contextName)); + return LiveEventEmojis(obj.getOrThrow(config, "emojis", contextName)); } } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/livechat/LiveChatDonationListItem.kt b/app/src/main/java/com/futo/platformplayer/views/livechat/LiveChatDonationListItem.kt index 05577fcb..1a4087a8 100644 --- a/app/src/main/java/com/futo/platformplayer/views/livechat/LiveChatDonationListItem.kt +++ b/app/src/main/java/com/futo/platformplayer/views/livechat/LiveChatDonationListItem.kt @@ -1,5 +1,6 @@ package com.futo.platformplayer.views.livechat +import CSSColor import android.graphics.Color import android.graphics.drawable.LevelListDrawable import android.text.Spannable @@ -24,6 +25,7 @@ import com.futo.platformplayer.views.adapters.AnyAdapter import com.futo.platformplayer.views.overlays.LiveChatOverlay import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import toAndroidColor class LiveChatDonationListItem(viewGroup: ViewGroup) : LiveChatListItem(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_chat_donation, viewGroup, false)) { @@ -55,10 +57,10 @@ class LiveChatDonationListItem(viewGroup: ViewGroup) _amount.text = event.amount.trim(); if(event.colorDonation != null && event.colorDonation.isHexColor()) { - val color = Color.parseColor(event.colorDonation); - _amountContainer.background.setTint(color); + val color = CSSColor.parseColor(event.colorDonation); + _amountContainer.background.setTint(color.toAndroidColor()); - if((color.green > 140 || color.red > 140 || color.blue > 140) && (color.red + color.green + color.blue) > 400) + if(color.lightness > 0.5) _amount.setTextColor(Color.BLACK); else _amount.setTextColor(Color.WHITE); diff --git a/app/src/main/java/com/futo/platformplayer/views/livechat/LiveChatDonationPill.kt b/app/src/main/java/com/futo/platformplayer/views/livechat/LiveChatDonationPill.kt index 02619424..34ae1c1b 100644 --- a/app/src/main/java/com/futo/platformplayer/views/livechat/LiveChatDonationPill.kt +++ b/app/src/main/java/com/futo/platformplayer/views/livechat/LiveChatDonationPill.kt @@ -13,6 +13,7 @@ import com.bumptech.glide.Glide import com.futo.platformplayer.R import com.futo.platformplayer.api.media.models.live.LiveEventDonation import com.futo.platformplayer.isHexColor +import toAndroidColor class LiveChatDonationPill: LinearLayout { private val _imageAuthor: ImageView; @@ -33,10 +34,10 @@ class LiveChatDonationPill: LinearLayout { if(donation.colorDonation != null && donation.colorDonation.isHexColor()) { - val color = Color.parseColor(donation.colorDonation); - root.background.setTint(color); + val color = CSSColor.parseColor(donation.colorDonation); + root.background.setTint(color.toAndroidColor()); - if((color.green > 140 || color.red > 140 || color.blue > 140) && (color.red + color.green + color.blue) > 400) + if(color.lightness > 0.5) _textAmount.setTextColor(Color.BLACK); else _textAmount.setTextColor(Color.WHITE); diff --git a/app/src/main/java/com/futo/platformplayer/views/livechat/LiveChatMessageListItem.kt b/app/src/main/java/com/futo/platformplayer/views/livechat/LiveChatMessageListItem.kt index ffe7f1b3..df742225 100644 --- a/app/src/main/java/com/futo/platformplayer/views/livechat/LiveChatMessageListItem.kt +++ b/app/src/main/java/com/futo/platformplayer/views/livechat/LiveChatMessageListItem.kt @@ -18,6 +18,7 @@ import com.futo.platformplayer.views.adapters.AnyAdapter import com.futo.platformplayer.views.overlays.LiveChatOverlay import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import toAndroidColor class LiveChatMessageListItem(viewGroup: ViewGroup) : LiveChatListItem(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_chat_message, viewGroup, false)) { @@ -75,7 +76,7 @@ class LiveChatMessageListItem(viewGroup: ViewGroup) if (!event.colorName.isNullOrEmpty()) { try { - _authorName.setTextColor(Color.parseColor(event.colorName)); + _authorName.setTextColor(CSSColor.parseColor(event.colorName).toAndroidColor()); } catch (ex: Throwable) { } } else diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt index fc3eff23..ea20bf74 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt @@ -43,6 +43,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import toAndroidColor class LiveChatOverlay : LinearLayout { @@ -291,10 +292,10 @@ class LiveChatOverlay : LinearLayout { _overlayDonation_Amount.text = donation.amount.trim(); _overlayDonation.visibility = VISIBLE; if(donation.colorDonation != null && donation.colorDonation.isHexColor()) { - val color = Color.parseColor(donation.colorDonation); - _overlayDonation_AmountContainer.background.setTint(color); + val color = CSSColor.parseColor(donation.colorDonation); + _overlayDonation_AmountContainer.background.setTint(color.toAndroidColor()); - if((color.green > 140 || color.red > 140 || color.blue > 140) && (color.red + color.green + color.blue) > 400) + if(color.lightness > 0.5) _overlayDonation_Amount.setTextColor(Color.BLACK) else _overlayDonation_Amount.setTextColor(Color.WHITE); diff --git a/app/src/test/java/com/futo/platformplayer/CSSColorTests.kt b/app/src/test/java/com/futo/platformplayer/CSSColorTests.kt new file mode 100644 index 00000000..e8f780cf --- /dev/null +++ b/app/src/test/java/com/futo/platformplayer/CSSColorTests.kt @@ -0,0 +1,257 @@ +package com.futo.platformplayer + +import CSSColor +import org.junit.Assert.assertEquals +import kotlin.math.PI +import kotlin.math.abs +import kotlin.test.Test +import kotlin.test.assertTrue + +class CSSColorTest { + + private fun approxEq(expected: Float, actual: Float, eps: Float = 1e-5f) { + assertTrue(abs(expected - actual) <= eps, "Expected $expected but got $actual") + } + + @Test fun `hex #RRGGBB parses correctly`() { + val c = CSSColor.parseColor("#336699") + assertEquals(0x33, c.red) + assertEquals(0x66, c.green) + assertEquals(0x99, c.blue) + assertEquals(255, c.alpha) + } + + @Test fun `hex #RGB shorthand expands`() { + val c = CSSColor.parseColor("#369") + assertEquals(0x33, c.red) + assertEquals(0x66, c.green) + assertEquals(0x99, c.blue) + } + + @Test fun `hex #RRGGBBAA parses alpha`() { + val c = CSSColor.parseColor("#33669980") + assertEquals(0x33, c.red) + assertEquals(0x66, c.green) + assertEquals(0x99, c.blue) + approxEq(128 / 255f, c.a) + assertEquals(128, c.alpha) + } + + @Test fun `hex #RGBA shorthand expands with alpha`() { + val c = CSSColor.parseColor("#3698") + assertEquals(0x33, c.red) + assertEquals(0x66, c.green) + assertEquals(0x99, c.blue) + assertEquals(0x88, c.alpha) + } + + @Test fun `hex uppercase shorthand parses`() { + val c = CSSColor.parseColor("#AbC") + // expands to AABBCC + assertEquals(0xAA, c.red) + assertEquals(0xBB, c.green) + assertEquals(0xCC, c.blue) + } + + @Test fun `rgb(ints) functional parser`() { + val c = CSSColor.parseColor("rgb(255,128,0)") + assertEquals(255, c.red) + assertEquals(128, c.green) + assertEquals(0, c.blue) + assertEquals(255, c.alpha) + } + + @Test fun `rgb(percent) functional parser`() { + val c = CSSColor.parseColor("rgb(100%,50%,0%)") + assertEquals(255, c.red) + assertEquals(128, c.green) + assertEquals(0, c.blue) + } + + @Test fun `rgba raw‐float alpha functional parser`() { + val c = CSSColor.parseColor("rgba(255,0,0,0.5)") + assertEquals(255, c.red) + assertEquals(0, c.green) + assertEquals(0, c.blue) + approxEq(0.5f, c.a) + } + + @Test fun `rgba percent alpha functional parser`() { + val c = CSSColor.parseColor("rgba(100%,0%,0%,50%)") + assertEquals(255, c.red) + assertEquals(0, c.green) + assertEquals(0, c.blue) + approxEq(0.5f, c.a) + } + + @Test fun `hsl() functional parser yields correct RGB`() { + // pure green: hue=120°, sat=100%, light=50% + val c = CSSColor.parseColor("hsl(120,100%,50%)") + assertEquals(0, c.red) + assertEquals(255, c.green) + assertEquals(0, c.blue) + } + + @Test fun `hsla percent alpha functional parser`() { + val c = CSSColor.parseColor("hsla(240,100%,50%,25%)") + // pure blue, alpha 25% + assertEquals(0, c.red) + assertEquals(0, c.green) + assertEquals(255, c.blue) + approxEq(0.25f, c.a) + } + + @Test fun `hsla raw‐float alpha functional parser`() { + val c = CSSColor.parseColor("hsla(240,100%,50%,0.25)") + assertEquals(0, c.red) + assertEquals(0, c.green) + assertEquals(255, c.blue) + approxEq(0.25f, c.a) + } + + @Test fun `hsl radian unit parsing`() { + // 180° = π radians → cyan + val c = CSSColor.parseColor("hsl(${PI}rad,100%,50%)") + assertEquals(0, c.red) + assertEquals(255, c.green) + assertEquals(255, c.blue) + } + + @Test fun `hsl turn unit parsing`() { + // 0.5 turn = 180° → cyan + val c = CSSColor.parseColor("hsl(0.5turn,100%,50%)") + assertEquals(0, c.red) + assertEquals(255, c.green) + assertEquals(255, c.blue) + } + + @Test fun `hsl grad unit parsing`() { + // 200 grad = 180° → cyan + val c = CSSColor.parseColor("hsl(200grad,100%,50%)") + assertEquals(0, c.red) + assertEquals(255, c.green) + assertEquals(255, c.blue) + } + + @Test fun `named colors parse`() { + val red = CSSColor.parseColor("red") + assertEquals(255, red.red) + assertEquals(0, red.green) + assertEquals(0, red.blue) + + val rebecca = CSSColor.parseColor("rebeccapurple") + assertEquals(0x66, rebecca.red) + assertEquals(0x33, rebecca.green) + assertEquals(0x99, rebecca.blue) + + val transparent = CSSColor.parseColor("transparent") + assertEquals(0, transparent.alpha) + } + + @Test fun `round-trip Android Int ↔ CSSColor`() { + val original = CSSColor(0.2f, 0.4f, 0.6f, 0.8f) + val colorInt = original.toRgbaInt() + val back = CSSColor.fromRgba(colorInt) + approxEq(original.r, back.r) + approxEq(original.g, back.g) + approxEq(original.b, back.b) + approxEq(original.a, back.a) + } + + @Test fun `individual channel setters`() { + val c = CSSColor(0f,0f,0f,1f) + c.red = 128; assertEquals(128, c.red); approxEq(128/255f, c.r) + c.green = 64; assertEquals(64, c.green); approxEq(64/255f, c.g) + c.blue = 32; assertEquals(32, c.blue); approxEq(32/255f, c.b) + c.alpha = 200; assertEquals(200, c.alpha); approxEq(200/255f, c.a) + } + + @Test fun `hsl channel setters update RGB`() { + val c = CSSColor.parseColor("hsl(0,100%,50%)") // red + c.hue = 120f // → green + assertEquals(0, c.red) + assertEquals(255, c.green) + assertEquals(0, c.blue) + + c.saturation = 0f // → gray + assertTrue(c.red == c.green && c.green == c.blue) + } + + @Test fun `convenience modifiers chain as expected`() { + val c = CSSColor.parseColor("#888888") + .lighten(0.1f) + .saturate(0.2f) + .rotateHue(45f) + + approxEq(0.633f, c.lightness, eps = 1e-3f) + approxEq(0.2f, c.saturation, eps = 1e-3f) + approxEq(45f, c.hue) + } + + @Test + fun `invalid formats throw IllegalArgumentException`() { + listOf("", "rgb()", "hsl(0,0)", "#12", "rgba(0,0,0,150%)", "hsla(0,0%,0%,2)").forEach { bad -> + try { + CSSColor.parseColor(bad) + assert(false) + } catch (e: Throwable) { + + } + } + } + + @Test fun `out‐of‐range RGB ints clamp`() { + val c = CSSColor.parseColor("rgb(300,-20, 260)") + assertEquals(255, c.red) + assertEquals(0, c.green) + assertEquals(255, c.blue) + } + + @Test fun `parser is case- and whitespace-tolerant`() { + val a = CSSColor.parseColor(" RgB( 10 ,20, 30 )") + assertEquals(10, a.red) + assertEquals(20, a.green) + assertEquals(30, a.blue) + + val b = CSSColor.parseColor(" ReBeCcaPURple ") + assertEquals(0x66, b.red) + assertEquals(0x33, b.green) + assertEquals(0x99, b.blue) + } + + @Test fun `hsl lightness extremes`() { + // lightness = 0 → black + val black = CSSColor.parseColor("hsl(123,45%,0%)") + assertEquals(0, black.red) + assertEquals(0, black.green) + assertEquals(0, black.blue) + // lightness = 100% → white + val white = CSSColor.parseColor("hsl(321,55%,100%)") + assertEquals(255, white.red) + assertEquals(255, white.green) + assertEquals(255, white.blue) + // saturation = 0 → gray (r==g==b) + val gray = CSSColor.parseColor("hsl(50,0%,60%)") + assertTrue(gray.red == gray.green && gray.green == gray.blue) + } + + @Test fun `hsl negative and large hues wrap`() { + val c1 = CSSColor.parseColor("hsl(-120,100%,50%)") // → same as 240° + assertEquals(0, c1.red) + assertEquals(0, c1.green) + assertEquals(255, c1.blue) + + val c2 = CSSColor.parseColor("hsl(480,100%,50%)") // → same as 120° + assertEquals(0, c2.red) + assertEquals(255, c2.green) + assertEquals(0, c2.blue) + } + + @Test fun `lighten then darken returns original`() { + val base = CSSColor.parseColor("#123456") + val round = base.lighten(0.2f).darken(0.2f) + approxEq(base.r, round.r) + approxEq(base.g, round.g) + approxEq(base.b, round.b) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/futo/platformplayer/NoiseProtocolTests.kt b/app/src/test/java/com/futo/platformplayer/NoiseProtocolTests.kt index 189fc767..6e0e3463 100644 --- a/app/src/test/java/com/futo/platformplayer/NoiseProtocolTests.kt +++ b/app/src/test/java/com/futo/platformplayer/NoiseProtocolTests.kt @@ -26,7 +26,7 @@ import java.util.Random import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit - +/* class NoiseProtocolTest { constructor() { Logger.setLogConsumers(listOf( @@ -625,4 +625,4 @@ class NoiseProtocolTest { throw Exception("Byte mismatch at index ${i}") } } -} \ No newline at end of file +}*/ \ No newline at end of file From b69402dfe922f96c92209317e8b5db7d92f336e0 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Thu, 3 Jul 2025 00:44:54 +0200 Subject: [PATCH 82/87] WIP Async support for Android --- .../com/futo/platformplayer/Extensions_V8.kt | 79 +++++++++++ .../api/media/platforms/js/JSClient.kt | 1 - .../sources/JSDashManifestRawAudioSource.kt | 6 +- .../models/sources/JSDashManifestRawSource.kt | 5 +- .../futo/platformplayer/engine/V8Plugin.kt | 124 +++++++++++++++++- .../engine/packages/PackageBridge.kt | 6 +- 6 files changed, 212 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt index 240a6cfc..ecf4a276 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt @@ -2,12 +2,22 @@ package com.futo.platformplayer import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.primitive.* +import com.caoccao.javet.values.reference.IV8ValuePromise import com.caoccao.javet.values.reference.V8ValueArray +import com.caoccao.javet.values.reference.V8ValueError import com.caoccao.javet.values.reference.V8ValueObject +import com.caoccao.javet.values.reference.V8ValuePromise import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.V8Plugin +import com.futo.platformplayer.engine.exceptions.ScriptExecutionException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.logging.Logger +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.cancel +import java.util.concurrent.CancellationException +import java.util.concurrent.CountDownLatch +import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType //V8 @@ -174,4 +184,73 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap { for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get(it).toString() }) map.put(prop, obj.getString(prop)); return map; +} + + +fun V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T { + val latch = CountDownLatch(1); + var promiseResult: T? = null; + var promiseException: Throwable? = null; + plugin.busy { + this.register(object: IV8ValuePromise.IListener { + override fun onFulfilled(p0: V8Value?) { + if(p0 is V8ValueError) + promiseException = ScriptExecutionException(plugin.config, p0.message); + else + promiseResult = p0 as T; + latch.countDown(); + } + override fun onRejected(p0: V8Value?) { + promiseException = (NotImplementedError("onRejected promise not implemented..")); + latch.countDown(); + } + override fun onCatch(p0: V8Value?) { + promiseException = (NotImplementedError("onCatch promise not implemented..")); + latch.countDown(); + } + }); + } + + plugin.registerPromise(this) { + promiseException = CancellationException("Cancelled by system"); + latch.countDown(); + } + plugin.unbusy { + latch.await(); + } + if(promiseException != null) + throw promiseException!!; + return promiseResult!!; +} +fun V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): Deferred { + val def = CompletableDeferred(); + val promise = this; + this.register(object: IV8ValuePromise.IListener { + override fun onFulfilled(p0: V8Value?) { + plugin.resolvePromise(promise); + def.complete(p0 as T); + } + override fun onRejected(p0: V8Value?) { + plugin.resolvePromise(promise); + def.completeExceptionally(NotImplementedError("onRejected promise not implemented..")); + } + override fun onCatch(p0: V8Value?) { + plugin.resolvePromise(promise); + def.completeExceptionally(NotImplementedError("onCatch promise not implemented..")); + } + }); + plugin.registerPromise(promise) { + if(def.isActive) + def.cancel("Cancelled by system"); + } + return def; +} + + +fun V8ValueObject.invokeV8(method: String, vararg obj: Any): T { + var result = this.invoke(method, *obj); + if(result is V8ValuePromise) { + return result.toV8ValueBlocking(this.getSourcePlugin()!!); + } + return result as T; } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt index d61ebc0b..8c4097ae 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 @@ -632,7 +632,6 @@ open class JSClient : IPlatformClient { plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})")); } - @JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page") @JSDocsParameter("url", "Url of content") override fun getContentRecommendations(url: String): IPager? = isBusyWith("getContentRecommendations") { 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 f4994e0b..a484527c 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 @@ -1,5 +1,6 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources +import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource @@ -13,6 +14,7 @@ import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrThrow +import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.others.Language import com.futo.platformplayer.states.StateDeveloper @@ -63,14 +65,14 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { _plugin.isBusyWith("dashAudio.generate") { - _obj.invokeString("generate"); + _obj.invokeV8("generate").value; } } } else result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { _plugin.isBusyWith("dashAudio.generate") { - _obj.invokeString("generate"); + _obj.invokeV8("generate").value; } } 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 184b783d..a724a26e 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 @@ -15,6 +15,7 @@ import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrThrow +import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.states.StateDeveloper interface IJSDashManifestRawSource { @@ -68,7 +69,7 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { _plugin.isBusyWith("dashVideo.generate") { - _obj.invokeString("generate"); + _obj.invokeV8("generate").value; } }); } @@ -76,7 +77,7 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo else result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { _plugin.isBusyWith("dashVideo.generate") { - _obj.invokeString("generate"); + _obj.invokeV8("generate").value; } }); 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 0cb2f196..8a3c1c20 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -10,7 +10,9 @@ import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.primitive.V8ValueBoolean import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueString +import com.caoccao.javet.values.reference.IV8ValuePromise import com.caoccao.javet.values.reference.V8ValueObject +import com.caoccao.javet.values.reference.V8ValuePromise import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.constructs.Event1 @@ -37,7 +39,15 @@ import com.futo.platformplayer.engine.packages.V8Package import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateAssets +import com.futo.platformplayer.toList +import com.futo.platformplayer.toV8ValueBlocking +import com.futo.platformplayer.toV8ValueAsync import com.futo.platformplayer.warnIfMainThread +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.cancel +import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -48,6 +58,7 @@ class V8Plugin { private val _clientAuth: ManagedHttpClient; private val _clientOthers: ConcurrentHashMap = ConcurrentHashMap(); + private val _promises = ConcurrentHashMapUnit)?>(); val httpClient: ManagedHttpClient get() = _client; val httpClientAuth: ManagedHttpClient get() = _clientAuth; @@ -223,37 +234,144 @@ class V8Plugin { Logger.i(TAG, "Plugin stopped"); onStopped.emit(this); } + cancelAllPromises(); } fun isThreadAlreadyBusy(): Boolean { return _busyLock.isHeldByCurrentThread; } fun busy(handle: ()->T): T { + _busyLock.lock(); + try { + return handle(); + } + finally { + _busyLock.unlock(); + } + /* _busyLock.withLock { //Logger.i(TAG, "Entered busy: " + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()); return handle(); + }*/ + } + fun unbusy(handle: ()->T): T { + val wasLocked = isThreadAlreadyBusy(); + if(!wasLocked) + return handle(); + val lockCount = _busyLock.holdCount; + for(i in 1..lockCount) + _busyLock.unlock(); + try { + Logger.w(TAG, "Unlocking V8 thread for [${config.name}] for a blocking resolve of a promise") + return handle(); + } + finally { + Logger.w(TAG, "Relocking V8 thread for [${config.name}] for a blocking resolve of a promise") + + for(i in 1..lockCount) + _busyLock.lock(); } } fun execute(js: String) : V8Value { return executeTyped(js); } + + suspend fun executeTypedAsync(js: String) : Deferred { + warnIfMainThread("V8Plugin.executeTyped"); + if(isStopped) + throw PluginEngineStoppedException(config, "Instance is stopped", js); + + return withContext(IO) { + return@withContext busy { + try { + val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet"); + val result = catchScriptErrors("Plugin[${config.name}]", js) { + runtime.getExecutor(js).execute() + }; + + if (result is V8ValuePromise) { + return@busy result.toV8ValueAsync(this@V8Plugin); + } else + return@busy CompletableDeferred(result as T); + } + catch(ex: Throwable) { + val def = CompletableDeferred(); + def.completeExceptionally(ex); + return@busy def; + } + } + } + } fun executeTyped(js: String) : T { warnIfMainThread("V8Plugin.executeTyped"); if(isStopped) throw PluginEngineStoppedException(config, "Instance is stopped", js); - return busy { - + val result = busy { val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet"); - return@busy catchScriptErrors("Plugin[${config.name}]", js) { + return@busy catchScriptErrors("Plugin[${config.name}]", js) { runtime.getExecutor(js).execute() }; + }; + if(result is V8ValuePromise) { + return result.toV8ValueBlocking(this@V8Plugin); } + return result as T; } 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 } } + + fun handlePromise(result: V8ValuePromise): CompletableDeferred { + val def = CompletableDeferred(); + result.register(object: IV8ValuePromise.IListener { + override fun onFulfilled(p0: V8Value?) { + resolvePromise(result); + def.complete(p0 as T); + } + override fun onRejected(p0: V8Value?) { + resolvePromise(result); + def.completeExceptionally(NotImplementedError("onRejected promise not implemented..")); + } + override fun onCatch(p0: V8Value?) { + resolvePromise(result); + def.completeExceptionally(NotImplementedError("onCatch promise not implemented..")); + } + }); + registerPromise(result) { + if(def.isActive) + def.cancel("Cancelled by system"); + } + return def; + } + fun registerPromise(promise: V8ValuePromise, onCancelled: ((V8ValuePromise)->Unit)? = null) { + Logger.v(TAG, "Promise registered for plugin [${config.name}]: ${promise.hashCode()}"); + if (onCancelled != null) { + _promises.put(promise, onCancelled) + }; + } + fun resolvePromise(promise: V8ValuePromise, cancelled: Boolean = false) { + Logger.v(TAG, "Promise resolved for plugin [${config.name}]: ${promise.hashCode()}"); + val found = synchronized(_promises) { + val found = _promises.getOrDefault(promise, null); + _promises.remove(promise); + return@synchronized found; + }; + if(found != null) + found(promise); + } + fun cancelAllPromises(){ + val promises = _promises.keys().toList(); + for(key in promises) { + try { + resolvePromise(key, true); + } + catch(ex: Throwable) {} + } + } + + private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? { //TODO: Auto get all package types? return when(packageName) { 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 72bdf34f..db44c1fc 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 @@ -84,7 +84,8 @@ class PackageBridge : V8Package { fun supportedFeatures(): Array { return arrayOf( "ReloadRequiredException", - "HttpBatchClient" + "HttpBatchClient", + "Async" ); } @@ -130,9 +131,12 @@ class PackageBridge : V8Package { } timeoutMap.remove(id); try { + Logger.w(TAG, "setTimeout before busy (${timeout}): ${_plugin.isBusy}"); _plugin.busy { + Logger.w(TAG, "setTimeout in busy"); if (!_plugin.isStopped) funcClone.callVoid(null, arrayOf()); + Logger.w(TAG, "setTimeout after"); } } catch (ex: Throwable) { Logger.e(TAG, "Failed timeout callback", ex); From 5b954727a1c1ebde2c2b9070c440ad5ce47a6087 Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 3 Jul 2025 14:30:01 +0200 Subject: [PATCH 83/87] Implemented incoming and outgoing raids. --- app/src/main/assets/scripts/source.js | 3 ++- .../api/media/models/live/LiveEventRaid.kt | 8 ++++++-- .../platformplayer/views/overlays/LiveChatOverlay.kt | 11 +++++------ app/src/main/res/layout/overlay_livechat.xml | 10 +++++----- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/app/src/main/assets/scripts/source.js b/app/src/main/assets/scripts/source.js index 9f38404d..20fb306d 100644 --- a/app/src/main/assets/scripts/source.js +++ b/app/src/main/assets/scripts/source.js @@ -707,11 +707,12 @@ class LiveEventViewCount extends LiveEvent { } } class LiveEventRaid extends LiveEvent { - constructor(targetUrl, targetName, targetThumbnail) { + constructor(targetUrl, targetName, targetThumbnail, isOutgoing) { super(100); this.targetUrl = targetUrl; this.targetName = targetName; this.targetThumbnail = targetThumbnail; + this.isOutgoing = isOutgoing; } } 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 f43a7c5b..6663852d 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 @@ -3,6 +3,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.getOrDefault import com.futo.platformplayer.getOrThrow class LiveEventRaid: IPlatformLiveEvent { @@ -11,11 +12,13 @@ class LiveEventRaid: IPlatformLiveEvent { val targetName: String; val targetThumbnail: String; val targetUrl: String; + val isOutgoing: Boolean; - constructor(name: String, url: String, thumbnail: String) { + constructor(name: String, url: String, thumbnail: String, isOutgoing: Boolean) { this.targetName = name; this.targetUrl = url; this.targetThumbnail = thumbnail; + this.isOutgoing = isOutgoing; } companion object { @@ -25,7 +28,8 @@ class LiveEventRaid: IPlatformLiveEvent { return LiveEventRaid( obj.getOrThrow(config, "targetName", contextName), obj.getOrThrow(config, "targetUrl", contextName), - obj.getOrThrow(config, "targetThumbnail", contextName)); + obj.getOrThrow(config, "targetThumbnail", contextName), + obj.getOrDefault(config, "isOutgoing", contextName, true) ?: true); } } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt index ea20bf74..a3a87946 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt @@ -14,9 +14,6 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.graphics.blue -import androidx.core.graphics.green -import androidx.core.graphics.red import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView @@ -70,7 +67,7 @@ class LiveChatOverlay : LinearLayout { private val _overlayRaid_Thumbnail: ImageView; private val _overlayRaid_ButtonGo: Button; - private val _overlayRaid_ButtonPrevent: Button; + private val _overlayRaid_ButtonDismiss: Button; private val _textViewers: TextView; @@ -151,7 +148,7 @@ class LiveChatOverlay : LinearLayout { _overlayRaid_Name = findViewById(R.id.raid_name); _overlayRaid_Thumbnail = findViewById(R.id.raid_thumbnail); _overlayRaid_ButtonGo = findViewById(R.id.raid_button_go); - _overlayRaid_ButtonPrevent = findViewById(R.id.raid_button_prevent); + _overlayRaid_ButtonDismiss = findViewById(R.id.raid_button_prevent); _overlayRaid.visibility = View.GONE; @@ -160,7 +157,7 @@ class LiveChatOverlay : LinearLayout { onRaidNow.emit(it); } } - _overlayRaid_ButtonPrevent.setOnClickListener { + _overlayRaid_ButtonDismiss.setOnClickListener { _currentRaid?.let { _currentRaid = null; _overlayRaid.visibility = View.GONE; @@ -373,6 +370,8 @@ class LiveChatOverlay : LinearLayout { } else _overlayRaid.visibility = View.GONE; + + _overlayRaid_ButtonGo.visibility = if (raid?.isOutgoing == true) View.VISIBLE else View.GONE } } fun setViewCount(viewCount: Int) { diff --git a/app/src/main/res/layout/overlay_livechat.xml b/app/src/main/res/layout/overlay_livechat.xml index 3e83d8e4..d4c13f6e 100644 --- a/app/src/main/res/layout/overlay_livechat.xml +++ b/app/src/main/res/layout/overlay_livechat.xml @@ -263,8 +263,8 @@ android:textSize="13dp" android:letterSpacing="0" android:fontFamily="@font/inter_regular" - android:layout_marginStart="20dp" - android:backgroundTint="#2F2F2F" + android:layout_marginStart="5dp" + android:backgroundTint="@color/colorPrimary" android:layout_marginEnd="5dp" android:text="@string/go_now"/>