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..65ac5bf5e 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/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 { 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..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; - 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..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 @@ -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,9 +17,9 @@ 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? val hasGenerate: Boolean; var manifest: String?; fun generateAsync(scope: CoroutineScope): Deferred; @@ -67,7 +61,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? = @@ -80,6 +74,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); @@ -126,7 +124,17 @@ open class JSDashManifestRawSource( } return@busy result.convert { - it.value + var manifest = it.value + if (manifest != null && language != null) { + 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 @@ -151,16 +159,26 @@ open class JSDashManifestRawSource( _plugin.isBusyWith("dashVideo.generate") { _obj.invokeV8("generate").value; } - }); + }) if(result != null){ + if (language != null) { + 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) } } } @@ -196,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; @@ -205,17 +223,38 @@ 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 - ) + 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=" + ) + }.joinToString("\n") { + var set = it.value + if (!set.contains("lang=")) { + 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; + videoDash - return@merge result; + return@merge result }; } override fun generate(): String? { @@ -227,18 +266,40 @@ 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) - } - else - result = videoDash; + 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=" + ) + }.joinToString("\n") { + var set = it.value + if (!set.contains("lang=")) { + 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 + videoDash + + return result } 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 d7b8c0b89..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 @@ -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 @@ -44,7 +45,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 +441,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 +462,64 @@ 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 allLanguages = (videoSources?.map { it.language } ?: listOf()) + .plus(audioSources?.map { it.language } ?: listOf()) + .distinct() + + val videoSourceItems = mutableListOf() + val audioSourceItems = mutableListOf() + var selectedLanguage: String? = null + 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 }?.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 = VISIBLE + else + it.visibility = GONE + } + } + audioSourceItems.forEach { + val item = it.itemTag; + if(item is IAudioSource) { + if(item.language == selected) + it.visibility = VISIBLE + else + it.visibility = GONE + } + } + } + } + else null val canSetSpeed = true val currentPlaybackRate = player.getPlaybackRate() @@ -508,19 +564,46 @@ 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 = GONE + } + } }.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) }) - }.toList().toTypedArray() + 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 = 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) + 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 = GONE + } + } + } + } ?: listOf() + )).toTypedArray() ) else null, if (video?.subtitles?.isNotEmpty() == true) SlideUpMenuGroup( this.context, context.getString(R.string.subtitles), "subtitles", *video.subtitles.map { @@ -572,7 +655,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 +684,15 @@ 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) { + val currentAudioTrack = player.exoPlayer?.player?.audioFormat + if (currentAudioTrack != null) { + overlayQualitySelector?.selectOption("audio", currentAudioTrack) + } + } } val currentPlaybackRate = player.getPlaybackRate() @@ -843,12 +935,17 @@ 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 ?: (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 76435cf0e..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 @@ -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 @@ -84,7 +81,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 +105,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 @@ -180,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 @@ -2043,9 +2042,15 @@ 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 subtitleSource = _lastSubtitleSource ?: (if (Settings.instance.playback.stickySubtitles) _player.getPreferredSubtitleSource(video, _subtitleLanguage) else null) ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null); + 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(primaryLanguage) + _player.setPreferredSubtitleLanguage(_subtitleLanguage) + Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)") if(videoSource == null && audioSource == null) { @@ -2310,7 +2315,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 +2345,15 @@ 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) { + 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 +2450,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(); @@ -2451,7 +2465,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 } @@ -2463,14 +2479,15 @@ class VideoDetailView : ConstraintLayout { Log.i(TAG, "Language count: ${allLanguages}"); var videoSourceItems = 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; - val primaryLanguage = Settings.instance.playback.getPrimaryLanguage(); - val hasPrimaryLanguage = videoSources?.any { it.language == primaryLanguage } ?: false; + 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) languageFilterLabels = listOf(english).plus(languageFilterLabels.filter { it != english }).toList(); @@ -2493,6 +2510,15 @@ class VideoDetailView : ConstraintLayout { it.visibility = View.GONE; } } + audioSourceItems.forEach { + val item = it.itemTag + if(item is IAudioSource) { + if(item.language == selected) + it.visibility = VISIBLE + else + it.visibility = GONE + } + } } } else null; @@ -2504,11 +2530,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() @@ -2627,7 +2657,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 ""; @@ -2637,8 +2667,27 @@ class VideoDetailView : ConstraintLayout { it.bitrate.toHumanBitrate(), (prefix + it.codec.trim()).trim(), tag = it, - call = { handleSelectAudioTrack(it) }); - }.toList().toTypedArray()) + call = { handleSelectAudioTrack(it) }).apply { + audioSourceItems.add(this) + if(selectedLanguage != null) { + if(it.language != selectedLanguage) + 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) + 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 = 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/helpers/VideoHelper.kt b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt index 1947736d8..9a8ad3620 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 @@ -85,10 +88,10 @@ class VideoHelper { else Language.UNKNOWN; } - if(altSources.any { it.language == languageToFilter }) { - 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.sortedBy { it.bitrate } + altSources.sortedBy { it.bitrate }.toList() } var bestSource = altSources.firstOrNull(); @@ -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/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 0404e4633..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 @@ -14,9 +14,11 @@ 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 +import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.VideoSize import androidx.media3.common.text.CueGroup import androidx.media3.common.util.UnstableApi @@ -98,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" @@ -147,6 +150,12 @@ 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 + private set private var _toResume = false; @@ -319,40 +328,88 @@ 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 fun selectVideoTrack(height: Int) { - targetTrackVideoHeight = height; - updateTrackSelector(); + targetTrackVideoHeight = height + _targetTrackVideoFormat = null + updateTrackSelector() } + fun selectAudioTrack(bitrate: Int) { - _targetTrackAudioBitrate = bitrate; - updateTrackSelector(); + _targetTrackAudioBitrate = bitrate + _targetTrackAudioFormat = null + updateTrackSelector() + } + + @OptIn(UnstableApi::class) + fun selectAudioTrack(format: Format?) { + _targetTrackAudioFormat = format; + _targetTrackAudioBitrate = format?.bitrate ?: -1 + 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); + val player = exoPlayer?.player ?: return + var builder = player.trackSelectionParameters.buildUpon() + + builder = builder.clearOverrides() + + if (_targetTrackVideoFormat != null || _targetTrackAudioFormat != null) { + 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 } } + 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); - } - - builder = if (isAudioMode) { - builder.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true) } else { - builder.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, false) + builder = builder.setMaxAudioBitrate(Int.MAX_VALUE) } - val trackSelector = exoPlayer?.player?.trackSelector; - if(trackSelector != null) { - trackSelector.parameters = builder.build(); + if (preferredAudioLanguage != null) { + builder = builder.setPreferredAudioLanguage(preferredAudioLanguage) + } + if (preferredSubtitleLanguage != null) { + builder = builder.setPreferredTextLanguage(preferredSubtitleLanguage) } + + builder = builder.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, isAudioMode) + + player.trackSelectionParameters = builder.build() } fun setChapters(chapters: List?) { @@ -381,14 +438,20 @@ abstract class FutoVideoPlayerBase : RelativeLayout { var videoSourceUsed = videoSource; var audioSourceUsed = audioSource; if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){ - videoSource.getUnderlyingPlugin()?.busy { - videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource); - audioSourceUsed = null; + 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 } } - 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; @@ -637,9 +700,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout { val scope = this; var startId = -1; try { - val plugin = videoSource.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) @@ -667,13 +730,13 @@ abstract class FutoVideoPlayerBase : RelativeLayout { _lastVideoMediaSource = DashMediaSource.Factory(dataSource) .createMediaSource( DashManifestParser().parse( - Uri.parse(videoSource.url), + Uri.parse(videoSource.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 +770,10 @@ 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), + .createMediaSource(DashManifestParser().parse( + (videoSource.url ?: "").toUri(), ByteArrayInputStream(videoSource.manifest?.toByteArray() ?: ByteArray(0)))); return true; } @@ -801,8 +866,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) { @@ -824,8 +889,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout { else DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); withContext(Dispatchers.Main) { - _lastVideoMediaSource = DashMediaSource.Factory(dataSource) - .createMediaSource(DashManifestParser().parse(Uri.parse(audioSource.url), + _lastAudioMediaSource = DashMediaSource.Factory(dataSource) + .createMediaSource(DashManifestParser().parse(Uri.parse(audioSource.url ?: ""), ByteArrayInputStream(generated?.toByteArray() ?: ByteArray(0)))); loadSelectedSources(play, resume); } @@ -833,12 +898,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 +922,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout { audioSource.getHttpDataSourceFactory() else DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT); - _lastVideoMediaSource = DashMediaSource.Factory(dataSource) + _lastAudioMediaSource = DashMediaSource.Factory(dataSource) .createMediaSource( DashManifestParser().parse( - Uri.parse(audioSource.url), + (audioSource.url ?: "").toUri(), ByteArrayInputStream(audioSource.manifest?.toByteArray() ?: ByteArray(0)) ) ); @@ -897,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) @@ -911,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);