From 545216c25cfdccfa536e812f1e34b5537156ff82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Tue, 31 Mar 2026 14:42:47 +0200 Subject: [PATCH 01/15] feat: improve track selection and restore audio-only for combined UMP --- .../futo/platformplayer/Extensions_Content.kt | 4 +- .../fragment/mainactivity/main/ShortView.kt | 38 +++++++++++--- .../mainactivity/main/VideoDetailView.kt | 41 ++++++++++++--- .../platformplayer/helpers/VideoHelper.kt | 52 ++++++++++++++++++- .../views/video/FutoVideoPlayerBase.kt | 50 +++++++++++++----- 5 files changed, 154 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt index 72903ec80..86a66f15b 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt @@ -9,6 +9,7 @@ import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource import com.futo.platformplayer.helpers.VideoHelper +import com.futo.platformplayer.views.video.FutoVideoPlayerBase.Companion.DEFAULT_USER_AGENT import com.futo.platformplayer.views.video.datasources.JSHttpDataSource fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this); @@ -24,8 +25,9 @@ fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory { } else if (requestModifier != null) { JSHttpDataSource.Factory().setRequestModifier(requestModifier); } else { - DefaultHttpDataSource.Factory(); + DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); } } + fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any()); \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt index d7b8c0b89..2e0566564 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt @@ -44,7 +44,9 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource +import com.futo.platformplayer.api.media.platforms.js.models.sources.IJSDashManifestRawSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource +import com.futo.platformplayer.helpers.toAudioSource import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event3 @@ -438,9 +440,8 @@ class ShortView : FrameLayout { } else { video = videoDetails videoSources = video?.video?.videoSources?.toList() - audioSources = - if (video?.video?.isUnMuxed == true) (video.video as VideoUnMuxedSourceDescriptor).audioSources.toList() - else null + audioSources = if (video?.video?.isUnMuxed == true) (video.video as VideoUnMuxedSourceDescriptor).audioSources.toList() + else video?.video?.videoSources?.map { it.toAudioSource() } if (videoLocal != null) { localVideoSources = videoLocal.videoSource.toList() localAudioSource = videoLocal.audioSource.toList() @@ -460,10 +461,15 @@ class ShortView : FrameLayout { ?.filterNotNull()?.toList() ?: listOf() else videoSources?.toList() ?: listOf() val bestAudioContainer = audioSources?.let { VideoHelper.selectBestAudioSource(it, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS)?.container } - val bestAudioSources = - if (doDedup) audioSources?.filter { it.container == bestAudioContainer } - ?.plus(audioSources.filter { it is IHLSManifestAudioSource || it is IDashManifestSource }) - ?.distinct()?.toList() ?: listOf() else audioSources?.toList() ?: listOf() + val bestAudioSources = if (doDedup && audioSources != null) { + val audioLangs = audioSources.map { it.language }.distinct() + audioLangs.map { lang -> + VideoHelper.selectBestAudioSource(audioSources.filter { it.language == lang }, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS) + }.plus(audioSources.filter { it is IHLSManifestAudioSource || it is IDashManifestSource || it is IJSDashManifestRawSource }) + .filterNotNull() + .distinct() + .toList() + } else audioSources?.toList() ?: listOf() val canSetSpeed = true val currentPlaybackRate = player.getPlaybackRate() @@ -572,7 +578,7 @@ class ShortView : FrameLayout { overlayQualitySelector?.selectOption("audio", _lastAudioSource) overlayQualitySelector?.selectOption("subtitles", _lastSubtitleSource) - if (_lastVideoSource is IDashManifestSource || _lastVideoSource is IHLSManifestSource) { + if (_lastVideoSource is IDashManifestSource || _lastVideoSource is IHLSManifestSource || _lastVideoSource is IJSDashManifestRawSource) { val videoTracks = player.exoPlayer?.player?.currentTracks?.groups?.firstOrNull { it.mediaTrackGroup.type == C.TRACK_TYPE_VIDEO } @@ -601,6 +607,22 @@ class ShortView : FrameLayout { ?.setSubText("${player.exoPlayer?.player?.videoFormat?.width}x${player.exoPlayer?.player?.videoFormat?.height}") overlayQualitySelector?.selectOption("video", "auto") } + + // Audio track selection from manifest + val audioTracks = player.exoPlayer?.player?.currentTracks?.groups?.firstOrNull { it.mediaTrackGroup.type == C.TRACK_TYPE_AUDIO } + if (audioTracks != null) { + var audioMenuGroup: SlideUpMenuGroup? = null + for (view in overlayQualitySelector!!.groupItems) { + if (view is SlideUpMenuGroup && view.groupTag == "audio") { + audioMenuGroup = view + } + } + + val currentAudioTrack = player.exoPlayer?.player?.audioFormat + if (currentAudioTrack != null) { + overlayQualitySelector?.selectOption("audio", currentAudioTrack) + } + } } val currentPlaybackRate = player.getPlaybackRate() 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 76435cf0e..780fa3610 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 @@ -84,7 +84,9 @@ import com.futo.platformplayer.api.media.models.video.LocalVideoDetails import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig +import com.futo.platformplayer.api.media.platforms.js.models.sources.IJSDashManifestRawSource import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails + import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.casting.CastConnectionState @@ -106,6 +108,7 @@ import com.futo.platformplayer.fixHtmlLinks import com.futo.platformplayer.fixHtmlWhitespace import com.futo.platformplayer.getNowDiffSeconds import com.futo.platformplayer.helpers.VideoHelper +import com.futo.platformplayer.helpers.toAudioSource import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.receivers.MediaControlReceiver @@ -2046,6 +2049,10 @@ class VideoDetailView : ConstraintLayout { val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount()); val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context)); val subtitleSource = _lastSubtitleSource ?: (if (Settings.instance.playback.stickySubtitles) _player.getPreferredSubtitleSource(video, _subtitleLanguage) else null) ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null); + + _player.setPreferredAudioLanguage(Settings.instance.playback.getPrimaryLanguage(context)); + _player.setPreferredSubtitleLanguage(_subtitleLanguage); + Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)") if(videoSource == null && audioSource == null) { @@ -2310,7 +2317,7 @@ class VideoDetailView : ConstraintLayout { _overlay_quality_selector?.selectOption("audio", _lastAudioSource); _overlay_quality_selector?.selectOption("subtitles", _lastSubtitleSource); - if (_lastVideoSource is IDashManifestSource || _lastVideoSource is IHLSManifestSource) { + if (_lastVideoSource is IDashManifestSource || _lastVideoSource is IHLSManifestSource || _lastVideoSource is IJSDashManifestRawSource) { val videoTracks = _player.exoPlayer?.player?.currentTracks?.groups?.firstOrNull { it.mediaTrackGroup.type == C.TRACK_TYPE_VIDEO } @@ -2340,6 +2347,22 @@ class VideoDetailView : ConstraintLayout { ?.setSubText("${_player.exoPlayer?.player?.videoFormat?.width}x${_player.exoPlayer?.player?.videoFormat?.height}") _overlay_quality_selector?.selectOption("video", "auto") } + + // Audio track selection from manifest + val audioTracks = _player.exoPlayer?.player?.currentTracks?.groups?.firstOrNull { it.mediaTrackGroup.type == C.TRACK_TYPE_AUDIO } + if (audioTracks != null) { + var audioMenuGroup: SlideUpMenuGroup? = null + for (view in _overlay_quality_selector!!.groupItems) { + if (view is SlideUpMenuGroup && view.groupTag == "audio") { + audioMenuGroup = view + } + } + + val currentAudioTrack = _player.exoPlayer?.player?.audioFormat + if (currentAudioTrack != null) { + _overlay_quality_selector?.selectOption("audio", currentAudioTrack) + } + } } val currentPlaybackRate = (if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()) ?: 1.0 @@ -2436,7 +2459,7 @@ class VideoDetailView : ConstraintLayout { videoSources = video?.video?.videoSources?.toList(); audioSources = if(video?.video?.isUnMuxed == true) (video.video as VideoUnMuxedSourceDescriptor).audioSources.toList() - else null + else video?.video?.videoSources?.map { it.toAudioSource() } if(videoLocal != null) { localVideoSources = videoLocal.videoSource.toList(); localAudioSource = videoLocal.audioSource.toList(); @@ -2504,11 +2527,15 @@ class VideoDetailView : ConstraintLayout { ?.filterNotNull() ?.toList() ?: listOf() else videoSources?.toList() ?: listOf() val bestAudioContainer = audioSources?.let { VideoHelper.selectBestAudioSource(it, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS)?.container }; - val bestAudioSources = if(doDedup) audioSources - ?.filter { it.container == bestAudioContainer } - ?.plus(audioSources.filter { it is IHLSManifestAudioSource || it is IDashManifestSource }) - ?.distinct() - ?.toList() ?: listOf() else audioSources?.toList() ?: listOf(); + val bestAudioSources = if(doDedup && audioSources != null) { + val audioLangs = audioSources.map { it.language }.distinct(); + audioLangs.map { lang -> + VideoHelper.selectBestAudioSource(audioSources.filter { it.language == lang }, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS) + }.plus(audioSources.filter { it is IHLSManifestAudioSource || it is IDashManifestSource || it is IJSDashManifestRawSource }) + .filterNotNull() + .distinct() + .toList() + } else audioSources?.toList() ?: listOf(); val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed() == true val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate() diff --git a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt index 1947736d8..596ed77ec 100644 --- a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt +++ b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt @@ -12,6 +12,7 @@ import androidx.media3.exoplayer.source.MediaSource import com.futo.platformplayer.Settings import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor +import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource @@ -23,9 +24,11 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource +import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource +import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlSource import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.others.Language import getHttpDataSourceFactory @@ -105,8 +108,10 @@ class VideoHelper { fun selectBestAudioSource(desc: IVideoSourceDescriptor, prefContainers : Array, prefLanguage: String? = null, targetBitrate: Long? = null) : IAudioSource? { - if(!desc.isUnMuxed) - return null; + if(!desc.isUnMuxed) { + val audioEquivalent = desc.videoSources.map { it.toAudioSource() }; + return selectBestAudioSource(audioEquivalent, prefContainers, prefLanguage, targetBitrate); + } return selectBestAudioSource((desc as VideoUnMuxedSourceDescriptor).audioSources.toList(), prefContainers, prefLanguage, targetBitrate); } @@ -329,3 +334,46 @@ class VideoHelper { } } } + +fun IVideoSource.toAudioSource(): IAudioSource { + if (this is IAudioSource) return this + return when (this) { + is JSVideoUrlRangeSource -> { + val plugin = this.getUnderlyingPlugin() ?: throw IllegalStateException("Plugin is null") + val obj = this.getUnderlyingObject() ?: throw IllegalStateException("Object is null") + JSAudioUrlRangeSource(plugin, obj) + } + is JSVideoUrlSource -> { + val plugin = this.getUnderlyingPlugin() ?: throw IllegalStateException("Plugin is null") + val obj = this.getUnderlyingObject() ?: throw IllegalStateException("Object is null") + JSAudioUrlSource(plugin, obj) + } + is IVideoUrlSource -> AudioUrlSource( + name = this.name, + url = this.getVideoUrl(), + bitrate = this.bitrate ?: 0, + container = this.container, + codec = this.codec, + language = this.language ?: Language.UNKNOWN, + duration = this.duration, + priority = this.priority, + original = this.original ?: false + ) + is JSDashManifestRawSource -> { + val plugin = this.getUnderlyingPlugin() ?: throw IllegalStateException("Plugin is null") + val obj = this.getUnderlyingObject() ?: throw IllegalStateException("Object is null") + JSDashManifestRawAudioSource(plugin, obj) + } + else -> AudioUrlSource( + name = this.name, + url = "", + bitrate = this.bitrate ?: 0, + container = this.container, + codec = this.codec, + language = this.language ?: Language.UNKNOWN, + duration = this.duration, + priority = this.priority, + original = this.original ?: false + ) + } +} 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 0404e4633..0afa06bac 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 @@ -147,6 +147,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout { var targetTrackVideoHeight = -1 private set private var _targetTrackAudioBitrate = -1 + var preferredAudioLanguage: String? = null + private set; + var preferredSubtitleLanguage: String? = null + private set; private var _toResume = false; @@ -330,6 +334,15 @@ abstract class FutoVideoPlayerBase : RelativeLayout { _targetTrackAudioBitrate = bitrate; updateTrackSelector(); } + fun setPreferredAudioLanguage(lang: String?) { + preferredAudioLanguage = lang; + updateTrackSelector(); + } + fun setPreferredSubtitleLanguage(lang: String?) { + preferredSubtitleLanguage = lang; + updateTrackSelector(); + } + @OptIn(UnstableApi::class) private fun updateTrackSelector() { var builder = DefaultTrackSelector.Parameters.Builder(context); @@ -343,6 +356,13 @@ abstract class FutoVideoPlayerBase : RelativeLayout { builder = builder.setMaxAudioBitrate(_targetTrackAudioBitrate); } + if (preferredAudioLanguage != null) { + builder = builder.setPreferredAudioLanguage(preferredAudioLanguage); + } + if (preferredSubtitleLanguage != null) { + builder = builder.setPreferredTextLanguage(preferredSubtitleLanguage); + } + builder = if (isAudioMode) { builder.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true) } else { @@ -637,8 +657,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout { val scope = this; var startId = -1; try { - val plugin = videoSource.getUnderlyingPlugin() ?: return@launch; - startId = plugin.getUnderlyingPlugin()?.runtimeId ?: -1; + val plugin = (videoSource as? JSSource)?.getUnderlyingPlugin() ?: return@launch; + startId = plugin.getUnderlyingPlugin().runtimeId ?: -1; val generatedDef = plugin.busy { videoSource.generateAsync(scope); }; withContext(Dispatchers.Main) { if (generatedDef.estDuration >= 0) { @@ -664,16 +684,17 @@ abstract class FutoVideoPlayerBase : RelativeLayout { if(dataSource is JSHttpDataSource.Factory && videoSource is JSDashManifestMergingRawSource) dataSource.setRequestExecutor2(withContext(Dispatchers.IO){videoSource.audio.getRequestExecutor()}); +val url = if (videoSource is JSDashManifestRawSource) videoSource.url else (videoSource as? JSDashManifestRawAudioSource)?.url; _lastVideoMediaSource = DashMediaSource.Factory(dataSource) .createMediaSource( DashManifestParser().parse( - Uri.parse(videoSource.url), + Uri.parse(url ?: ""), ByteArrayInputStream( generated?.toByteArray() ?: ByteArray(0) ) ) ); - if(lastVideoSource == videoSource || (videoSource is JSDashManifestMergingRawSource && videoSource.video == lastVideoSource)); + if(lastVideoSource == videoSource || (videoSource is JSDashManifestMergingRawSource && videoSource.video == lastVideoSource)) loadSelectedSources(play, resume); } } @@ -707,8 +728,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout { if(dataSource is JSHttpDataSource.Factory && videoSource is JSDashManifestMergingRawSource) dataSource.setRequestExecutor2(videoSource.audio.getRequestExecutor()); + val url = if (videoSource is JSDashManifestRawSource) videoSource.url else (videoSource as? JSDashManifestRawAudioSource)?.url; _lastVideoMediaSource = DashMediaSource.Factory(dataSource) - .createMediaSource(DashManifestParser().parse(Uri.parse(videoSource.url), + .createMediaSource(DashManifestParser().parse(Uri.parse(url ?: ""), ByteArrayInputStream(videoSource.manifest?.toByteArray() ?: ByteArray(0)))); return true; } @@ -801,8 +823,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout { val scope = this; var startId = -1; try { - val plugin = audioSource.getUnderlyingPlugin() ?: return@launch; - startId = audioSource.getUnderlyingPlugin()?.getUnderlyingPlugin()?.runtimeId ?: -1; + val plugin = (audioSource as? JSSource)?.getUnderlyingPlugin() ?: return@launch; + startId = plugin.getUnderlyingPlugin()?.runtimeId ?: -1; val generatedDef = plugin.busy { audioSource.generateAsync(scope); } withContext(Dispatchers.Main) { if (generatedDef.estDuration >= 0) { @@ -823,9 +845,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout { audioSource.getHttpDataSourceFactory() else DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); + val url = if (audioSource is JSDashManifestRawSource) audioSource.url else (audioSource as? JSDashManifestRawAudioSource)?.url; withContext(Dispatchers.Main) { - _lastVideoMediaSource = DashMediaSource.Factory(dataSource) - .createMediaSource(DashManifestParser().parse(Uri.parse(audioSource.url), + _lastAudioMediaSource = DashMediaSource.Factory(dataSource) + .createMediaSource(DashManifestParser().parse(Uri.parse(url ?: ""), ByteArrayInputStream(generated?.toByteArray() ?: ByteArray(0)))); loadSelectedSources(play, resume); } @@ -833,12 +856,12 @@ abstract class FutoVideoPlayerBase : RelativeLayout { } catch(reloadRequired: ScriptReloadRequiredException) { Logger.i(TAG, "Reload required detected"); - val plugin = audioSource.getUnderlyingPlugin(); + val plugin = (audioSource as? JSSource)?.getUnderlyingPlugin(); if(plugin == null) return@launch; if(startId != -1 && plugin.getUnderlyingPlugin()?.runtimeId != startId) return@launch; - StatePlatform.instance.reEnableClient(plugin.id, { + StatePlatform.instance.reEnableClient(plugin.config.id, { onReloadRequired.emit(); }); } @@ -857,10 +880,11 @@ abstract class FutoVideoPlayerBase : RelativeLayout { audioSource.getHttpDataSourceFactory() else DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); - _lastVideoMediaSource = DashMediaSource.Factory(dataSource) + val url = if (audioSource is JSDashManifestRawSource) audioSource.url else (audioSource as? JSDashManifestRawAudioSource)?.url; + _lastAudioMediaSource = DashMediaSource.Factory(dataSource) .createMediaSource( DashManifestParser().parse( - Uri.parse(audioSource.url), + Uri.parse(url ?: ""), ByteArrayInputStream(audioSource.manifest?.toByteArray() ?: ByteArray(0)) ) ); From 9a6a78c7f4272d94a68f42f8d374f83f0c88b998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Tue, 31 Mar 2026 20:08:48 +0200 Subject: [PATCH 02/15] feat: improve track selection UI and handle multi-language audio filtering --- .../sources/JSDashManifestRawAudioSource.kt | 2 +- .../models/sources/JSDashManifestRawSource.kt | 3 +- .../fragment/mainactivity/main/ShortView.kt | 69 ++++++++++++++++++- .../mainactivity/main/VideoDetailView.kt | 26 +++++-- .../views/video/FutoVideoPlayerBase.kt | 12 ++-- 5 files changed, 95 insertions(+), 17 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 04632a069..477c60b3b 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 @@ -35,7 +35,7 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS override val language: String; - val url: String; + override 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/JSDashManifestRawSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt index 463a43f9b..04e28575c 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 @@ -26,6 +26,7 @@ import kotlinx.coroutines.Deferred import kotlinx.coroutines.async interface IJSDashManifestRawSource { + val url: String? val hasGenerate: Boolean; var manifest: String?; fun generateAsync(scope: CoroutineScope): Deferred; @@ -67,7 +68,7 @@ open class JSDashManifestRawSource( override val priority: Boolean = _obj.getOrDefault(cfg, "priority", ctx, false) ?: false - val url: String? = + override val url: String? = _obj.getOrDefault(cfg, "url", ctx, null) override var manifest: String? = diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt index 2e0566564..87314cb60 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt @@ -471,6 +471,55 @@ class ShortView : FrameLayout { .toList() } else audioSources?.toList() ?: listOf() + val allLanguages = (videoSources?.map { it.language } ?: listOf()) + .plus(audioSources?.map { it.language } ?: listOf()) + .distinct(); + + var videoSourceItems = mutableListOf(); + var audioSourceItems = mutableListOf(); + var selectedLanguage: String? = null; + val languageFilters = if(allLanguages.filter { it != null }.count() > 1) + SlideUpMenuButtonList(this.context, null, "language_filter", true).apply { + var languageFilterLabels = allLanguages.filterNotNull().toList(); + val english = languageFilterLabels.find { it?.lowercase() == "en" }; + val originalLanguage = videoSources?.find { it.original == true }?.language ?: audioSources?.find { it.original == true }?.language; + val primaryLanguage = Settings.instance.playback.getPrimaryLanguage(); + val hasPrimaryLanguage = allLanguages.any { it == primaryLanguage }; + + if(english != null) + languageFilterLabels = listOf(english).plus(languageFilterLabels.filter { it != english }).toList(); + if(primaryLanguage != null && languageFilterLabels.contains(primaryLanguage)) + languageFilterLabels = listOf(primaryLanguage).plus(languageFilterLabels.filter { it != primaryLanguage }).toList(); + if(originalLanguage != null) + languageFilterLabels = listOf(originalLanguage).plus(languageFilterLabels.filter { it != originalLanguage }).toList(); + + selectedLanguage = originalLanguage ?: (if(hasPrimaryLanguage) primaryLanguage else null); + setButtons(languageFilterLabels, selectedLanguage); + onClick.subscribe { selected -> + setSelected(selected); + + videoSourceItems.forEach { + val item = it.itemTag; + if(item is IVideoSource) { + if(item.language == selected) + it.visibility = View.VISIBLE; + else + it.visibility = View.GONE; + } + } + audioSourceItems.forEach { + val item = it.itemTag; + if(item is IAudioSource) { + if(item.language == selected) + it.visibility = View.VISIBLE; + else + it.visibility = View.GONE; + } + } + } + } + else null; + val canSetSpeed = true val currentPlaybackRate = player.getPlaybackRate() overlayQualitySelector = @@ -514,18 +563,32 @@ class ShortView : FrameLayout { SlideUpMenuItem(this.context, R.drawable.ic_music, "${it.label ?: it.containerMimeType} ${it.bitrate}", "", tag = it, call = { player.selectAudioTrack(it.bitrate) }) }.toList().toTypedArray() ) - else null, if (bestVideoSources.isNotEmpty()) SlideUpMenuGroup( + else null, + languageFilters, + if (bestVideoSources.isNotEmpty()) SlideUpMenuGroup( this.context, context.getString(R.string.video), "video", *bestVideoSources.map { val estSize = VideoHelper.estimateSourceSize(it) val prefix = if (estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "" - SlideUpMenuItem(this.context, R.drawable.ic_movie, it.name, if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "", (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectVideoTrack(it) }) + SlideUpMenuItem(this.context, R.drawable.ic_movie, it.name, if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "", (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectVideoTrack(it) }).apply { + videoSourceItems.add(this) + if (selectedLanguage != null) { + if (it.language != selectedLanguage) + this.visibility = View.GONE + } + } }.toList().toTypedArray() ) else null, if (bestAudioSources.isNotEmpty()) SlideUpMenuGroup( this.context, context.getString(R.string.audio), "audio", *bestAudioSources.map { val estSize = VideoHelper.estimateSourceSize(it) val prefix = if (estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "" - SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectAudioTrack(it) }) + SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectAudioTrack(it) }).apply { + audioSourceItems.add(this) + if (selectedLanguage != null) { + if (it.language != selectedLanguage) + this.visibility = View.GONE + } + } }.toList().toTypedArray() ) else null, if (video?.subtitles?.isNotEmpty() == true) SlideUpMenuGroup( 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 780fa3610..b32ad7668 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 @@ -2474,7 +2474,9 @@ class VideoDetailView : ConstraintLayout { val doDedup = Settings.instance.playback.simplifySources; - val allLanguages = videoSources?.map { it.language }?.distinct() ?: listOf(); + val allLanguages = (videoSources?.map { it.language } ?: listOf()) + .plus(audioSources?.map { it.language } ?: listOf()) + .distinct(); val langResCombinations = if(videoSources != null) allLanguages.flatMap { lang -> videoSources .filter { v -> v.language == lang } @@ -2486,14 +2488,15 @@ class VideoDetailView : ConstraintLayout { Log.i(TAG, "Language count: ${allLanguages}"); var videoSourceItems = mutableListOf(); + var audioSourceItems = mutableListOf(); var selectedLanguage: String? = null; val languageFilters = if(allLanguages.filter { it != null }.count() > 1) SlideUpMenuButtonList(this.context, null, "language_filter", true).apply { var languageFilterLabels = allLanguages.filterNotNull().toList(); val english = languageFilterLabels.find { it?.lowercase() == "en" }; - val originalLanguage = videoSources?.find { it.original == true }?.language; + val originalLanguage = videoSources?.find { it.original == true }?.language ?: audioSources?.find { it.original == true }?.language; val primaryLanguage = Settings.instance.playback.getPrimaryLanguage(); - val hasPrimaryLanguage = videoSources?.any { it.language == primaryLanguage } ?: false; + val hasPrimaryLanguage = allLanguages.any { it == primaryLanguage }; if(english != null) languageFilterLabels = listOf(english).plus(languageFilterLabels.filter { it != english }).toList(); @@ -2516,6 +2519,15 @@ class VideoDetailView : ConstraintLayout { it.visibility = View.GONE; } } + audioSourceItems.forEach { + val item = it.itemTag; + if(item is IAudioSource) { + if(item.language == selected) + it.visibility = View.VISIBLE; + else + it.visibility = View.GONE; + } + } } } else null; @@ -2664,7 +2676,13 @@ class VideoDetailView : ConstraintLayout { it.bitrate.toHumanBitrate(), (prefix + it.codec.trim()).trim(), tag = it, - call = { handleSelectAudioTrack(it) }); + call = { handleSelectAudioTrack(it) }).apply { + audioSourceItems.add(this); + if(selectedLanguage != null) { + if(it.language != selectedLanguage) + this.visibility = View.GONE; + } + } }.toList().toTypedArray()) else null, if(video?.subtitles?.isNotEmpty() == true) 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 0afa06bac..1f41b7f06 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 @@ -684,11 +684,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout { if(dataSource is JSHttpDataSource.Factory && videoSource is JSDashManifestMergingRawSource) dataSource.setRequestExecutor2(withContext(Dispatchers.IO){videoSource.audio.getRequestExecutor()}); -val url = if (videoSource is JSDashManifestRawSource) videoSource.url else (videoSource as? JSDashManifestRawAudioSource)?.url; _lastVideoMediaSource = DashMediaSource.Factory(dataSource) .createMediaSource( DashManifestParser().parse( - Uri.parse(url ?: ""), + Uri.parse(videoSource.url ?: ""), ByteArrayInputStream( generated?.toByteArray() ?: ByteArray(0) ) @@ -728,9 +727,8 @@ val url = if (videoSource is JSDashManifestRawSource) videoSource.url else (vide if(dataSource is JSHttpDataSource.Factory && videoSource is JSDashManifestMergingRawSource) dataSource.setRequestExecutor2(videoSource.audio.getRequestExecutor()); - val url = if (videoSource is JSDashManifestRawSource) videoSource.url else (videoSource as? JSDashManifestRawAudioSource)?.url; _lastVideoMediaSource = DashMediaSource.Factory(dataSource) - .createMediaSource(DashManifestParser().parse(Uri.parse(url ?: ""), + .createMediaSource(DashManifestParser().parse(Uri.parse(videoSource.url ?: ""), ByteArrayInputStream(videoSource.manifest?.toByteArray() ?: ByteArray(0)))); return true; } @@ -845,10 +843,9 @@ val url = if (videoSource is JSDashManifestRawSource) videoSource.url else (vide audioSource.getHttpDataSourceFactory() else DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); - val url = if (audioSource is JSDashManifestRawSource) audioSource.url else (audioSource as? JSDashManifestRawAudioSource)?.url; withContext(Dispatchers.Main) { _lastAudioMediaSource = DashMediaSource.Factory(dataSource) - .createMediaSource(DashManifestParser().parse(Uri.parse(url ?: ""), + .createMediaSource(DashManifestParser().parse(Uri.parse(audioSource.url ?: ""), ByteArrayInputStream(generated?.toByteArray() ?: ByteArray(0)))); loadSelectedSources(play, resume); } @@ -880,11 +877,10 @@ val url = if (videoSource is JSDashManifestRawSource) videoSource.url else (vide audioSource.getHttpDataSourceFactory() else DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); - val url = if (audioSource is JSDashManifestRawSource) audioSource.url else (audioSource as? JSDashManifestRawAudioSource)?.url; _lastAudioMediaSource = DashMediaSource.Factory(dataSource) .createMediaSource( DashManifestParser().parse( - Uri.parse(url ?: ""), + Uri.parse(audioSource.url ?: ""), ByteArrayInputStream(audioSource.manifest?.toByteArray() ?: ByteArray(0)) ) ); From 4d7d8eb79d0ba74cbaa6bce23247c270dc7f3bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Tue, 31 Mar 2026 20:45:21 +0200 Subject: [PATCH 03/15] feat: finalize track selection visibility logic in ShortView --- .../futo/platformplayer/fragment/mainactivity/main/ShortView.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt index 87314cb60..56c43d72e 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt @@ -7,6 +7,7 @@ import android.graphics.drawable.Animatable import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.LayoutInflater +import android.view.View import android.view.WindowManager import android.view.animation.AccelerateInterpolator import android.view.animation.OvershootInterpolator From 5d71dbcca09a30ca1876d7c39627ea399301b905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Tue, 31 Mar 2026 21:02:51 +0200 Subject: [PATCH 04/15] feat: improve track selection with manifest support and fix language auto-picking --- .../fragment/mainactivity/main/ShortView.kt | 21 +++++++- .../mainactivity/main/VideoDetailView.kt | 17 ++++++- .../views/video/FutoVideoPlayerBase.kt | 51 ++++++++++++++++--- 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt index 56c43d72e..e309e0525 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt @@ -580,7 +580,7 @@ class ShortView : FrameLayout { }.toList().toTypedArray() ) else null, if (bestAudioSources.isNotEmpty()) SlideUpMenuGroup( - this.context, context.getString(R.string.audio), "audio", *bestAudioSources.map { + this.context, context.getString(R.string.audio), "audio", *(bestAudioSources.map { val estSize = VideoHelper.estimateSourceSize(it) val prefix = if (estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "" SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectAudioTrack(it) }).apply { @@ -590,7 +590,20 @@ class ShortView : FrameLayout { this.visibility = View.GONE } } - }.toList().toTypedArray() + }.toList() + ( + player.exoPlayer?.player?.currentTracks?.groups?.filter { it.mediaTrackGroup.type == C.TRACK_TYPE_AUDIO }?.flatMap { group -> + (0 until group.mediaTrackGroup.length).map { i -> + val format = group.mediaTrackGroup.getFormat(i); + SlideUpMenuItem(this.context, R.drawable.ic_music, format.label ?: format.id ?: "Track $i", format.bitrate.toHumanBitrate(), format.language ?: "", tag = format, call = { player.selectAudioTrack(format) }).apply { + audioSourceItems.add(this); + if (selectedLanguage != null) { + if (format.language != selectedLanguage) + this.visibility = View.GONE; + } + } + } + } ?: listOf() + )).toTypedArray() ) else null, if (video?.subtitles?.isNotEmpty() == true) SlideUpMenuGroup( this.context, context.getString(R.string.subtitles), "subtitles", *video.subtitles.map { @@ -935,6 +948,10 @@ class ShortView : FrameLayout { ?: player.getPreferredAudioSource(videoDetails, Settings.instance.playback.getPrimaryLanguage(context)) val subtitleSource = _lastSubtitleSource ?: (if (videoDetails is VideoLocal) videoDetails.subtitlesSources.firstOrNull() else null) + + player.setPreferredAudioLanguage(Settings.instance.playback.getPrimaryLanguage(context)); + player.setPreferredSubtitleLanguage(null); // Shorts usually don't have sticky subtitles in the same way + Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)") if (videoSource == null && audioSource == null) { 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 b32ad7668..f750713d4 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 @@ -2666,7 +2666,7 @@ class VideoDetailView : ConstraintLayout { else null, if(bestAudioSources.isNotEmpty()) SlideUpMenuGroup(this.context, context.getString(R.string.audio), "audio", - *bestAudioSources + *(bestAudioSources .map { val estSize = VideoHelper.estimateSourceSize(it); val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""; @@ -2683,7 +2683,20 @@ class VideoDetailView : ConstraintLayout { this.visibility = View.GONE; } } - }.toList().toTypedArray()) + }.toList() + ( + _player.exoPlayer?.player?.currentTracks?.groups?.filter { it.mediaTrackGroup.type == C.TRACK_TYPE_AUDIO }?.flatMap { group -> + (0 until group.mediaTrackGroup.length).map { i -> + val format = group.mediaTrackGroup.getFormat(i); + SlideUpMenuItem(this.context, R.drawable.ic_music, format.label ?: format.id ?: "Track $i", format.bitrate.toHumanBitrate(), format.language ?: "", tag = format, call = { _player.selectAudioTrack(format) }).apply { + audioSourceItems.add(this); + if (selectedLanguage != null) { + if (format.language != selectedLanguage) + this.visibility = View.GONE; + } + } + } + } ?: listOf() + )).toTypedArray()) else null, if(video?.subtitles?.isNotEmpty() == true) SlideUpMenuGroup(this.context, context.getString(R.string.subtitles), "subtitles", 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 1f41b7f06..10c3114d5 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 @@ -17,6 +17,7 @@ import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player +import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.VideoSize import androidx.media3.common.text.CueGroup import androidx.media3.common.util.UnstableApi @@ -147,6 +148,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout { var targetTrackVideoHeight = -1 private set private var _targetTrackAudioBitrate = -1 + private var _targetTrackVideoFormat: Format? = null; + private var _targetTrackAudioFormat: Format? = null; var preferredAudioLanguage: String? = null private set; var preferredSubtitleLanguage: String? = null @@ -328,10 +331,22 @@ abstract class FutoVideoPlayerBase : RelativeLayout { //TODO: Temporary solution, Implement custom track selector without using constraints fun selectVideoTrack(height: Int) { targetTrackVideoHeight = height; + _targetTrackVideoFormat = null; + updateTrackSelector(); + } + fun selectVideoTrack(format: Format?) { + _targetTrackVideoFormat = format; + targetTrackVideoHeight = format?.height ?: -1; updateTrackSelector(); } fun selectAudioTrack(bitrate: Int) { _targetTrackAudioBitrate = bitrate; + _targetTrackAudioFormat = null; + updateTrackSelector(); + } + fun selectAudioTrack(format: Format?) { + _targetTrackAudioFormat = format; + _targetTrackAudioBitrate = format?.bitrate ?: -1; updateTrackSelector(); } fun setPreferredAudioLanguage(lang: String?) { @@ -345,15 +360,43 @@ abstract class FutoVideoPlayerBase : RelativeLayout { @OptIn(UnstableApi::class) private fun updateTrackSelector() { - var builder = DefaultTrackSelector.Parameters.Builder(context); + var builder = (exoPlayer?.player?.trackSelectionParameters as? DefaultTrackSelector.Parameters)?.buildUpon() + ?: DefaultTrackSelector.Parameters.Builder(context); + + builder = builder.clearOverrides(); + + if (_targetTrackVideoFormat != null || _targetTrackAudioFormat != null) { + val tracks = exoPlayer?.player?.currentTracks; + if (tracks != null) { + if (_targetTrackVideoFormat != null) { + val group = tracks.groups.find { it.mediaTrackGroup.type == C.TRACK_TYPE_VIDEO && (0 until it.mediaTrackGroup.length).any { i -> it.mediaTrackGroup.getFormat(i) == _targetTrackVideoFormat } }; + if (group != null) { + builder = builder.addOverride(TrackSelectionOverride(group.mediaTrackGroup, (0 until group.mediaTrackGroup.length).find { i -> group.mediaTrackGroup.getFormat(i) == _targetTrackVideoFormat }!!)); + } + } + if (_targetTrackAudioFormat != null) { + val group = tracks.groups.find { it.mediaTrackGroup.type == C.TRACK_TYPE_AUDIO && (0 until it.mediaTrackGroup.length).any { i -> it.mediaTrackGroup.getFormat(i) == _targetTrackAudioFormat } }; + if (group != null) { + builder = builder.addOverride(TrackSelectionOverride(group.mediaTrackGroup, (0 until group.mediaTrackGroup.length).find { i -> group.mediaTrackGroup.getFormat(i) == _targetTrackAudioFormat }!!)); + } + } + } + } + if(targetTrackVideoHeight > 0) { builder = builder .setMinVideoSize(0, targetTrackVideoHeight - 10) .setMaxVideoSize(9999, targetTrackVideoHeight + 10); + } else { + builder = builder + .setMinVideoSize(0, 0) + .setMaxVideoSize(9999, 9999); } if(_targetTrackAudioBitrate > 0) { builder = builder.setMaxAudioBitrate(_targetTrackAudioBitrate); + } else { + builder = builder.setMaxAudioBitrate(Int.MAX_VALUE); } if (preferredAudioLanguage != null) { @@ -363,11 +406,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { builder = builder.setPreferredTextLanguage(preferredSubtitleLanguage); } - builder = if (isAudioMode) { - builder.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true) - } else { - builder.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, false) - } + builder = builder.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, isAudioMode); val trackSelector = exoPlayer?.player?.trackSelector; if(trackSelector != null) { From 85794eabfce06bbc0bf27ca9403f29ae9c77f7c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Tue, 31 Mar 2026 21:15:21 +0200 Subject: [PATCH 05/15] fix: ensure automatic language selection is correctly applied to the player --- .../com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt | 1 + 1 file changed, 1 insertion(+) 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 10c3114d5..8b137cad8 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 @@ -14,6 +14,7 @@ import androidx.lifecycle.coroutineScope import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.media3.common.C +import androidx.media3.common.Format import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player From c2566d020547ff5c10d714bb8c1f741298c13691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Wed, 1 Apr 2026 06:01:15 +0200 Subject: [PATCH 06/15] feat: fix automatic track selection and improve manifest merging for multi-language UMP --- .../models/sources/JSDashManifestRawSource.kt | 86 ++++++++++++++++--- .../fragment/mainactivity/main/ShortView.kt | 3 +- .../mainactivity/main/VideoDetailView.kt | 8 +- .../platformplayer/helpers/VideoHelper.kt | 4 +- .../futo/platformplayer/states/StatePlayer.kt | 5 +- .../views/video/FutoVideoPlayerBase.kt | 27 +++--- 6 files changed, 103 insertions(+), 30 deletions(-) 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 04e28575c..3d4abd043 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,6 +81,10 @@ open class JSDashManifestRawSource( override var streamMetaData: StreamMetaData? = null + companion object { + val adaptationSetRegex = Regex("", RegexOption.DOT_MATCHES_ALL); + } + private var _pregenerate: V8Deferred? = null fun pregenerateAsync(scope: CoroutineScope): V8Deferred? { _pregenerate = generateAsync(scope); @@ -127,7 +131,19 @@ open class JSDashManifestRawSource( } return@busy result.convert { - it.value + var manifest = it.value + if (manifest != null && language != null) { + val sets = adaptationSetRegex.findAll(manifest); + var changed = false; + for (set in sets) { + if ((set.value.contains("contentType=\"audio\"") || set.value.contains("mimeType=\"audio/")) && !set.value.contains("lang=")) { + val newSet = set.value.replaceFirst("(_config, "initStart", "JSDashManifestRawSource", null) ?: 0; val initEnd = _obj.getOrDefault(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0; @@ -207,12 +235,28 @@ class JSDashManifestMergingRawSource( //TODO: Temporary simple solution..make more reliable version var result: String? = null; - val audioAdaptationSet = adaptationSetRegex.find(audioDash!!); - if (audioAdaptationSet != null) { - result = videoDash.replace( - "", - "\n" + audioAdaptationSet.value - ) + val audioAdaptationSets = adaptationSetRegex.findAll(audioDash!!); + val audioTracks = audioAdaptationSets + .filter { it.value.contains("contentType=\"audio\"") || it.value.contains("mimeType=\"audio/") || it.value.contains("lang=") } + .map { + var set = it.value; + if (!set.contains("lang=") && audio.language != null) { + set = set.replaceFirst("")) { + videoDash.replaceFirst("", "\n$audioTracks") + } else if (videoDash.contains("")) { + videoDash.replace("", "$audioTracks\n") + } else if (videoDash.contains("")) { + videoDash.replace("", "$audioTracks\n") + } else { + videoDash + audioTracks + } } else result = videoDash; @@ -229,9 +273,28 @@ class JSDashManifestMergingRawSource( //TODO: Temporary simple solution..make more reliable version var result: String? = null; - val audioAdaptationSet = adaptationSetRegex.find(audioDash!!); - if(audioAdaptationSet != null) { - result = videoDash.replace("", "\n" + audioAdaptationSet.value) + val audioAdaptationSets = adaptationSetRegex.findAll(audioDash!!); + val audioTracks = audioAdaptationSets + .filter { it.value.contains("contentType=\"audio\"") || it.value.contains("mimeType=\"audio/") || it.value.contains("lang=") } + .map { + var set = it.value; + if (!set.contains("lang=") && audio.language != null) { + set = set.replaceFirst("")) { + videoDash.replaceFirst("", "\n$audioTracks") + } else if (videoDash.contains("")) { + videoDash.replace("", "$audioTracks\n") + } else if (videoDash.contains("")) { + videoDash.replace("", "$audioTracks\n") + } else { + videoDash + audioTracks + } } else result = videoDash; @@ -240,6 +303,5 @@ class JSDashManifestMergingRawSource( } companion object { - private val adaptationSetRegex = Regex(".*?<\\/AdaptationSet>", RegexOption.DOT_MATCHES_ALL); } -} \ No newline at end of file + } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt index e309e0525..10b2bc67f 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt @@ -942,8 +942,9 @@ class ShortView : FrameLayout { updateQualitySourcesOverlay(videoDetails, null) try { + val primaryLanguage = Settings.instance.playback.getPrimaryLanguage(context); val videoSource = _lastVideoSource - ?: player.getPreferredVideoSource(videoDetails, Settings.instance.playback.getCurrentPreferredQualityPixelCount()) + ?: player.getPreferredVideoSource(videoDetails, Settings.instance.playback.getCurrentPreferredQualityPixelCount(), primaryLanguage) val audioSource = _lastAudioSource ?: player.getPreferredAudioSource(videoDetails, Settings.instance.playback.getPrimaryLanguage(context)) val subtitleSource = _lastSubtitleSource 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 f750713d4..6e54a56ba 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 @@ -2046,11 +2046,13 @@ class VideoDetailView : ConstraintLayout { val video = (videoLocal ?: video) ?: return; try { - val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount()); - val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context)); + val primaryLanguage = Settings.instance.playback.getPrimaryLanguage(context); + + val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount(), primaryLanguage); + val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, primaryLanguage); val subtitleSource = _lastSubtitleSource ?: (if (Settings.instance.playback.stickySubtitles) _player.getPreferredSubtitleSource(video, _subtitleLanguage) else null) ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null); - _player.setPreferredAudioLanguage(Settings.instance.playback.getPrimaryLanguage(context)); + _player.setPreferredAudioLanguage(primaryLanguage); _player.setPreferredSubtitleLanguage(_subtitleLanguage); Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)") diff --git a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt index 596ed77ec..f62d10cd9 100644 --- a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt +++ b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt @@ -89,9 +89,9 @@ class VideoHelper { Language.UNKNOWN; } if(altSources.any { it.language == languageToFilter }) { - altSources.filter { it.language == languageToFilter }.sortedBy { it.bitrate }.toList(); + altSources = altSources.filter { it.language == languageToFilter }.sortedBy { it.bitrate }.toList(); } else { - altSources.sortedBy { it.bitrate } + altSources = altSources.sortedBy { it.bitrate }.toList(); } var bestSource = altSources.firstOrNull(); diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt index 2e4845fb4..f35d57e8a 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt @@ -10,6 +10,7 @@ import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.Renderer +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.text.TextOutput import androidx.media3.exoplayer.text.TextRenderer @@ -679,9 +680,11 @@ class StatePlayer { } @OptIn(UnstableApi::class) - private fun createExoPlayer(context : Context): ExoPlayer { + private fun createExoPlayer(context: Context): ExoPlayer { return ExoPlayer.Builder(context) + .setTrackSelector(DefaultTrackSelector(context)) .setRenderersFactory( + object : DefaultRenderersFactory(context) { override fun buildTextRenderers( context: Context, 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 8b137cad8..52e63c3d4 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 @@ -327,6 +327,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { exoPlayer?.modifyState(exoPlayerStateName, {state -> state.listener = null}); newPlayer?.modifyState(exoPlayerStateName, {state -> state.listener = _playerEventListener}); exoPlayer = newPlayer; + updateTrackSelector(); } //TODO: Temporary solution, Implement custom track selector without using constraints @@ -361,13 +362,13 @@ abstract class FutoVideoPlayerBase : RelativeLayout { @OptIn(UnstableApi::class) private fun updateTrackSelector() { - var builder = (exoPlayer?.player?.trackSelectionParameters as? DefaultTrackSelector.Parameters)?.buildUpon() - ?: DefaultTrackSelector.Parameters.Builder(context); + val player = exoPlayer?.player ?: return; + var builder = player.trackSelectionParameters.buildUpon(); builder = builder.clearOverrides(); if (_targetTrackVideoFormat != null || _targetTrackAudioFormat != null) { - val tracks = exoPlayer?.player?.currentTracks; + val tracks = player.currentTracks; if (tracks != null) { if (_targetTrackVideoFormat != null) { val group = tracks.groups.find { it.mediaTrackGroup.type == C.TRACK_TYPE_VIDEO && (0 until it.mediaTrackGroup.length).any { i -> it.mediaTrackGroup.getFormat(i) == _targetTrackVideoFormat } }; @@ -409,10 +410,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { builder = builder.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, isAudioMode); - val trackSelector = exoPlayer?.player?.trackSelector; - if(trackSelector != null) { - trackSelector.parameters = builder.build(); - } + player.trackSelectionParameters = builder.build(); } fun setChapters(chapters: List?) { @@ -441,8 +439,14 @@ abstract class FutoVideoPlayerBase : RelativeLayout { var videoSourceUsed = videoSource; var audioSourceUsed = audioSource; if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){ - videoSource.getUnderlyingPlugin()?.busy { - videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource); + if (videoSource.url != audioSource.url || videoSource.name != audioSource.name) { + Logger.i(TAG, "Merging different DASH manifests"); + videoSource.getUnderlyingPlugin()?.busy { + videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource); + audioSourceUsed = null; + } + } else { + Logger.i(TAG, "Skipping merge of identical DASH manifests (UMP)"); audioSourceUsed = null; } } @@ -767,6 +771,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { if(dataSource is JSHttpDataSource.Factory && videoSource is JSDashManifestMergingRawSource) dataSource.setRequestExecutor2(videoSource.audio.getRequestExecutor()); + _lastVideoMediaSource = DashMediaSource.Factory(dataSource) .createMediaSource(DashManifestParser().parse(Uri.parse(videoSource.url ?: ""), ByteArrayInputStream(videoSource.manifest?.toByteArray() ?: ByteArray(0)))); @@ -957,7 +962,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { //Prefered source selection - fun getPreferredVideoSource(video: IPlatformVideoDetails, targetPixels: Int = -1): IVideoSource? { + fun getPreferredVideoSource(video: IPlatformVideoDetails, targetPixels: Int = -1, preferredLanguage: String? = null): IVideoSource? { val usePreview = false; if(usePreview) { if(video.preview != null && video.preview is VideoMuxedSourceDescriptor) @@ -971,7 +976,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { else if(video.hls != null) return video.hls; else - return VideoHelper.selectBestVideoSource(video.video, targetPixels, PREFERED_VIDEO_CONTAINERS) + return VideoHelper.selectBestVideoSource(video.video, targetPixels, PREFERED_VIDEO_CONTAINERS, preferredLanguage) } fun getPreferredAudioSource(video: IPlatformVideoDetails, preferredLanguage: String?): IAudioSource? { return VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, preferredLanguage); From 360b6f91bc27ab6c9bf3fc8f466afe4507982b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Wed, 1 Apr 2026 06:41:10 +0200 Subject: [PATCH 07/15] chore: removed lints --- .../models/sources/JSDashManifestRawSource.kt | 56 +++++++++---------- 1 file changed, 26 insertions(+), 30 deletions(-) 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 3d4abd043..62e34f622 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 @@ -133,13 +133,11 @@ open class JSDashManifestRawSource( return@busy result.convert { var manifest = it.value if (manifest != null && language != null) { - val sets = adaptationSetRegex.findAll(manifest); - var changed = false; + val sets = adaptationSetRegex.findAll(manifest) for (set in sets) { if ((set.value.contains("contentType=\"audio\"") || set.value.contains("mimeType=\"audio/")) && !set.value.contains("lang=")) { - val newSet = set.value.replaceFirst("("generate").value; } - }); + }) } } else @@ -168,28 +166,26 @@ open class JSDashManifestRawSource( _plugin.isBusyWith("dashVideo.generate") { _obj.invokeV8("generate").value; } - }); + }) if(result != null){ if (language != null) { - val sets = adaptationSetRegex.findAll(result); - var changed = false; + val sets = adaptationSetRegex.findAll(result) for (set in sets) { if ((set.value.contains("contentType=\"audio\"") || set.value.contains("mimeType=\"audio/")) && !set.value.contains("lang=")) { val newSet = set.value.replaceFirst("(_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; + 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); + streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd) } } } @@ -234,18 +230,18 @@ class JSDashManifestMergingRawSource( //TODO: Temporary simple solution..make more reliable version - var result: String? = null; - val audioAdaptationSets = adaptationSetRegex.findAll(audioDash!!); + var result: String? + val audioAdaptationSets = adaptationSetRegex.findAll(audioDash!!) val audioTracks = audioAdaptationSets .filter { it.value.contains("contentType=\"audio\"") || it.value.contains("mimeType=\"audio/") || it.value.contains("lang=") } .map { - var set = it.value; + var set = it.value if (!set.contains("lang=") && audio.language != null) { - set = set.replaceFirst("")) { @@ -258,9 +254,9 @@ class JSDashManifestMergingRawSource( videoDash + audioTracks } } else - result = videoDash; + result = videoDash - return@merge result; + return@merge result }; } override fun generate(): String? { @@ -273,19 +269,19 @@ class JSDashManifestMergingRawSource( //TODO: Temporary simple solution..make more reliable version var result: String? = null; - val audioAdaptationSets = adaptationSetRegex.findAll(audioDash!!); + val audioAdaptationSets = adaptationSetRegex.findAll(audioDash!!) val audioTracks = audioAdaptationSets .filter { it.value.contains("contentType=\"audio\"") || it.value.contains("mimeType=\"audio/") || it.value.contains("lang=") } .map { var set = it.value; if (!set.contains("lang=") && audio.language != null) { - set = set.replaceFirst("")) { videoDash.replaceFirst("", "\n$audioTracks") } else if (videoDash.contains("")) { @@ -297,9 +293,9 @@ class JSDashManifestMergingRawSource( } } else - result = videoDash; + result = videoDash - return result; + return result } companion object { From 2c8f54cdb0928b14e13e0724c900e7e9dbf4a91a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Wed, 1 Apr 2026 06:53:24 +0200 Subject: [PATCH 08/15] chore: removed lints --- .../views/video/FutoVideoPlayerBase.kt | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index 52e63c3d4..dc38f69e2 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 @@ -100,6 +100,7 @@ import java.io.ByteArrayInputStream import java.io.File import java.util.concurrent.atomic.AtomicInteger import kotlin.math.abs +import androidx.core.net.toUri abstract class FutoVideoPlayerBase : RelativeLayout { private val TAG = "FutoVideoPlayerBase" @@ -149,8 +150,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout { var targetTrackVideoHeight = -1 private set private var _targetTrackAudioBitrate = -1 - private var _targetTrackVideoFormat: Format? = null; - private var _targetTrackAudioFormat: Format? = null; + private var _targetTrackVideoFormat: Format? = null + private var _targetTrackAudioFormat: Format? = null var preferredAudioLanguage: String? = null private set; var preferredSubtitleLanguage: String? = null @@ -327,59 +328,61 @@ abstract class FutoVideoPlayerBase : RelativeLayout { exoPlayer?.modifyState(exoPlayerStateName, {state -> state.listener = null}); newPlayer?.modifyState(exoPlayerStateName, {state -> state.listener = _playerEventListener}); exoPlayer = newPlayer; - updateTrackSelector(); + updateTrackSelector() } //TODO: Temporary solution, Implement custom track selector without using constraints fun selectVideoTrack(height: Int) { targetTrackVideoHeight = height; _targetTrackVideoFormat = null; - updateTrackSelector(); + updateTrackSelector() } fun selectVideoTrack(format: Format?) { _targetTrackVideoFormat = format; targetTrackVideoHeight = format?.height ?: -1; - updateTrackSelector(); + updateTrackSelector() } fun selectAudioTrack(bitrate: Int) { _targetTrackAudioBitrate = bitrate; - _targetTrackAudioFormat = null; - updateTrackSelector(); + _targetTrackAudioFormat = null + updateTrackSelector() } + + @OptIn(UnstableApi::class) fun selectAudioTrack(format: Format?) { _targetTrackAudioFormat = format; _targetTrackAudioBitrate = format?.bitrate ?: -1; - updateTrackSelector(); + updateTrackSelector() } fun setPreferredAudioLanguage(lang: String?) { preferredAudioLanguage = lang; - updateTrackSelector(); + updateTrackSelector() } fun setPreferredSubtitleLanguage(lang: String?) { preferredSubtitleLanguage = lang; - updateTrackSelector(); + updateTrackSelector() } @OptIn(UnstableApi::class) private fun updateTrackSelector() { - val player = exoPlayer?.player ?: return; - var builder = player.trackSelectionParameters.buildUpon(); + val player = exoPlayer?.player ?: return + var builder = player.trackSelectionParameters.buildUpon() - builder = builder.clearOverrides(); + builder = builder.clearOverrides() if (_targetTrackVideoFormat != null || _targetTrackAudioFormat != null) { - val tracks = player.currentTracks; + val tracks = player.currentTracks if (tracks != null) { if (_targetTrackVideoFormat != null) { - val group = tracks.groups.find { it.mediaTrackGroup.type == C.TRACK_TYPE_VIDEO && (0 until it.mediaTrackGroup.length).any { i -> it.mediaTrackGroup.getFormat(i) == _targetTrackVideoFormat } }; + val group = tracks.groups.find { it.mediaTrackGroup.type == C.TRACK_TYPE_VIDEO && (0 until it.mediaTrackGroup.length).any { i -> it.mediaTrackGroup.getFormat(i) == _targetTrackVideoFormat } } if (group != null) { - builder = builder.addOverride(TrackSelectionOverride(group.mediaTrackGroup, (0 until group.mediaTrackGroup.length).find { i -> group.mediaTrackGroup.getFormat(i) == _targetTrackVideoFormat }!!)); + builder = builder.addOverride(TrackSelectionOverride(group.mediaTrackGroup, (0 until group.mediaTrackGroup.length).find { i -> group.mediaTrackGroup.getFormat(i) == _targetTrackVideoFormat }!!)) } } if (_targetTrackAudioFormat != null) { - val group = tracks.groups.find { it.mediaTrackGroup.type == C.TRACK_TYPE_AUDIO && (0 until it.mediaTrackGroup.length).any { i -> it.mediaTrackGroup.getFormat(i) == _targetTrackAudioFormat } }; + val group = tracks.groups.find { it.mediaTrackGroup.type == C.TRACK_TYPE_AUDIO && (0 until it.mediaTrackGroup.length).any { i -> it.mediaTrackGroup.getFormat(i) == _targetTrackAudioFormat } } if (group != null) { - builder = builder.addOverride(TrackSelectionOverride(group.mediaTrackGroup, (0 until group.mediaTrackGroup.length).find { i -> group.mediaTrackGroup.getFormat(i) == _targetTrackAudioFormat }!!)); + builder = builder.addOverride(TrackSelectionOverride(group.mediaTrackGroup, (0 until group.mediaTrackGroup.length).find { i -> group.mediaTrackGroup.getFormat(i) == _targetTrackAudioFormat }!!)) } } } @@ -392,25 +395,25 @@ abstract class FutoVideoPlayerBase : RelativeLayout { } else { builder = builder .setMinVideoSize(0, 0) - .setMaxVideoSize(9999, 9999); + .setMaxVideoSize(9999, 9999) } if(_targetTrackAudioBitrate > 0) { builder = builder.setMaxAudioBitrate(_targetTrackAudioBitrate); } else { - builder = builder.setMaxAudioBitrate(Int.MAX_VALUE); + builder = builder.setMaxAudioBitrate(Int.MAX_VALUE) } if (preferredAudioLanguage != null) { - builder = builder.setPreferredAudioLanguage(preferredAudioLanguage); + builder = builder.setPreferredAudioLanguage(preferredAudioLanguage) } if (preferredSubtitleLanguage != null) { - builder = builder.setPreferredTextLanguage(preferredSubtitleLanguage); + builder = builder.setPreferredTextLanguage(preferredSubtitleLanguage) } - builder = builder.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, isAudioMode); + builder = builder.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, isAudioMode) - player.trackSelectionParameters = builder.build(); + player.trackSelectionParameters = builder.build() } fun setChapters(chapters: List?) { @@ -442,17 +445,17 @@ abstract class FutoVideoPlayerBase : RelativeLayout { if (videoSource.url != audioSource.url || videoSource.name != audioSource.name) { Logger.i(TAG, "Merging different DASH manifests"); videoSource.getUnderlyingPlugin()?.busy { - videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource); - audioSourceUsed = null; + videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource) + audioSourceUsed = null } } else { - Logger.i(TAG, "Skipping merge of identical DASH manifests (UMP)"); - audioSourceUsed = null; + Logger.i(TAG, "Skipping merge of identical DASH manifests (UMP)") + audioSourceUsed = null } } - val didSetVideo = swapSourceInternal(videoSourceUsed, play, resume); - val didSetAudio = swapSourceInternal(audioSourceUsed, play, resume); + val didSetVideo = swapSourceInternal(videoSourceUsed, play, resume) + val didSetAudio = swapSourceInternal(audioSourceUsed, play, resume) if(!keepSubtitles) _lastSubtitleMediaSource = null; @@ -773,7 +776,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout { dataSource.setRequestExecutor2(videoSource.audio.getRequestExecutor()); _lastVideoMediaSource = DashMediaSource.Factory(dataSource) - .createMediaSource(DashManifestParser().parse(Uri.parse(videoSource.url ?: ""), + .createMediaSource(DashManifestParser().parse( + (videoSource.url ?: "").toUri(), ByteArrayInputStream(videoSource.manifest?.toByteArray() ?: ByteArray(0)))); return true; } From cca68db3c604dab7239b372c76f3defd4039bdad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Wed, 1 Apr 2026 06:53:39 +0200 Subject: [PATCH 09/15] chore: removed lints --- .../mainactivity/main/VideoDetailView.kt | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 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 6e54a56ba..44e940cbf 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 @@ -33,7 +33,6 @@ import android.widget.ImageButton import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView -import androidx.compose.ui.text.toLowerCase import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.lifecycleScope import androidx.media3.common.C @@ -43,7 +42,6 @@ import androidx.media3.datasource.HttpDataSource import androidx.media3.ui.PlayerControlView import androidx.media3.ui.TimeBar import com.bumptech.glide.Glide -import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition import com.futo.platformplayer.BuildConfig @@ -57,7 +55,6 @@ import com.futo.platformplayer.api.media.LiveChatManager import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException -import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink import com.futo.platformplayer.api.media.models.chapters.ChapterType import com.futo.platformplayer.api.media.models.chapters.IChapter @@ -183,7 +180,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json import userpackage.Protocol import java.time.OffsetDateTime import java.util.Locale @@ -2046,14 +2042,14 @@ class VideoDetailView : ConstraintLayout { val video = (videoLocal ?: video) ?: return; try { - val primaryLanguage = Settings.instance.playback.getPrimaryLanguage(context); + val primaryLanguage = Settings.instance.playback.getPrimaryLanguage(context) - val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount(), primaryLanguage); - val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, primaryLanguage); - val subtitleSource = _lastSubtitleSource ?: (if (Settings.instance.playback.stickySubtitles) _player.getPreferredSubtitleSource(video, _subtitleLanguage) else null) ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null); + val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount(), primaryLanguage) + val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, primaryLanguage) + val subtitleSource = _lastSubtitleSource ?: (if (Settings.instance.playback.stickySubtitles) _player.getPreferredSubtitleSource(video, _subtitleLanguage) else null) ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null) - _player.setPreferredAudioLanguage(primaryLanguage); - _player.setPreferredSubtitleLanguage(_subtitleLanguage); + _player.setPreferredAudioLanguage(primaryLanguage) + _player.setPreferredSubtitleLanguage(_subtitleLanguage) Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)") @@ -2478,7 +2474,7 @@ class VideoDetailView : ConstraintLayout { val allLanguages = (videoSources?.map { it.language } ?: listOf()) .plus(audioSources?.map { it.language } ?: listOf()) - .distinct(); + .distinct() val langResCombinations = if(videoSources != null) allLanguages.flatMap { lang -> videoSources .filter { v -> v.language == lang } @@ -2496,9 +2492,9 @@ class VideoDetailView : ConstraintLayout { SlideUpMenuButtonList(this.context, null, "language_filter", true).apply { var languageFilterLabels = allLanguages.filterNotNull().toList(); val english = languageFilterLabels.find { it?.lowercase() == "en" }; - val originalLanguage = videoSources?.find { it.original == true }?.language ?: audioSources?.find { it.original == true }?.language; + val originalLanguage = videoSources?.find { it.original == true }?.language ?: audioSources?.find { it.original == true }?.language val primaryLanguage = Settings.instance.playback.getPrimaryLanguage(); - val hasPrimaryLanguage = allLanguages.any { it == primaryLanguage }; + val hasPrimaryLanguage = allLanguages.any { it == primaryLanguage } if(english != null) languageFilterLabels = listOf(english).plus(languageFilterLabels.filter { it != english }).toList(); @@ -2525,9 +2521,9 @@ class VideoDetailView : ConstraintLayout { val item = it.itemTag; if(item is IAudioSource) { if(item.language == selected) - it.visibility = View.VISIBLE; + it.visibility = VISIBLE; else - it.visibility = View.GONE; + it.visibility = GONE; } } } @@ -2542,14 +2538,14 @@ class VideoDetailView : ConstraintLayout { ?.toList() ?: listOf() else videoSources?.toList() ?: listOf() val bestAudioContainer = audioSources?.let { VideoHelper.selectBestAudioSource(it, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS)?.container }; val bestAudioSources = if(doDedup && audioSources != null) { - val audioLangs = audioSources.map { it.language }.distinct(); + val audioLangs = audioSources.map { it.language }.distinct() audioLangs.map { lang -> VideoHelper.selectBestAudioSource(audioSources.filter { it.language == lang }, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS) }.plus(audioSources.filter { it is IHLSManifestAudioSource || it is IDashManifestSource || it is IJSDashManifestRawSource }) .filterNotNull() .distinct() .toList() - } else audioSources?.toList() ?: listOf(); + } else audioSources?.toList() ?: listOf() val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed() == true val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate() @@ -2682,7 +2678,7 @@ class VideoDetailView : ConstraintLayout { audioSourceItems.add(this); if(selectedLanguage != null) { if(it.language != selectedLanguage) - this.visibility = View.GONE; + this.visibility = GONE } } }.toList() + ( @@ -2693,7 +2689,7 @@ class VideoDetailView : ConstraintLayout { audioSourceItems.add(this); if (selectedLanguage != null) { if (format.language != selectedLanguage) - this.visibility = View.GONE; + this.visibility = GONE } } } From 683b55ac73b7ed736c793bacb032adbce7c6644e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Wed, 1 Apr 2026 06:53:54 +0200 Subject: [PATCH 10/15] chore: removed lints --- .../fragment/mainactivity/main/ShortView.kt | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt index 10b2bc67f..07669ee24 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt @@ -474,45 +474,45 @@ class ShortView : FrameLayout { val allLanguages = (videoSources?.map { it.language } ?: listOf()) .plus(audioSources?.map { it.language } ?: listOf()) - .distinct(); + .distinct() - var videoSourceItems = mutableListOf(); - var audioSourceItems = mutableListOf(); + var videoSourceItems = mutableListOf() + var audioSourceItems = mutableListOf() var selectedLanguage: String? = null; val languageFilters = if(allLanguages.filter { it != null }.count() > 1) SlideUpMenuButtonList(this.context, null, "language_filter", true).apply { - var languageFilterLabels = allLanguages.filterNotNull().toList(); - val english = languageFilterLabels.find { it?.lowercase() == "en" }; - val originalLanguage = videoSources?.find { it.original == true }?.language ?: audioSources?.find { it.original == true }?.language; - val primaryLanguage = Settings.instance.playback.getPrimaryLanguage(); - val hasPrimaryLanguage = allLanguages.any { it == primaryLanguage }; + var languageFilterLabels = allLanguages.filterNotNull().toList() + val english = languageFilterLabels.find { it?.lowercase() == "en" } + val originalLanguage = videoSources?.find { it.original == true }?.language ?: audioSources?.find { it.original == true }?.language + val primaryLanguage = Settings.instance.playback.getPrimaryLanguage() + val hasPrimaryLanguage = allLanguages.any { it == primaryLanguage } if(english != null) - languageFilterLabels = listOf(english).plus(languageFilterLabels.filter { it != english }).toList(); + languageFilterLabels = listOf(english).plus(languageFilterLabels.filter { it != english }).toList() if(primaryLanguage != null && languageFilterLabels.contains(primaryLanguage)) - languageFilterLabels = listOf(primaryLanguage).plus(languageFilterLabels.filter { it != primaryLanguage }).toList(); + languageFilterLabels = listOf(primaryLanguage).plus(languageFilterLabels.filter { it != primaryLanguage }).toList() if(originalLanguage != null) - languageFilterLabels = listOf(originalLanguage).plus(languageFilterLabels.filter { it != originalLanguage }).toList(); + languageFilterLabels = listOf(originalLanguage).plus(languageFilterLabels.filter { it != originalLanguage }).toList() - selectedLanguage = originalLanguage ?: (if(hasPrimaryLanguage) primaryLanguage else null); - setButtons(languageFilterLabels, selectedLanguage); + selectedLanguage = originalLanguage ?: (if(hasPrimaryLanguage) primaryLanguage else null) + setButtons(languageFilterLabels, selectedLanguage) onClick.subscribe { selected -> - setSelected(selected); + setSelected(selected) videoSourceItems.forEach { - val item = it.itemTag; + val item = it.itemTag if(item is IVideoSource) { if(item.language == selected) - it.visibility = View.VISIBLE; + it.visibility = VISIBLE; else - it.visibility = View.GONE; + it.visibility = GONE; } } audioSourceItems.forEach { val item = it.itemTag; if(item is IAudioSource) { if(item.language == selected) - it.visibility = View.VISIBLE; + it.visibility = VISIBLE; else it.visibility = View.GONE; } @@ -574,7 +574,7 @@ class ShortView : FrameLayout { videoSourceItems.add(this) if (selectedLanguage != null) { if (it.language != selectedLanguage) - this.visibility = View.GONE + this.visibility = GONE } } }.toList().toTypedArray() @@ -587,18 +587,18 @@ class ShortView : FrameLayout { audioSourceItems.add(this) if (selectedLanguage != null) { if (it.language != selectedLanguage) - this.visibility = View.GONE + this.visibility = GONE } } }.toList() + ( player.exoPlayer?.player?.currentTracks?.groups?.filter { it.mediaTrackGroup.type == C.TRACK_TYPE_AUDIO }?.flatMap { group -> (0 until group.mediaTrackGroup.length).map { i -> - val format = group.mediaTrackGroup.getFormat(i); + val format = group.mediaTrackGroup.getFormat(i) SlideUpMenuItem(this.context, R.drawable.ic_music, format.label ?: format.id ?: "Track $i", format.bitrate.toHumanBitrate(), format.language ?: "", tag = format, call = { player.selectAudioTrack(format) }).apply { audioSourceItems.add(this); if (selectedLanguage != null) { if (format.language != selectedLanguage) - this.visibility = View.GONE; + this.visibility = GONE } } } From 3ff3b8f23c0fe244f98817e3a8c52f3515061f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Wed, 1 Apr 2026 06:54:12 +0200 Subject: [PATCH 11/15] chore: removed lints --- app/src/main/java/com/futo/platformplayer/Extensions_Content.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt index 86a66f15b..65ac5bf5e 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_Content.kt @@ -25,7 +25,7 @@ fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory { } else if (requestModifier != null) { JSHttpDataSource.Factory().setRequestModifier(requestModifier); } else { - DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); + DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT) } } From e3ac8e84dfdf1291d3cd7ba9693b891a62182dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Wed, 1 Apr 2026 06:54:35 +0200 Subject: [PATCH 12/15] chore: removed lints --- app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index 2a1f12194..5927776a3 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -585,7 +585,7 @@ class UISlideOverlays { .distinct() .map { res -> Pair(res, lang) } } else listOf(); - var videoSourceItems = mutableListOf(); + var videoSourceItems = mutableListOf() var selectedLanguage: String? = null; val languageFilters = if(allLanguages.filter { it != null }.count() > 1) SlideUpMenuButtonList(container.context, null, "language_filter", true).apply { From 3ddb50f22a02e4254c70b82deb7ef21b71743b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Wed, 1 Apr 2026 06:55:32 +0200 Subject: [PATCH 13/15] chore: removed lints --- .../js/models/sources/JSDashManifestRawAudioSource.kt | 7 +------ .../js/models/sources/JSDashManifestRawSource.kt | 11 ++--------- .../com/futo/platformplayer/helpers/VideoHelper.kt | 8 ++++---- 3 files changed, 7 insertions(+), 19 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 477c60b3b..20399ac62 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 @@ -4,14 +4,10 @@ import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.V8Deferred import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource -import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource -import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData import com.futo.platformplayer.api.media.platforms.js.DevJSClient 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.getOrDefault import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrThrow @@ -22,7 +18,6 @@ import com.futo.platformplayer.others.Language import com.futo.platformplayer.states.StateDeveloper import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource { override val container : String; @@ -35,7 +30,7 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS override val language: String; - override val url: String; + override 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/JSDashManifestRawSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSDashManifestRawSource.kt index 62e34f622..598a5272f 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 @@ -1,20 +1,14 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources -import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.V8Deferred -import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource -import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData import com.futo.platformplayer.api.media.platforms.js.DevJSClient 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.getOrDefault -import com.futo.platformplayer.getOrNull import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.invokeV8Async @@ -23,7 +17,6 @@ import com.futo.platformplayer.states.StateDeveloper import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async interface IJSDashManifestRawSource { val url: String? @@ -82,7 +75,7 @@ open class JSDashManifestRawSource( override var streamMetaData: StreamMetaData? = null companion object { - val adaptationSetRegex = Regex("", RegexOption.DOT_MATCHES_ALL); + val adaptationSetRegex = Regex("", RegexOption.DOT_MATCHES_ALL) } private var _pregenerate: V8Deferred? = null @@ -173,7 +166,7 @@ open class JSDashManifestRawSource( val sets = adaptationSetRegex.findAll(result) for (set in sets) { if ((set.value.contains("contentType=\"audio\"") || set.value.contains("mimeType=\"audio/")) && !set.value.contains("lang=")) { - val newSet = set.value.replaceFirst(", prefLanguage: String? = null, targetBitrate: Long? = null) : IAudioSource? { if(!desc.isUnMuxed) { - val audioEquivalent = desc.videoSources.map { it.toAudioSource() }; - return selectBestAudioSource(audioEquivalent, prefContainers, prefLanguage, targetBitrate); + val audioEquivalent = desc.videoSources.map { it.toAudioSource() } + return selectBestAudioSource(audioEquivalent, prefContainers, prefLanguage, targetBitrate) } return selectBestAudioSource((desc as VideoUnMuxedSourceDescriptor).audioSources.toList(), prefContainers, prefLanguage, targetBitrate); From f470fe4b2cad9485fc7f882c3f3445035110a877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Wed, 1 Apr 2026 07:02:42 +0200 Subject: [PATCH 14/15] chore: removed lints --- .../models/sources/JSDashManifestRawSource.kt | 2 +- .../fragment/mainactivity/main/ShortView.kt | 16 +++++----- .../mainactivity/main/VideoDetailView.kt | 14 ++++---- .../views/video/FutoVideoPlayerBase.kt | 32 ++++++++----------- 4 files changed, 30 insertions(+), 34 deletions(-) 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 598a5272f..db2b002c2 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 @@ -266,7 +266,7 @@ class JSDashManifestMergingRawSource( val audioTracks = audioAdaptationSets .filter { it.value.contains("contentType=\"audio\"") || it.value.contains("mimeType=\"audio/") || it.value.contains("lang=") } .map { - var set = it.value; + var set = it.value if (!set.contains("lang=") && audio.language != null) { set = set.replaceFirst("() var audioSourceItems = mutableListOf() - var selectedLanguage: String? = null; + var selectedLanguage: String? = null val languageFilters = if(allLanguages.filter { it != null }.count() > 1) SlideUpMenuButtonList(this.context, null, "language_filter", true).apply { var languageFilterLabels = allLanguages.filterNotNull().toList() @@ -503,18 +503,18 @@ class ShortView : FrameLayout { val item = it.itemTag if(item is IVideoSource) { if(item.language == selected) - it.visibility = VISIBLE; + it.visibility = VISIBLE else - it.visibility = GONE; + it.visibility = GONE } } audioSourceItems.forEach { val item = it.itemTag; if(item is IAudioSource) { if(item.language == selected) - it.visibility = VISIBLE; + it.visibility = VISIBLE else - it.visibility = View.GONE; + it.visibility = GONE } } } @@ -595,7 +595,7 @@ class ShortView : FrameLayout { (0 until group.mediaTrackGroup.length).map { i -> val format = group.mediaTrackGroup.getFormat(i) SlideUpMenuItem(this.context, R.drawable.ic_music, format.label ?: format.id ?: "Track $i", format.bitrate.toHumanBitrate(), format.language ?: "", tag = format, call = { player.selectAudioTrack(format) }).apply { - audioSourceItems.add(this); + audioSourceItems.add(this) if (selectedLanguage != null) { if (format.language != selectedLanguage) this.visibility = GONE @@ -942,7 +942,7 @@ class ShortView : FrameLayout { updateQualitySourcesOverlay(videoDetails, null) try { - val primaryLanguage = Settings.instance.playback.getPrimaryLanguage(context); + val primaryLanguage = Settings.instance.playback.getPrimaryLanguage(context) val videoSource = _lastVideoSource ?: player.getPreferredVideoSource(videoDetails, Settings.instance.playback.getCurrentPreferredQualityPixelCount(), primaryLanguage) val audioSource = _lastAudioSource @@ -950,7 +950,7 @@ class ShortView : FrameLayout { val subtitleSource = _lastSubtitleSource ?: (if (videoDetails is VideoLocal) videoDetails.subtitlesSources.firstOrNull() else null) - player.setPreferredAudioLanguage(Settings.instance.playback.getPrimaryLanguage(context)); + player.setPreferredAudioLanguage(Settings.instance.playback.getPrimaryLanguage(context)) player.setPreferredSubtitleLanguage(null); // Shorts usually don't have sticky subtitles in the same way Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)") 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 44e940cbf..3789fd8ac 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 @@ -2486,7 +2486,7 @@ class VideoDetailView : ConstraintLayout { Log.i(TAG, "Language count: ${allLanguages}"); var videoSourceItems = mutableListOf(); - var audioSourceItems = mutableListOf(); + var audioSourceItems = mutableListOf() var selectedLanguage: String? = null; val languageFilters = if(allLanguages.filter { it != null }.count() > 1) SlideUpMenuButtonList(this.context, null, "language_filter", true).apply { @@ -2518,12 +2518,12 @@ class VideoDetailView : ConstraintLayout { } } audioSourceItems.forEach { - val item = it.itemTag; + val item = it.itemTag if(item is IAudioSource) { if(item.language == selected) - it.visibility = VISIBLE; + it.visibility = VISIBLE else - it.visibility = GONE; + it.visibility = GONE } } } @@ -2675,7 +2675,7 @@ class VideoDetailView : ConstraintLayout { (prefix + it.codec.trim()).trim(), tag = it, call = { handleSelectAudioTrack(it) }).apply { - audioSourceItems.add(this); + audioSourceItems.add(this) if(selectedLanguage != null) { if(it.language != selectedLanguage) this.visibility = GONE @@ -2684,9 +2684,9 @@ class VideoDetailView : ConstraintLayout { }.toList() + ( _player.exoPlayer?.player?.currentTracks?.groups?.filter { it.mediaTrackGroup.type == C.TRACK_TYPE_AUDIO }?.flatMap { group -> (0 until group.mediaTrackGroup.length).map { i -> - val format = group.mediaTrackGroup.getFormat(i); + val format = group.mediaTrackGroup.getFormat(i) SlideUpMenuItem(this.context, R.drawable.ic_music, format.label ?: format.id ?: "Track $i", format.bitrate.toHumanBitrate(), format.language ?: "", tag = format, call = { _player.selectAudioTrack(format) }).apply { - audioSourceItems.add(this); + audioSourceItems.add(this) if (selectedLanguage != null) { if (format.language != selectedLanguage) this.visibility = GONE 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 dc38f69e2..c819ae2d6 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 @@ -153,9 +153,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout { private var _targetTrackVideoFormat: Format? = null private var _targetTrackAudioFormat: Format? = null var preferredAudioLanguage: String? = null - private set; + private set var preferredSubtitleLanguage: String? = null - private set; + private set private var _toResume = false; @@ -333,17 +333,13 @@ abstract class FutoVideoPlayerBase : RelativeLayout { //TODO: Temporary solution, Implement custom track selector without using constraints fun selectVideoTrack(height: Int) { - targetTrackVideoHeight = height; - _targetTrackVideoFormat = null; - updateTrackSelector() - } - fun selectVideoTrack(format: Format?) { - _targetTrackVideoFormat = format; - targetTrackVideoHeight = format?.height ?: -1; + targetTrackVideoHeight = height + _targetTrackVideoFormat = null updateTrackSelector() } + fun selectAudioTrack(bitrate: Int) { - _targetTrackAudioBitrate = bitrate; + _targetTrackAudioBitrate = bitrate _targetTrackAudioFormat = null updateTrackSelector() } @@ -351,15 +347,15 @@ abstract class FutoVideoPlayerBase : RelativeLayout { @OptIn(UnstableApi::class) fun selectAudioTrack(format: Format?) { _targetTrackAudioFormat = format; - _targetTrackAudioBitrate = format?.bitrate ?: -1; + _targetTrackAudioBitrate = format?.bitrate ?: -1 updateTrackSelector() } fun setPreferredAudioLanguage(lang: String?) { - preferredAudioLanguage = lang; + preferredAudioLanguage = lang updateTrackSelector() } fun setPreferredSubtitleLanguage(lang: String?) { - preferredSubtitleLanguage = lang; + preferredSubtitleLanguage = lang updateTrackSelector() } @@ -443,7 +439,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { var audioSourceUsed = audioSource; if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){ if (videoSource.url != audioSource.url || videoSource.name != audioSource.name) { - Logger.i(TAG, "Merging different DASH manifests"); + Logger.i(TAG, "Merging different DASH manifests") videoSource.getUnderlyingPlugin()?.busy { videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource) audioSourceUsed = null @@ -704,9 +700,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout { val scope = this; var startId = -1; try { - val plugin = (videoSource as? JSSource)?.getUnderlyingPlugin() ?: return@launch; - startId = plugin.getUnderlyingPlugin().runtimeId ?: -1; - val generatedDef = plugin.busy { videoSource.generateAsync(scope); }; + val plugin = (videoSource as? JSSource)?.getUnderlyingPlugin() ?: return@launch + startId = plugin.getUnderlyingPlugin().runtimeId + val generatedDef = plugin.busy { videoSource.generateAsync(scope); } withContext(Dispatchers.Main) { if (generatedDef.estDuration >= 0) { setLoading(generatedDef.estDuration) @@ -902,7 +898,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { } catch(reloadRequired: ScriptReloadRequiredException) { Logger.i(TAG, "Reload required detected"); - val plugin = (audioSource as? JSSource)?.getUnderlyingPlugin(); + val plugin = (audioSource as? JSSource)?.getUnderlyingPlugin() if(plugin == null) return@launch; if(startId != -1 && plugin.getUnderlyingPlugin()?.runtimeId != startId) From 49cd2f7c6b71a5ebc0d4db3b457154542a73f9ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Wed, 1 Apr 2026 07:20:38 +0200 Subject: [PATCH 15/15] chore: fixing lint issues --- .../models/sources/JSDashManifestRawSource.kt | 51 +++++++++++-------- .../fragment/mainactivity/main/ShortView.kt | 17 ++----- .../mainactivity/main/VideoDetailView.kt | 13 ++--- .../platformplayer/helpers/VideoHelper.kt | 6 +-- .../views/video/FutoVideoPlayerBase.kt | 2 +- 5 files changed, 42 insertions(+), 47 deletions(-) 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 db2b002c2..728e2192d 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 @@ -75,7 +75,7 @@ open class JSDashManifestRawSource( override var streamMetaData: StreamMetaData? = null companion object { - val adaptationSetRegex = Regex("", RegexOption.DOT_MATCHES_ALL) + val adaptationSetRegex = Regex("", RegexOption.DOT_MATCHES_ALL) } private var _pregenerate: V8Deferred? = null @@ -214,7 +214,7 @@ class JSDashManifestMergingRawSource( val videoDashDef = video.generateAsync(scope); val audioDashDef = audio.generateAsync(scope); - return V8Deferred.merge(scope, listOf(videoDashDef, audioDashDef)) { + return V8Deferred.merge(scope, listOf(videoDashDef, audioDashDef)) { it -> val (videoDash: String?, audioDash: String?) = it; if (videoDash != null && audioDash == null) return@merge videoDash; @@ -226,18 +226,23 @@ class JSDashManifestMergingRawSource( var result: String? val audioAdaptationSets = adaptationSetRegex.findAll(audioDash!!) val audioTracks = audioAdaptationSets - .filter { it.value.contains("contentType=\"audio\"") || it.value.contains("mimeType=\"audio/") || it.value.contains("lang=") } - .map { + .filter { + it.value.contains("contentType=\"audio\"") || it.value.contains("mimeType=\"audio/") || it.value.contains( + "lang=" + ) + }.joinToString("\n") { var set = it.value - if (!set.contains("lang=") && audio.language != null) { - set = set.replaceFirst("")) { + result = if (audioTracks.isNotEmpty()) { + if (videoDash.contains("")) { videoDash.replaceFirst("", "\n$audioTracks") } else if (videoDash.contains("")) { videoDash.replace("", "$audioTracks\n") @@ -247,7 +252,7 @@ class JSDashManifestMergingRawSource( videoDash + audioTracks } } else - result = videoDash + videoDash return@merge result }; @@ -261,21 +266,26 @@ class JSDashManifestMergingRawSource( //TODO: Temporary simple solution..make more reliable version - var result: String? = null; + var result: String? val audioAdaptationSets = adaptationSetRegex.findAll(audioDash!!) val audioTracks = audioAdaptationSets - .filter { it.value.contains("contentType=\"audio\"") || it.value.contains("mimeType=\"audio/") || it.value.contains("lang=") } - .map { + .filter { + it.value.contains("contentType=\"audio\"") || it.value.contains("mimeType=\"audio/") || it.value.contains( + "lang=" + ) + }.joinToString("\n") { var set = it.value - if (!set.contains("lang=") && audio.language != null) { - set = set.replaceFirst("")) { + result = if (audioTracks.isNotEmpty()) { + if (videoDash.contains("")) { videoDash.replaceFirst("", "\n$audioTracks") } else if (videoDash.contains("")) { videoDash.replace("", "$audioTracks\n") @@ -284,9 +294,8 @@ class JSDashManifestMergingRawSource( } else { videoDash + audioTracks } - } - else - result = videoDash + } else + videoDash return result } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt index 1c1334ff5..38a2bea81 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ShortView.kt @@ -476,14 +476,14 @@ class ShortView : FrameLayout { .plus(audioSources?.map { it.language } ?: listOf()) .distinct() - var videoSourceItems = mutableListOf() - var audioSourceItems = mutableListOf() + val videoSourceItems = mutableListOf() + val audioSourceItems = mutableListOf() var selectedLanguage: String? = null - val languageFilters = if(allLanguages.filter { it != null }.count() > 1) + val languageFilters = if(allLanguages.filterNotNull().count() > 1) SlideUpMenuButtonList(this.context, null, "language_filter", true).apply { var languageFilterLabels = allLanguages.filterNotNull().toList() val english = languageFilterLabels.find { it?.lowercase() == "en" } - val originalLanguage = videoSources?.find { it.original == true }?.language ?: audioSources?.find { it.original == true }?.language + val originalLanguage = videoSources?.find { it.original == true }?.language ?: audioSources?.find { it.original }?.language val primaryLanguage = Settings.instance.playback.getPrimaryLanguage() val hasPrimaryLanguage = allLanguages.any { it == primaryLanguage } @@ -519,7 +519,7 @@ class ShortView : FrameLayout { } } } - else null; + else null val canSetSpeed = true val currentPlaybackRate = player.getPlaybackRate() @@ -688,13 +688,6 @@ class ShortView : FrameLayout { // Audio track selection from manifest val audioTracks = player.exoPlayer?.player?.currentTracks?.groups?.firstOrNull { it.mediaTrackGroup.type == C.TRACK_TYPE_AUDIO } if (audioTracks != null) { - var audioMenuGroup: SlideUpMenuGroup? = null - for (view in overlayQualitySelector!!.groupItems) { - if (view is SlideUpMenuGroup && view.groupTag == "audio") { - audioMenuGroup = view - } - } - val currentAudioTrack = player.exoPlayer?.player?.audioFormat if (currentAudioTrack != null) { overlayQualitySelector?.selectOption("audio", currentAudioTrack) 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 3789fd8ac..e40d1b199 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 @@ -2349,13 +2349,6 @@ class VideoDetailView : ConstraintLayout { // Audio track selection from manifest val audioTracks = _player.exoPlayer?.player?.currentTracks?.groups?.firstOrNull { it.mediaTrackGroup.type == C.TRACK_TYPE_AUDIO } if (audioTracks != null) { - var audioMenuGroup: SlideUpMenuGroup? = null - for (view in _overlay_quality_selector!!.groupItems) { - if (view is SlideUpMenuGroup && view.groupTag == "audio") { - audioMenuGroup = view - } - } - val currentAudioTrack = _player.exoPlayer?.player?.audioFormat if (currentAudioTrack != null) { _overlay_quality_selector?.selectOption("audio", currentAudioTrack) @@ -2486,14 +2479,14 @@ class VideoDetailView : ConstraintLayout { Log.i(TAG, "Language count: ${allLanguages}"); var videoSourceItems = mutableListOf(); - var audioSourceItems = mutableListOf() + val audioSourceItems = mutableListOf() var selectedLanguage: String? = null; val languageFilters = if(allLanguages.filter { it != null }.count() > 1) SlideUpMenuButtonList(this.context, null, "language_filter", true).apply { var languageFilterLabels = allLanguages.filterNotNull().toList(); val english = languageFilterLabels.find { it?.lowercase() == "en" }; - val originalLanguage = videoSources?.find { it.original == true }?.language ?: audioSources?.find { it.original == true }?.language - val primaryLanguage = Settings.instance.playback.getPrimaryLanguage(); + val originalLanguage = videoSources?.find { it.original == true }?.language ?: audioSources?.find { it.original }?.language + val primaryLanguage = Settings.instance.playback.getPrimaryLanguage() val hasPrimaryLanguage = allLanguages.any { it == primaryLanguage } if(english != null) diff --git a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt index f3b202fc1..9a8ad3620 100644 --- a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt +++ b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt @@ -88,10 +88,10 @@ class VideoHelper { else Language.UNKNOWN; } - if(altSources.any { it.language == languageToFilter }) { - altSources = altSources.filter { it.language == languageToFilter }.sortedBy { it.bitrate }.toList() + altSources = if(altSources.any { it.language == languageToFilter }) { + altSources.filter { it.language == languageToFilter }.sortedBy { it.bitrate }.toList() } else { - altSources = altSources.sortedBy { it.bitrate }.toList() + altSources.sortedBy { it.bitrate }.toList() } var bestSource = altSources.firstOrNull(); 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 c819ae2d6..42f3a5a59 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 @@ -925,7 +925,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout { _lastAudioMediaSource = DashMediaSource.Factory(dataSource) .createMediaSource( DashManifestParser().parse( - Uri.parse(audioSource.url ?: ""), + (audioSource.url ?: "").toUri(), ByteArrayInputStream(audioSource.manifest?.toByteArray() ?: ByteArray(0)) ) );