forked from recloudstream/cloudstream
Added support for embedded subtitles
This commit is contained in:
parent
b4b06b2389
commit
2d0e75b921
8 changed files with 105 additions and 16 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue