mirror of
				https://github.com/recloudstream/cloudstream.git
				synced 2024-08-15 01:53:11 +00:00 
			
		
		
		
	Reworked the internal subtitle API to fix edge-cases when importing subtitles
This commit is contained in:
		
							parent
							
								
									6d13cf0b01
								
							
						
					
					
						commit
						4b0b6f6f20
					
				
					 5 changed files with 102 additions and 102 deletions
				
			
		|  | @ -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<Pair<String, Boolean>>() | ||||
|     private var playerSelectedSubtitleTracks = listOf<Pair<String, Boolean>>() | ||||
| 
 | ||||
|     /** isPlaying */ | ||||
|     private var updateIsPlaying: ((Pair<CSPlayerLoading, CSPlayerLoading>) -> Unit)? = null | ||||
|  | @ -311,14 +310,18 @@ class CS3IPlayer : IPlayer { | |||
|      * */ | ||||
|     private fun List<Tracks.Group>.getFormats(): List<Pair<Format, Int>> { | ||||
|         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<Pair<Format, Int>> { | ||||
|         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 | ||||
|                     } | ||||
|  |  | |||
|  | @ -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<SubtitleData>, 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() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -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()) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
|  |  | |||
|  | @ -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<String, String> | ||||
| ) | ||||
| ) { | ||||
|     /** 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<SubtitleData> = 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 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue