Added support for embedded subtitles

This commit is contained in:
Blatzar 2022-05-20 20:25:56 +02:00
parent b4b06b2389
commit 2d0e75b921
8 changed files with 105 additions and 16 deletions

View file

@ -92,6 +92,10 @@ abstract class AbstractPlayerFragment(
throw NotImplementedError() throw NotImplementedError()
} }
open fun embeddedSubtitlesFetched(subtitles: List<SubtitleData>) {
throw NotImplementedError()
}
open fun exitedPipMode() { open fun exitedPipMode() {
throw NotImplementedError() throw NotImplementedError()
} }
@ -338,7 +342,9 @@ abstract class AbstractPlayerFragment(
PRELOAD_NEXT_EPISODE_PERCENTAGE, PRELOAD_NEXT_EPISODE_PERCENTAGE,
NEXT_WATCH_EPISODE_PERCENTAGE, NEXT_WATCH_EPISODE_PERCENTAGE,
UPDATE_SYNC_PROGRESS_PERCENTAGE, UPDATE_SYNC_PROGRESS_PERCENTAGE,
), subtitlesUpdates = ::subtitlesChanged ),
subtitlesUpdates = ::subtitlesChanged,
embeddedSubtitlesFetched = ::embeddedSubtitlesFetched,
) )
if (player is CS3IPlayer) { if (player is CS3IPlayer) {

View file

@ -27,9 +27,11 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromName
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.ExtractorUri
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import java.io.File import java.io.File
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
@ -95,6 +97,7 @@ class CS3IPlayer : IPlayer {
private var prevEpisode: (() -> Unit)? = null private var prevEpisode: (() -> Unit)? = null
private var playerUpdated: ((Any?) -> Unit)? = null private var playerUpdated: ((Any?) -> Unit)? = null
private var embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)? = null
override fun initCallbacks( override fun initCallbacks(
playerUpdated: (Any?) -> Unit, playerUpdated: (Any?) -> Unit,
@ -107,6 +110,7 @@ class CS3IPlayer : IPlayer {
nextEpisode: (() -> Unit)?, nextEpisode: (() -> Unit)?,
prevEpisode: (() -> Unit)?, prevEpisode: (() -> Unit)?,
subtitlesUpdates: (() -> Unit)?, subtitlesUpdates: (() -> Unit)?,
embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)?,
) { ) {
this.playerUpdated = playerUpdated this.playerUpdated = playerUpdated
this.updateIsPlaying = updateIsPlaying this.updateIsPlaying = updateIsPlaying
@ -118,6 +122,7 @@ class CS3IPlayer : IPlayer {
this.nextEpisode = nextEpisode this.nextEpisode = nextEpisode
this.prevEpisode = prevEpisode this.prevEpisode = prevEpisode
this.subtitlesUpdates = subtitlesUpdates this.subtitlesUpdates = subtitlesUpdates
this.embeddedSubtitlesFetched = embeddedSubtitlesFetched
} }
// I know, this is not a perfect solution, however it works for fixing subs // I know, this is not a perfect solution, however it works for fixing subs
@ -202,7 +207,14 @@ class CS3IPlayer : IPlayer {
trackSelector.setParameters( trackSelector.setParameters(
trackSelector.buildUponParameters() trackSelector.buildUponParameters()
.setPreferredTextLanguage("_$name") .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")
}
) )
// ugliest code I have written, it seeks 1ms to *update* the subtitles // ugliest code I have written, it seeks 1ms to *update* the subtitles
@ -231,16 +243,20 @@ class CS3IPlayer : IPlayer {
} }
override fun getSubtitleOffset(): Long { override fun getSubtitleOffset(): Long {
return currentSubtitleOffset//currentTextRenderer?.getRenderOffsetMs() ?: currentSubtitleOffset return currentSubtitleOffset //currentTextRenderer?.getRenderOffsetMs() ?: currentSubtitleOffset
} }
override fun getCurrentPreferredSubtitle(): SubtitleData? { override fun getCurrentPreferredSubtitle(): SubtitleData? {
return subtitleHelper.getAllSubtitles().firstOrNull { sub -> return subtitleHelper.getAllSubtitles().firstOrNull { sub ->
exoPlayerSelectedTracks.any { 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 - // The replace is needed as exoplayer translates _ to -
// Also we prefix the languages with _ // Also we prefix the languages with _
it.second && it.first.replace("-", "").equals( it.second && it.first.replace("-", "").equals(
sub.name.replace("-", ""), realName.replace("-", ""),
ignoreCase = true ignoreCase = true
) )
} }
@ -616,9 +632,46 @@ class CS3IPlayer : IPlayer {
* Records the current used subtitle/track. Needed as exoplayer seems to have loose track language selection. * Records the current used subtitle/track. Needed as exoplayer seems to have loose track language selection.
* */ * */
override fun onTracksInfoChanged(tracksInfo: TracksInfo) { override fun onTracksInfoChanged(tracksInfo: TracksInfo) {
fun Format.isSubtitle(): Boolean {
return this.sampleMimeType?.contains("video/") == false &&
this.sampleMimeType?.contains("audio/") == false
}
normalSafeApiCall {
exoPlayerSelectedTracks = exoPlayerSelectedTracks =
tracksInfo.trackGroupInfos.mapNotNull { it.trackGroup.getFormat(0).language?.let { lang -> lang to it.isSelected } } tracksInfo.trackGroupInfos.mapNotNull {
val format = it.trackGroup.getFormat(0)
if (format.isSubtitle())
format.language?.let { lang -> lang to it.isSelected }
else null
}
val exoPlayerReportedTracks = tracksInfo.trackGroupInfos.mapNotNull {
// Filter out unsupported tracks
if (it.isSupported)
it.trackGroup.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
)
}
embeddedSubtitlesFetched?.invoke(exoPlayerReportedTracks)
subtitlesUpdates?.invoke() subtitlesUpdates?.invoke()
}
super.onTracksInfoChanged(tracksInfo) super.onTracksInfoChanged(tracksInfo)
} }
@ -766,6 +819,15 @@ class CS3IPlayer : IPlayer {
// TODO // TODO
throw NotImplementedError() throw NotImplementedError()
} }
SubtitleOrigin.EMBEDDED_IN_VIDEO -> {
if (offlineSourceFactory != null) {
activeSubtitles.add(sub)
SingleSampleMediaSource.Factory(offlineSourceFactory)
.createMediaSource(subConfig, C.TIME_UNSET)
} else {
null
}
}
} }
} }
return Pair(subSources, activeSubtitles) return Pair(subSources, activeSubtitles)

View file

@ -40,7 +40,7 @@ class CustomDecoder : SubtitleDecoder {
) )
val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\d\s]*?[])}]\s*""")) val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\d\s]*?[])}]\s*"""))
fun trimStr(string: String) : String { fun trimStr(string: String): String {
return string.trimStart().trim('\uFEFF', '\u200B').replace( return string.trimStart().trim('\uFEFF', '\u200B').replace(
Regex("[\u00A0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u205F]"), Regex("[\u00A0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u205F]"),
" " " "
@ -109,10 +109,10 @@ class CustomDecoder : SubtitleDecoder {
captionRegex.forEach { rgx -> captionRegex.forEach { rgx ->
fullStr = fullStr.replace(rgx, "\n") fullStr = fullStr.replace(rgx, "\n")
} }
fullStr.replace(Regex("(\r\n|\r|\n){2,}"),"\n") fullStr.replace(Regex("(\r\n|\r|\n){2,}"), "\n")
buff.data = ByteBuffer.wrap(fullStr.toByteArray()) buff.data = ByteBuffer.wrap(fullStr.toByteArray())
} catch (e : Exception) { } catch (e: Exception) {
data.position(pos) data.position(pos)
buff.data = data buff.data = data
} }

View file

@ -82,6 +82,10 @@ class GeneratorPlayer : FullScreenPlayer() {
return player.setPreferredSubtitles(sub) return player.setPreferredSubtitles(sub)
} }
override fun embeddedSubtitlesFetched(subtitles: List<SubtitleData>) {
viewModel.addSubtitles(subtitles.toSet())
}
private fun noSubtitles(): Boolean { private fun noSubtitles(): Boolean {
return setSubtitles(null) return setSubtitles(null)
} }
@ -258,7 +262,7 @@ class GeneratorPlayer : FullScreenPlayer() {
var startSource = 0 var startSource = 0
val sortedUrls = sortLinks(useQualitySettings = false) val sortedUrls = sortLinks(useQualitySettings = false)
if (sortedUrls.isNullOrEmpty()) { if (sortedUrls.isEmpty()) {
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.isGone = true sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.isGone = true
} else { } else {
startSource = sortedUrls.indexOf(currentSelectedLink) startSource = sortedUrls.indexOf(currentSelectedLink)

View file

@ -85,6 +85,7 @@ interface IPlayer {
nextEpisode: (() -> Unit)? = null, // this is used by the player to load the next episode nextEpisode: (() -> Unit)? = null, // this is used by the player to load the next episode
prevEpisode: (() -> Unit)? = null, // this is used by the player to load the previous episode prevEpisode: (() -> Unit)? = null, // this is used by the player to load the previous episode
subtitlesUpdates: (() -> Unit)? = null, // callback from player to inform that subtitles have updated in some way subtitlesUpdates: (() -> Unit)? = null, // callback from player to inform that subtitles have updated in some way
embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)? = null, // callback from player to give all embedded subtitles
) )
fun updateSubtitleStyle(style: SaveCaptionStyle) fun updateSubtitleStyle(style: SaveCaptionStyle)

View file

@ -93,10 +93,18 @@ class PlayerGeneratorViewModel : ViewModel() {
} }
} }
/**
* If duplicate nothing will happen
* */
fun addSubtitles(file: Set<SubtitleData>) { fun addSubtitles(file: Set<SubtitleData>) {
val subs = (_currentSubs.value?.toMutableSet() ?: mutableSetOf()) val currentSubs = _currentSubs.value ?: emptySet()
subs.addAll(file) // Prevent duplicates
_currentSubs.postValue(subs) val allSubs = (currentSubs + file).distinct().toSet()
// 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)
_currentSubs.postValue(allSubs)
} }
private var currentJob: Job? = null private var currentJob: Job? = null

View file

@ -23,8 +23,13 @@ enum class SubtitleOrigin {
URL, URL,
DOWNLOADED_FILE, DOWNLOADED_FILE,
OPEN_SUBTITLES, OPEN_SUBTITLES,
EMBEDDED_IN_VIDEO
} }
/**
* @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
* */
data class SubtitleData( data class SubtitleData(
val name: String, val name: String,
val url: String, val url: String,
@ -77,6 +82,9 @@ class PlayerSubtitleHelper {
// TODO // TODO
throw NotImplementedError() throw NotImplementedError()
} }
SubtitleOrigin.EMBEDDED_IN_VIDEO -> {
throw NotImplementedError()
}
} }
} }

View file

@ -201,7 +201,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
putString(URL_BUNDLE, card.url) putString(URL_BUNDLE, card.url)
putString(API_NAME_BUNDLE, card.apiName) putString(API_NAME_BUNDLE, card.apiName)
if (card is DataStoreHelper.ResumeWatchingResult) { if (card is DataStoreHelper.ResumeWatchingResult) {
println("CARD::::: $card") // println("CARD::::: $card")
if (card.season != null) if (card.season != null)
putInt(SEASON_BUNDLE, card.season) putInt(SEASON_BUNDLE, card.season)
if (card.episode != null) if (card.episode != null)