diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b50cb62a..306d2c77 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -48,7 +48,7 @@ android { targetSdk = 33 versionCode = 55 - versionName = "3.2.3" + versionName = "3.2.5" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") @@ -190,7 +190,7 @@ dependencies { // Networking // implementation("com.squareup.okhttp3:okhttp:4.9.2") // implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") - implementation("com.github.Blatzar:NiceHttp:0.4.0") + implementation("com.github.Blatzar:NiceHttp:0.4.1") // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") // Util to skip the URI file fuckery 🙏 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 8eda6e30..8f7e06f9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -8,8 +8,7 @@ import android.util.Log import android.widget.FrameLayout import androidx.preference.PreferenceManager import com.google.android.exoplayer2.* -import com.google.android.exoplayer2.C.TRACK_TYPE_AUDIO -import com.google.android.exoplayer2.C.TRACK_TYPE_VIDEO +import com.google.android.exoplayer2.C.* import com.google.android.exoplayer2.database.StandaloneDatabaseProvider import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource import com.google.android.exoplayer2.source.* @@ -89,10 +88,10 @@ class CS3IPlayer : IPlayer { /** * Tracks reported to be used by exoplayer, since sometimes it has a mind of it's own when selecting subs. - * String = lowercase language as set by .setLanguage("_$langId") + * String = id * Boolean = if it's active * */ - private var exoPlayerSelectedTracks = listOf>() + private var playerSelectedSubtitleTracks = listOf>() /** isPlaying */ private var updateIsPlaying: ((Pair) -> Unit)? = null @@ -311,14 +310,18 @@ class CS3IPlayer : IPlayer { * */ private fun List.getFormats(): List> { return this.map { - (0 until it.mediaTrackGroup.length).mapNotNull { i -> - if (it.isSupported) - it.mediaTrackGroup.getFormat(i) to i - else null - } + it.getFormats() }.flatten() } + private fun Tracks.Group.getFormats(): List> { + return (0 until this.mediaTrackGroup.length).mapNotNull { i -> + if (this.isSupported) + this.mediaTrackGroup.getFormat(i) to i + else null + } + } + private fun Format.toAudioTrack(): AudioTrack { return AudioTrack( this.id, @@ -361,12 +364,17 @@ class CS3IPlayer : IPlayer { override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean { Log.i(TAG, "setPreferredSubtitles init $subtitle") currentSubtitles = subtitle + + fun getTextTrack(id: String) = + exoPlayer?.currentTracks?.groups?.filter { it.type == TRACK_TYPE_TEXT } + ?.getTrack(id) + return (exoPlayer?.trackSelector as? DefaultTrackSelector?)?.let { trackSelector -> - val name = subtitle?.name - if (name.isNullOrBlank()) { + if (subtitle == null) { trackSelector.setParameters( trackSelector.buildUponParameters() .setPreferredTextLanguage(null) + .clearOverridesOfType(TRACK_TYPE_TEXT) ) } else { when (subtitleHelper.subtitleStatus(subtitle)) { @@ -380,12 +388,15 @@ class CS3IPlayer : IPlayer { trackSelector.setParameters( trackSelector.buildUponParameters() .apply { - if (subtitle.origin == SubtitleOrigin.EMBEDDED_IN_VIDEO) - // The real Language (two letter) is in the url - // No underscore as the .url is the actual exoplayer designated language - setPreferredTextLanguage(subtitle.url) - else - setPreferredTextLanguage("_$name") + val track = getTextTrack(subtitle.getId()) + if (track != null) { + setOverrideForType( + TrackSelectionOverride( + track.first, + track.second + ) + ) + } } ) @@ -419,17 +430,8 @@ class CS3IPlayer : IPlayer { override fun getCurrentPreferredSubtitle(): SubtitleData? { return subtitleHelper.getAllSubtitles().firstOrNull { sub -> - exoPlayerSelectedTracks.any { - // When embedded the real language is in .url as the real name is a two letter code - val realName = - if (sub.origin == SubtitleOrigin.EMBEDDED_IN_VIDEO) sub.url else sub.name - - // The replace is needed as exoplayer translates _ to - - // Also we prefix the languages with _ - it.second && it.first.replace("-", "").equals( - realName.replace("-", ""), - ignoreCase = true - ) + playerSelectedSubtitleTracks.any { (id, isSelected) -> + isSelected && sub.getId() == id } } } @@ -747,7 +749,7 @@ class CS3IPlayer : IPlayer { } } - private fun getCurrentTimestamp(writePosition : Long? = null): EpisodeSkip.SkipStamp? { + private fun getCurrentTimestamp(writePosition: Long? = null): EpisodeSkip.SkipStamp? { val position = writePosition ?: this@CS3IPlayer.getPosition() ?: return null for (lastTimeStamp in lastTimeStamps) { if (lastTimeStamp.startMs <= position && position < lastTimeStamp.endMs) { @@ -757,7 +759,7 @@ class CS3IPlayer : IPlayer { return null } - fun updatedTime(writePosition : Long? = null) { + fun updatedTime(writePosition: Long? = null) { getCurrentTimestamp(writePosition)?.let { timestamp -> onTimestampInvoked?.invoke(timestamp) } @@ -883,43 +885,36 @@ class CS3IPlayer : IPlayer { } exoPlayer?.addListener(object : Player.Listener { override fun onTracksChanged(tracks: Tracks) { - fun Format.isSubtitle(): Boolean { - return this.sampleMimeType?.contains("video/") == false && - this.sampleMimeType?.contains("audio/") == false - } - normalSafeApiCall { - exoPlayerSelectedTracks = - tracks.groups.mapNotNull { - val format = it.mediaTrackGroup.getFormat(0) - if (format.isSubtitle()) - format.language?.let { lang -> lang to it.isSelected } - else null - } + val textTracks = tracks.groups.filter { it.type == TRACK_TYPE_TEXT } - val exoPlayerReportedTracks = tracks.groups.mapNotNull { - // Filter out unsupported tracks - if (it.isSupported) - it.mediaTrackGroup.getFormat(0) - else - null - }.mapNotNull { - // Filter out non subs, already used subs and subs without languages - if (!it.isSubtitle() || - // Anything starting with - is not embedded - it.language?.startsWith("-") == true || - it.language == null - ) return@mapNotNull null - return@mapNotNull SubtitleData( - // Nicer looking displayed names - fromTwoLettersToLanguage(it.language!!) ?: it.language!!, - // See setPreferredTextLanguage - it.language!!, - SubtitleOrigin.EMBEDDED_IN_VIDEO, - it.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP, - emptyMap() - ) - } + playerSelectedSubtitleTracks = + textTracks.map { group -> + group.getFormats().mapNotNull { (format, _) -> + (format.id ?: return@mapNotNull null) to group.isSelected + } + }.flatten() + + val exoPlayerReportedTracks = + tracks.groups.filter { it.type == TRACK_TYPE_TEXT }.getFormats() + .mapNotNull { (format, _) -> + // Filter out non subs, already used subs and subs without languages + if (format.id == null || + format.language == null || + format.language?.startsWith("-") == true + ) return@mapNotNull null + + return@mapNotNull SubtitleData( + // Nicer looking displayed names + fromTwoLettersToLanguage(format.language!!) + ?: format.language!!, + // See setPreferredTextLanguage + format.id!!, + SubtitleOrigin.EMBEDDED_IN_VIDEO, + format.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP, + emptyMap() + ) + } embeddedSubtitlesFetched?.invoke(exoPlayerReportedTracks) onTracksInfoChanged?.invoke() @@ -978,7 +973,7 @@ class CS3IPlayer : IPlayer { // This is to switch mirrors automatically if the stream has not been fetched, but // allow playing the buffer without internet as then the duration is fetched. if (error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED - && exoPlayer?.duration != C.TIME_UNSET + && exoPlayer?.duration != TIME_UNSET ) { exoPlayer?.prepare() } else { @@ -1137,14 +1132,15 @@ class CS3IPlayer : IPlayer { val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url)) .setMimeType(sub.mimeType) .setLanguage("_${sub.name}") - .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .setId(sub.getId()) + .setSelectionFlags(SELECTION_FLAG_DEFAULT) .build() when (sub.origin) { SubtitleOrigin.DOWNLOADED_FILE -> { if (offlineSourceFactory != null) { activeSubtitles.add(sub) SingleSampleMediaSource.Factory(offlineSourceFactory) - .createMediaSource(subConfig, C.TIME_UNSET) + .createMediaSource(subConfig, TIME_UNSET) } else { null } @@ -1156,7 +1152,7 @@ class CS3IPlayer : IPlayer { if (sub.headers.isNotEmpty()) this.setDefaultRequestProperties(sub.headers) }) - .createMediaSource(subConfig, C.TIME_UNSET) + .createMediaSource(subConfig, TIME_UNSET) } else { null } @@ -1165,7 +1161,7 @@ class CS3IPlayer : IPlayer { if (offlineSourceFactory != null) { activeSubtitles.add(sub) SingleSampleMediaSource.Factory(offlineSourceFactory) - .createMediaSource(subConfig, C.TIME_UNSET) + .createMediaSource(subConfig, TIME_UNSET) } else { null } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 95a9393f..fa0a2a7f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -438,16 +438,17 @@ class GeneratorPlayer : FullScreenPlayer() { private fun addAndSelectSubtitles(subtitleData: SubtitleData) { val ctx = context ?: return - setSubtitles(subtitleData) - // this is used instead of observe, because observe is too slow val subs = currentSubs + subtitleData + // this is used instead of observe(viewModel._currentSubs), because observe is too slow + player.setActiveSubtitles(subs) + // Save current time as to not reset player to 00:00 player.saveData() - player.setActiveSubtitles(subs) player.reloadPlayer(ctx) + setSubtitles(subtitleData) viewModel.addSubtitles(setOf(subtitleData)) selectSourceDialog?.dismissSafe() @@ -959,7 +960,7 @@ class GeneratorPlayer : FullScreenPlayer() { subtitles: Set, settings: Boolean, downloads: Boolean ): SubtitleData? { val langCode = preferredAutoSelectSubtitles ?: return null - val lang = SubtitleHelper.fromTwoLettersToLanguage(langCode) ?: return null + val lang = fromTwoLettersToLanguage(langCode) ?: return null if (downloads) { return subtitles.firstOrNull { sub -> (sub.origin == SubtitleOrigin.DOWNLOADED_FILE && sub.name == context?.getString( @@ -970,22 +971,11 @@ class GeneratorPlayer : FullScreenPlayer() { sortSubs(subtitles).firstOrNull { sub -> val t = sub.name.replace(Regex("[^A-Za-z]"), " ").trim() - (settings || (downloads && sub.origin == SubtitleOrigin.DOWNLOADED_FILE)) && t == lang || t.startsWith( - "$lang " - ) || t == langCode + (settings) && t == lang || t.startsWith(lang) || t == langCode }?.let { sub -> return sub } - // post check in case both did not catch anything - if (downloads) { - return subtitles.firstOrNull { sub -> - (sub.origin == SubtitleOrigin.DOWNLOADED_FILE || sub.name == context?.getString( - R.string.default_subtitles - )) - } - } - return null } @@ -1006,14 +996,12 @@ class GeneratorPlayer : FullScreenPlayer() { getAutoSelectSubtitle( currentSubs, settings = true, downloads = false )?.let { sub -> - if (setSubtitles(sub)) { player.saveData() player.reloadPlayer(ctx) player.handleEvent(CSPlayerEvent.Play) return true } - } } } @@ -1304,8 +1292,10 @@ class GeneratorPlayer : FullScreenPlayer() { Log.i("subfilter", "Filtering subtitle") langFilterList.forEach { lang -> Log.i("subfilter", "Lang: $lang") - setOfSub += set.filter { it.name.contains(lang, ignoreCase = true) } - .toMutableSet() + setOfSub += set.filter { + it.name.contains(lang, ignoreCase = true) || + it.origin != SubtitleOrigin.URL + } } currentSubs = setOfSub } else { @@ -1313,7 +1303,13 @@ class GeneratorPlayer : FullScreenPlayer() { } player.setActiveSubtitles(set) - autoSelectSubtitles() + // If the file is downloaded then do not select auto select the subtitles + // Downloaded subtitles cannot be selected immediately after loading since + // player.getCurrentPreferredSubtitle() cannot fetch data from non-loaded subtitles + // Resulting in unselecting the downloaded subtitle + if (set.lastOrNull()?.origin != SubtitleOrigin.DOWNLOADED_FILE) { + autoSelectSubtitles() + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 4f16e9f6..dc33f67c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -113,8 +113,9 @@ class PlayerGeneratorViewModel : ViewModel() { // Do not post if there's nothing new // Posting will refresh subtitles which will in turn // make the subs to english if previously unselected - if (allSubs != currentSubs) + if (allSubs != currentSubs) { _currentSubs.postValue(allSubs) + } } private var currentJob: Job? = null @@ -164,9 +165,10 @@ class PlayerGeneratorViewModel : ViewModel() { } _loadingLinks.postValue(loadingState) - _currentLinks.postValue(currentLinks) - _currentSubs.postValue(currentSubs) + _currentSubs.postValue( + currentSubs.union(_currentSubs.value ?: emptySet()) + ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt index b06812c4..8d85f176 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt @@ -28,7 +28,7 @@ enum class SubtitleOrigin { /** * @param name To be displayed in the player - * @param url Url for the subtitle, when EMBEDDED_IN_VIDEO this variable is used as the real backend language + * @param url Url for the subtitle, when EMBEDDED_IN_VIDEO this variable is used as the real backend id * @param headers if empty it will use the base onlineDataSource headers else only the specified headers * */ data class SubtitleData( @@ -37,7 +37,13 @@ data class SubtitleData( val origin: SubtitleOrigin, val mimeType: String, val headers: Map -) +) { + /** Internal ID for exoplayer, unique for each link*/ + fun getId(): String { + return if (origin == SubtitleOrigin.EMBEDDED_IN_VIDEO) url + else "$url|$name" + } +} class PlayerSubtitleHelper { private var activeSubtitles: Set = emptySet() @@ -79,11 +85,11 @@ class PlayerSubtitleHelper { } } - fun subtitleStatus(sub : SubtitleData?): SubtitleStatus { - if(activeSubtitles.contains(sub)) { + fun subtitleStatus(sub: SubtitleData?): SubtitleStatus { + if (activeSubtitles.contains(sub)) { return SubtitleStatus.IS_ACTIVE } - if(allSubtitles.contains(sub)) { + if (allSubtitles.contains(sub)) { return SubtitleStatus.REQUIRES_RELOAD } return SubtitleStatus.NOT_FOUND @@ -95,7 +101,7 @@ class PlayerSubtitleHelper { regexSubtitlesToRemoveCaptions = style.removeCaptions subtitleView?.context?.let { ctx -> subStyle = style - Log.i(TAG,"SET STYLE = $style") + Log.i(TAG, "SET STYLE = $style") subtitleView?.setStyle(ctx.fromSaveToStyle(style)) subtitleView?.translationY = -style.elevation.toPx.toFloat() val size = style.fixedTextSize