forked from recloudstream/cloudstream
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
|
@ -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 🙏
|
||||
|
|
|
@ -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,12 +310,16 @@ 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
|
||||
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
|
||||
}
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
private fun Format.toAudioTrack(): AudioTrack {
|
||||
|
@ -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,40 +885,33 @@ 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 {
|
||||
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 (!it.isSubtitle() ||
|
||||
// Anything starting with - is not embedded
|
||||
it.language?.startsWith("-") == true ||
|
||||
it.language == null
|
||||
if (format.id == null ||
|
||||
format.language == null ||
|
||||
format.language?.startsWith("-") == true
|
||||
) return@mapNotNull null
|
||||
|
||||
return@mapNotNull SubtitleData(
|
||||
// Nicer looking displayed names
|
||||
fromTwoLettersToLanguage(it.language!!) ?: it.language!!,
|
||||
fromTwoLettersToLanguage(format.language!!)
|
||||
?: format.language!!,
|
||||
// See setPreferredTextLanguage
|
||||
it.language!!,
|
||||
format.id!!,
|
||||
SubtitleOrigin.EMBEDDED_IN_VIDEO,
|
||||
it.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP,
|
||||
format.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP,
|
||||
emptyMap()
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
// 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,9 +113,10 @@ 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
|
||||
private var currentStampJob: 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…
Reference in a new issue