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
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 🙏

View File

@ -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
}

View File

@ -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()
}
}
}
}

View File

@ -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())
)
}
}

View File

@ -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