Reworked the internal subtitle API to fix edge-cases when importing subtitles

This commit is contained in:
Blatzar 2022-12-08 16:18:03 +01:00
parent 6d13cf0b01
commit 4b0b6f6f20
5 changed files with 102 additions and 102 deletions

View file

@ -48,7 +48,7 @@ android {
targetSdk = 33 targetSdk = 33
versionCode = 55 versionCode = 55
versionName = "3.2.3" versionName = "3.2.5"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
@ -190,7 +190,7 @@ dependencies {
// Networking // Networking
// implementation("com.squareup.okhttp3:okhttp:4.9.2") // implementation("com.squareup.okhttp3:okhttp:4.9.2")
// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") // 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 // To fix SSL fuckery on android 9
implementation("org.conscrypt:conscrypt-android:2.2.1") implementation("org.conscrypt:conscrypt-android:2.2.1")
// Util to skip the URI file fuckery 🙏 // Util to skip the URI file fuckery 🙏

View file

@ -8,8 +8,7 @@ import android.util.Log
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.exoplayer2.* import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.C.TRACK_TYPE_AUDIO import com.google.android.exoplayer2.C.*
import com.google.android.exoplayer2.C.TRACK_TYPE_VIDEO
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
import com.google.android.exoplayer2.source.* 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. * 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 * Boolean = if it's active
* */ * */
private var exoPlayerSelectedTracks = listOf<Pair<String, Boolean>>() private var playerSelectedSubtitleTracks = listOf<Pair<String, Boolean>>()
/** isPlaying */ /** isPlaying */
private var updateIsPlaying: ((Pair<CSPlayerLoading, CSPlayerLoading>) -> Unit)? = null 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>> { private fun List<Tracks.Group>.getFormats(): List<Pair<Format, Int>> {
return this.map { return this.map {
(0 until it.mediaTrackGroup.length).mapNotNull { i -> it.getFormats()
if (it.isSupported)
it.mediaTrackGroup.getFormat(i) to i
else null
}
}.flatten() }.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 { private fun Format.toAudioTrack(): AudioTrack {
return AudioTrack( return AudioTrack(
this.id, this.id,
@ -361,12 +364,17 @@ class CS3IPlayer : IPlayer {
override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean { override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean {
Log.i(TAG, "setPreferredSubtitles init $subtitle") Log.i(TAG, "setPreferredSubtitles init $subtitle")
currentSubtitles = 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 -> return (exoPlayer?.trackSelector as? DefaultTrackSelector?)?.let { trackSelector ->
val name = subtitle?.name if (subtitle == null) {
if (name.isNullOrBlank()) {
trackSelector.setParameters( trackSelector.setParameters(
trackSelector.buildUponParameters() trackSelector.buildUponParameters()
.setPreferredTextLanguage(null) .setPreferredTextLanguage(null)
.clearOverridesOfType(TRACK_TYPE_TEXT)
) )
} else { } else {
when (subtitleHelper.subtitleStatus(subtitle)) { when (subtitleHelper.subtitleStatus(subtitle)) {
@ -380,12 +388,15 @@ class CS3IPlayer : IPlayer {
trackSelector.setParameters( trackSelector.setParameters(
trackSelector.buildUponParameters() trackSelector.buildUponParameters()
.apply { .apply {
if (subtitle.origin == SubtitleOrigin.EMBEDDED_IN_VIDEO) val track = getTextTrack(subtitle.getId())
// The real Language (two letter) is in the url if (track != null) {
// No underscore as the .url is the actual exoplayer designated language setOverrideForType(
setPreferredTextLanguage(subtitle.url) TrackSelectionOverride(
else track.first,
setPreferredTextLanguage("_$name") track.second
)
)
}
} }
) )
@ -419,17 +430,8 @@ class CS3IPlayer : IPlayer {
override fun getCurrentPreferredSubtitle(): SubtitleData? { override fun getCurrentPreferredSubtitle(): SubtitleData? {
return subtitleHelper.getAllSubtitles().firstOrNull { sub -> return subtitleHelper.getAllSubtitles().firstOrNull { sub ->
exoPlayerSelectedTracks.any { playerSelectedSubtitleTracks.any { (id, isSelected) ->
// When embedded the real language is in .url as the real name is a two letter code isSelected && sub.getId() == id
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
)
} }
} }
} }
@ -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 val position = writePosition ?: this@CS3IPlayer.getPosition() ?: return null
for (lastTimeStamp in lastTimeStamps) { for (lastTimeStamp in lastTimeStamps) {
if (lastTimeStamp.startMs <= position && position < lastTimeStamp.endMs) { if (lastTimeStamp.startMs <= position && position < lastTimeStamp.endMs) {
@ -757,7 +759,7 @@ class CS3IPlayer : IPlayer {
return null return null
} }
fun updatedTime(writePosition : Long? = null) { fun updatedTime(writePosition: Long? = null) {
getCurrentTimestamp(writePosition)?.let { timestamp -> getCurrentTimestamp(writePosition)?.let { timestamp ->
onTimestampInvoked?.invoke(timestamp) onTimestampInvoked?.invoke(timestamp)
} }
@ -883,43 +885,36 @@ class CS3IPlayer : IPlayer {
} }
exoPlayer?.addListener(object : Player.Listener { exoPlayer?.addListener(object : Player.Listener {
override fun onTracksChanged(tracks: Tracks) { override fun onTracksChanged(tracks: Tracks) {
fun Format.isSubtitle(): Boolean {
return this.sampleMimeType?.contains("video/") == false &&
this.sampleMimeType?.contains("audio/") == false
}
normalSafeApiCall { normalSafeApiCall {
exoPlayerSelectedTracks = val textTracks = tracks.groups.filter { it.type == TRACK_TYPE_TEXT }
tracks.groups.mapNotNull {
val format = it.mediaTrackGroup.getFormat(0)
if (format.isSubtitle())
format.language?.let { lang -> lang to it.isSelected }
else null
}
val exoPlayerReportedTracks = tracks.groups.mapNotNull { playerSelectedSubtitleTracks =
// Filter out unsupported tracks textTracks.map { group ->
if (it.isSupported) group.getFormats().mapNotNull { (format, _) ->
it.mediaTrackGroup.getFormat(0) (format.id ?: return@mapNotNull null) to group.isSelected
else }
null }.flatten()
}.mapNotNull {
// Filter out non subs, already used subs and subs without languages val exoPlayerReportedTracks =
if (!it.isSubtitle() || tracks.groups.filter { it.type == TRACK_TYPE_TEXT }.getFormats()
// Anything starting with - is not embedded .mapNotNull { (format, _) ->
it.language?.startsWith("-") == true || // Filter out non subs, already used subs and subs without languages
it.language == null if (format.id == null ||
) return@mapNotNull null format.language == null ||
return@mapNotNull SubtitleData( format.language?.startsWith("-") == true
// Nicer looking displayed names ) return@mapNotNull null
fromTwoLettersToLanguage(it.language!!) ?: it.language!!,
// See setPreferredTextLanguage return@mapNotNull SubtitleData(
it.language!!, // Nicer looking displayed names
SubtitleOrigin.EMBEDDED_IN_VIDEO, fromTwoLettersToLanguage(format.language!!)
it.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP, ?: format.language!!,
emptyMap() // See setPreferredTextLanguage
) format.id!!,
} SubtitleOrigin.EMBEDDED_IN_VIDEO,
format.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP,
emptyMap()
)
}
embeddedSubtitlesFetched?.invoke(exoPlayerReportedTracks) embeddedSubtitlesFetched?.invoke(exoPlayerReportedTracks)
onTracksInfoChanged?.invoke() onTracksInfoChanged?.invoke()
@ -978,7 +973,7 @@ class CS3IPlayer : IPlayer {
// This is to switch mirrors automatically if the stream has not been fetched, but // 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. // allow playing the buffer without internet as then the duration is fetched.
if (error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED if (error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED
&& exoPlayer?.duration != C.TIME_UNSET && exoPlayer?.duration != TIME_UNSET
) { ) {
exoPlayer?.prepare() exoPlayer?.prepare()
} else { } else {
@ -1137,14 +1132,15 @@ class CS3IPlayer : IPlayer {
val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url)) val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url))
.setMimeType(sub.mimeType) .setMimeType(sub.mimeType)
.setLanguage("_${sub.name}") .setLanguage("_${sub.name}")
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT) .setId(sub.getId())
.setSelectionFlags(SELECTION_FLAG_DEFAULT)
.build() .build()
when (sub.origin) { when (sub.origin) {
SubtitleOrigin.DOWNLOADED_FILE -> { SubtitleOrigin.DOWNLOADED_FILE -> {
if (offlineSourceFactory != null) { if (offlineSourceFactory != null) {
activeSubtitles.add(sub) activeSubtitles.add(sub)
SingleSampleMediaSource.Factory(offlineSourceFactory) SingleSampleMediaSource.Factory(offlineSourceFactory)
.createMediaSource(subConfig, C.TIME_UNSET) .createMediaSource(subConfig, TIME_UNSET)
} else { } else {
null null
} }
@ -1156,7 +1152,7 @@ class CS3IPlayer : IPlayer {
if (sub.headers.isNotEmpty()) if (sub.headers.isNotEmpty())
this.setDefaultRequestProperties(sub.headers) this.setDefaultRequestProperties(sub.headers)
}) })
.createMediaSource(subConfig, C.TIME_UNSET) .createMediaSource(subConfig, TIME_UNSET)
} else { } else {
null null
} }
@ -1165,7 +1161,7 @@ class CS3IPlayer : IPlayer {
if (offlineSourceFactory != null) { if (offlineSourceFactory != null) {
activeSubtitles.add(sub) activeSubtitles.add(sub)
SingleSampleMediaSource.Factory(offlineSourceFactory) SingleSampleMediaSource.Factory(offlineSourceFactory)
.createMediaSource(subConfig, C.TIME_UNSET) .createMediaSource(subConfig, TIME_UNSET)
} else { } else {
null null
} }

View file

@ -438,16 +438,17 @@ class GeneratorPlayer : FullScreenPlayer() {
private fun addAndSelectSubtitles(subtitleData: SubtitleData) { private fun addAndSelectSubtitles(subtitleData: SubtitleData) {
val ctx = context ?: return val ctx = context ?: return
setSubtitles(subtitleData)
// this is used instead of observe, because observe is too slow
val subs = currentSubs + subtitleData 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 // Save current time as to not reset player to 00:00
player.saveData() player.saveData()
player.setActiveSubtitles(subs)
player.reloadPlayer(ctx) player.reloadPlayer(ctx)
setSubtitles(subtitleData)
viewModel.addSubtitles(setOf(subtitleData)) viewModel.addSubtitles(setOf(subtitleData))
selectSourceDialog?.dismissSafe() selectSourceDialog?.dismissSafe()
@ -959,7 +960,7 @@ class GeneratorPlayer : FullScreenPlayer() {
subtitles: Set<SubtitleData>, settings: Boolean, downloads: Boolean subtitles: Set<SubtitleData>, settings: Boolean, downloads: Boolean
): SubtitleData? { ): SubtitleData? {
val langCode = preferredAutoSelectSubtitles ?: return null val langCode = preferredAutoSelectSubtitles ?: return null
val lang = SubtitleHelper.fromTwoLettersToLanguage(langCode) ?: return null val lang = fromTwoLettersToLanguage(langCode) ?: return null
if (downloads) { if (downloads) {
return subtitles.firstOrNull { sub -> return subtitles.firstOrNull { sub ->
(sub.origin == SubtitleOrigin.DOWNLOADED_FILE && sub.name == context?.getString( (sub.origin == SubtitleOrigin.DOWNLOADED_FILE && sub.name == context?.getString(
@ -970,22 +971,11 @@ class GeneratorPlayer : FullScreenPlayer() {
sortSubs(subtitles).firstOrNull { sub -> sortSubs(subtitles).firstOrNull { sub ->
val t = sub.name.replace(Regex("[^A-Za-z]"), " ").trim() val t = sub.name.replace(Regex("[^A-Za-z]"), " ").trim()
(settings || (downloads && sub.origin == SubtitleOrigin.DOWNLOADED_FILE)) && t == lang || t.startsWith( (settings) && t == lang || t.startsWith(lang) || t == langCode
"$lang "
) || t == langCode
}?.let { sub -> }?.let { sub ->
return 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 return null
} }
@ -1006,14 +996,12 @@ class GeneratorPlayer : FullScreenPlayer() {
getAutoSelectSubtitle( getAutoSelectSubtitle(
currentSubs, settings = true, downloads = false currentSubs, settings = true, downloads = false
)?.let { sub -> )?.let { sub ->
if (setSubtitles(sub)) { if (setSubtitles(sub)) {
player.saveData() player.saveData()
player.reloadPlayer(ctx) player.reloadPlayer(ctx)
player.handleEvent(CSPlayerEvent.Play) player.handleEvent(CSPlayerEvent.Play)
return true return true
} }
} }
} }
} }
@ -1304,8 +1292,10 @@ class GeneratorPlayer : FullScreenPlayer() {
Log.i("subfilter", "Filtering subtitle") Log.i("subfilter", "Filtering subtitle")
langFilterList.forEach { lang -> langFilterList.forEach { lang ->
Log.i("subfilter", "Lang: $lang") Log.i("subfilter", "Lang: $lang")
setOfSub += set.filter { it.name.contains(lang, ignoreCase = true) } setOfSub += set.filter {
.toMutableSet() it.name.contains(lang, ignoreCase = true) ||
it.origin != SubtitleOrigin.URL
}
} }
currentSubs = setOfSub currentSubs = setOfSub
} else { } else {
@ -1313,7 +1303,13 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
player.setActiveSubtitles(set) 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()
}
} }
} }
} }

View file

@ -113,8 +113,9 @@ class PlayerGeneratorViewModel : ViewModel() {
// Do not post if there's nothing new // Do not post if there's nothing new
// Posting will refresh subtitles which will in turn // Posting will refresh subtitles which will in turn
// make the subs to english if previously unselected // make the subs to english if previously unselected
if (allSubs != currentSubs) if (allSubs != currentSubs) {
_currentSubs.postValue(allSubs) _currentSubs.postValue(allSubs)
}
} }
private var currentJob: Job? = null private var currentJob: Job? = null
@ -164,9 +165,10 @@ class PlayerGeneratorViewModel : ViewModel() {
} }
_loadingLinks.postValue(loadingState) _loadingLinks.postValue(loadingState)
_currentLinks.postValue(currentLinks) _currentLinks.postValue(currentLinks)
_currentSubs.postValue(currentSubs) _currentSubs.postValue(
currentSubs.union(_currentSubs.value ?: emptySet())
)
} }
} }

View file

@ -28,7 +28,7 @@ enum class SubtitleOrigin {
/** /**
* @param name To be displayed in the player * @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 * @param headers if empty it will use the base onlineDataSource headers else only the specified headers
* */ * */
data class SubtitleData( data class SubtitleData(
@ -37,7 +37,13 @@ data class SubtitleData(
val origin: SubtitleOrigin, val origin: SubtitleOrigin,
val mimeType: String, val mimeType: String,
val headers: Map<String, 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 { class PlayerSubtitleHelper {
private var activeSubtitles: Set<SubtitleData> = emptySet() private var activeSubtitles: Set<SubtitleData> = emptySet()
@ -79,11 +85,11 @@ class PlayerSubtitleHelper {
} }
} }
fun subtitleStatus(sub : SubtitleData?): SubtitleStatus { fun subtitleStatus(sub: SubtitleData?): SubtitleStatus {
if(activeSubtitles.contains(sub)) { if (activeSubtitles.contains(sub)) {
return SubtitleStatus.IS_ACTIVE return SubtitleStatus.IS_ACTIVE
} }
if(allSubtitles.contains(sub)) { if (allSubtitles.contains(sub)) {
return SubtitleStatus.REQUIRES_RELOAD return SubtitleStatus.REQUIRES_RELOAD
} }
return SubtitleStatus.NOT_FOUND return SubtitleStatus.NOT_FOUND
@ -95,7 +101,7 @@ class PlayerSubtitleHelper {
regexSubtitlesToRemoveCaptions = style.removeCaptions regexSubtitlesToRemoveCaptions = style.removeCaptions
subtitleView?.context?.let { ctx -> subtitleView?.context?.let { ctx ->
subStyle = style subStyle = style
Log.i(TAG,"SET STYLE = $style") Log.i(TAG, "SET STYLE = $style")
subtitleView?.setStyle(ctx.fromSaveToStyle(style)) subtitleView?.setStyle(ctx.fromSaveToStyle(style))
subtitleView?.translationY = -style.elevation.toPx.toFloat() subtitleView?.translationY = -style.elevation.toPx.toFloat()
val size = style.fixedTextSize val size = style.fixedTextSize