Update media dependency

This commit is contained in:
CranberrySoup 2024-08-13 23:50:10 +02:00
parent 7936ccf5d3
commit 458ea29ec9
12 changed files with 235 additions and 683 deletions

View file

@ -191,15 +191,15 @@ dependencies {
implementation("dev.zacsweers.autoservice:auto-service-ksp:1.2.0") implementation("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
// Media 3 (ExoPlayer) // Media 3 (ExoPlayer)
implementation("androidx.media3:media3-ui:1.1.1") implementation("androidx.media3:media3-ui:1.4.0")
implementation("androidx.media3:media3-cast:1.1.1") implementation("androidx.media3:media3-cast:1.4.0")
implementation("androidx.media3:media3-common:1.1.1") implementation("androidx.media3:media3-common:1.4.0")
implementation("androidx.media3:media3-session:1.1.1") implementation("androidx.media3:media3-session:1.4.0")
implementation("androidx.media3:media3-exoplayer:1.1.1") implementation("androidx.media3:media3-exoplayer:1.4.0")
implementation("com.google.android.mediahome:video:1.0.0") implementation("com.google.android.mediahome:video:1.0.0")
implementation("androidx.media3:media3-exoplayer-hls:1.1.1") implementation("androidx.media3:media3-exoplayer-hls:1.4.0")
implementation("androidx.media3:media3-exoplayer-dash:1.1.1") implementation("androidx.media3:media3-exoplayer-dash:1.4.0")
implementation("androidx.media3:media3-datasource-okhttp:1.1.1") implementation("androidx.media3:media3-datasource-okhttp:1.4.0")
// PlayBack // PlayBack
implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker

View file

@ -25,7 +25,6 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import androidx.media3.ui.* import androidx.media3.ui.*

View file

@ -35,7 +35,6 @@ import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.SeekParameters import androidx.media3.exoplayer.SeekParameters
import androidx.media3.exoplayer.dash.DashMediaSource import androidx.media3.exoplayer.dash.DashMediaSource
@ -88,6 +87,7 @@ const val toleranceBeforeUs = 300_000L
* seek position, in microseconds. Must be non-negative. * seek position, in microseconds. Must be non-negative.
*/ */
const val toleranceAfterUs = 300_000L const val toleranceAfterUs = 300_000L
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
class CS3IPlayer : IPlayer { class CS3IPlayer : IPlayer {
private var isPlaying = false private var isPlaying = false
@ -147,7 +147,7 @@ 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 = id * String = id (without exoplayer track number)
* Boolean = if it's active * Boolean = if it's active
* */ * */
private var playerSelectedSubtitleTracks = listOf<Pair<String, Boolean>>() private var playerSelectedSubtitleTracks = listOf<Pair<String, Boolean>>()
@ -188,6 +188,10 @@ class CS3IPlayer : IPlayer {
} }
} }
fun String.stripTrackId(): String {
return this.replace(Regex("""^\d+:"""), "")
}
fun initSubtitles(subView: SubtitleView?, subHolder: FrameLayout?, style: SaveCaptionStyle?) { fun initSubtitles(subView: SubtitleView?, subHolder: FrameLayout?, style: SaveCaptionStyle?) {
subtitleHelper.initSubtitles(subView, subHolder, style) subtitleHelper.initSubtitles(subView, subHolder, style)
} }
@ -272,7 +276,11 @@ class CS3IPlayer : IPlayer {
return this.firstNotNullOfOrNull { group -> return this.firstNotNullOfOrNull { group ->
(0 until group.mediaTrackGroup.length).map { (0 until group.mediaTrackGroup.length).map {
group.getTrackFormat(it) to it group.getTrackFormat(it) to it
}.firstOrNull { it.first.id == id } }.firstOrNull {
// The format id system is "trackNumber:trackID"
// The track number is not generated by us so we filter it out
it.first.id?.stripTrackId() == id
}
?.let { group.mediaTrackGroup to it.second } ?.let { group.mediaTrackGroup to it.second }
} }
} }
@ -355,21 +363,28 @@ class CS3IPlayer : IPlayer {
private fun Format.toAudioTrack(): AudioTrack { private fun Format.toAudioTrack(): AudioTrack {
return AudioTrack( return AudioTrack(
this.id, this.id?.stripTrackId(),
this.label, this.label,
// isPlaying,
this.language this.language
) )
} }
private fun Format.toSubtitleTrack(): TextTrack {
return TextTrack(
this.id?.stripTrackId(),
this.label,
this.language,
this.sampleMimeType
)
}
private fun Format.toVideoTrack(): VideoTrack { private fun Format.toVideoTrack(): VideoTrack {
return VideoTrack( return VideoTrack(
this.id, this.id?.stripTrackId(),
this.label, this.label,
// isPlaying,
this.language, this.language,
this.width, this.width,
this.height this.height,
) )
} }
@ -381,11 +396,20 @@ class CS3IPlayer : IPlayer {
val audioTracks = allTracks.filter { it.type == TRACK_TYPE_AUDIO }.getFormats() val audioTracks = allTracks.filter { it.type == TRACK_TYPE_AUDIO }.getFormats()
.map { it.first.toAudioTrack() } .map { it.first.toAudioTrack() }
val textTracks = allTracks.filter { it.type == TRACK_TYPE_TEXT }.getFormats()
.map { it.first.toSubtitleTrack() }
val currentTextTracks = textTracks.filter { track ->
playerSelectedSubtitleTracks.any { it.second && it.first == track.id }
}
return CurrentTracks( return CurrentTracks(
exoPlayer?.videoFormat?.toVideoTrack(), exoPlayer?.videoFormat?.toVideoTrack(),
exoPlayer?.audioFormat?.toAudioTrack(), exoPlayer?.audioFormat?.toAudioTrack(),
currentTextTracks,
videoTracks, videoTracks,
audioTracks audioTracks,
textTracks
) )
} }
@ -455,7 +479,8 @@ class CS3IPlayer : IPlayer {
override fun setSubtitleOffset(offset: Long) { override fun setSubtitleOffset(offset: Long) {
currentSubtitleOffset = offset currentSubtitleOffset = offset
currentTextRenderer?.setRenderOffsetMs(offset) CustomDecoder.subtitleOffset = offset
currentTextRenderer?.reset()
} }
override fun getSubtitleOffset(): Long { override fun getSubtitleOffset(): Long {
@ -503,7 +528,6 @@ class CS3IPlayer : IPlayer {
release() release()
} }
//simpleCache?.release() //simpleCache?.release()
currentTextRenderer = null
exoPlayer = null exoPlayer = null
//simpleCache = null //simpleCache = null
@ -601,7 +625,10 @@ class CS3IPlayer : IPlayer {
} }
private fun Context.createOfflineSource(): DataSource.Factory { private fun Context.createOfflineSource(): DataSource.Factory {
return DefaultDataSource.Factory(this, DefaultHttpDataSource.Factory().setUserAgent(USER_AGENT)) return DefaultDataSource.Factory(
this,
DefaultHttpDataSource.Factory().setUserAgent(USER_AGENT)
)
} }
private fun getCache(context: Context, cacheSize: Long): SimpleCache? { private fun getCache(context: Context, cacheSize: Long): SimpleCache? {
@ -646,7 +673,8 @@ class CS3IPlayer : IPlayer {
return trackSelector return trackSelector
} }
var currentTextRenderer: CustomTextRenderer? = null private var currentSubtitleDecoder: CustomSubtitleDecoderFactory? = null
private var currentTextRenderer: TextRenderer? = null
private fun buildExoPlayer( private fun buildExoPlayer(
context: Context, context: Context,
@ -672,8 +700,8 @@ class CS3IPlayer : IPlayer {
.setRenderersFactory { eventHandler, videoRendererEventListener, audioRendererEventListener, textRendererOutput, metadataRendererOutput -> .setRenderersFactory { eventHandler, videoRendererEventListener, audioRendererEventListener, textRendererOutput, metadataRendererOutput ->
DefaultRenderersFactory(context).apply { DefaultRenderersFactory(context).apply {
setEnableDecoderFallback(true) setEnableDecoderFallback(true)
// Enable Ffmpeg extension // Enable Ffmpeg extension.
setExtensionRendererMode(EXTENSION_RENDERER_MODE_ON) setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER)
}.createRenderers( }.createRenderers(
eventHandler, eventHandler,
videoRendererEventListener, videoRendererEventListener,
@ -682,14 +710,23 @@ class CS3IPlayer : IPlayer {
metadataRendererOutput metadataRendererOutput
).map { ).map {
if (it is TextRenderer) { if (it is TextRenderer) {
val currentTextRenderer = CustomTextRenderer( CustomDecoder.subtitleOffset = subtitleOffset
subtitleOffset, val decoder = CustomSubtitleDecoderFactory()
val currentTextRenderer = TextRenderer(
textRendererOutput, textRendererOutput,
eventHandler.looper, eventHandler.looper,
CustomSubtitleDecoderFactory() decoder
).also { renderer -> this.currentTextRenderer = renderer } ).apply {
// Required to make the decoder work with old subtitles
// Upgrade CustomSubtitleDecoderFactory when media3 supports it
experimentalSetLegacyDecodingEnabled(true)
}.also { renderer ->
this.currentTextRenderer = renderer
this.currentSubtitleDecoder = decoder
}
currentTextRenderer currentTextRenderer
} else it } else
it
}.toTypedArray() }.toTypedArray()
} }
.setTrackSelector( .setTrackSelector(
@ -952,7 +989,8 @@ class CS3IPlayer : IPlayer {
playerSelectedSubtitleTracks = playerSelectedSubtitleTracks =
textTracks.map { group -> textTracks.map { group ->
group.getFormats().mapNotNull { (format, _) -> group.getFormats().mapNotNull { (format, _) ->
(format.id ?: return@mapNotNull null) to group.isSelected (format.id?.stripTrackId()
?: return@mapNotNull null) to group.isSelected
} }
}.flatten() }.flatten()
@ -970,7 +1008,7 @@ class CS3IPlayer : IPlayer {
fromTwoLettersToLanguage(format.language!!) fromTwoLettersToLanguage(format.language!!)
?: format.language!!, ?: format.language!!,
// See setPreferredTextLanguage // See setPreferredTextLanguage
format.id!!, format.id!!.stripTrackId(),
SubtitleOrigin.EMBEDDED_IN_VIDEO, SubtitleOrigin.EMBEDDED_IN_VIDEO,
format.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP, format.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP,
emptyMap(), emptyMap(),
@ -1210,7 +1248,7 @@ class CS3IPlayer : IPlayer {
): Pair<List<SingleSampleMediaSource>, List<SubtitleData>> { ): Pair<List<SingleSampleMediaSource>, List<SubtitleData>> {
val activeSubtitles = ArrayList<SubtitleData>() val activeSubtitles = ArrayList<SubtitleData>()
val subSources = subHelper.getAllSubtitles().mapNotNull { sub -> val subSources = subHelper.getAllSubtitles().mapNotNull { sub ->
val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url)) val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.getFixedUrl()))
.setMimeType(sub.mimeType) .setMimeType(sub.mimeType)
.setLanguage("_${sub.name}") .setLanguage("_${sub.name}")
.setId(sub.getId()) .setId(sub.getId())

View file

@ -3,37 +3,36 @@ package com.lagradost.cloudstream3.ui.player
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.preference.PreferenceManager
import androidx.media3.common.Format import androidx.media3.common.Format
import androidx.media3.common.MimeTypes import androidx.media3.common.MimeTypes
import androidx.media3.common.util.Consumer
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.text.ExoplayerCuesDecoder
import androidx.media3.exoplayer.text.SubtitleDecoderFactory import androidx.media3.exoplayer.text.SubtitleDecoderFactory
import androidx.media3.extractor.text.CuesWithTiming
import androidx.media3.extractor.text.SimpleSubtitleDecoder
import androidx.media3.extractor.text.Subtitle
import androidx.media3.extractor.text.SubtitleDecoder import androidx.media3.extractor.text.SubtitleDecoder
import androidx.media3.extractor.text.SubtitleInputBuffer import androidx.media3.extractor.text.SubtitleParser
import androidx.media3.extractor.text.SubtitleOutputBuffer import androidx.media3.extractor.text.dvb.DvbParser
import androidx.media3.extractor.text.cea.Cea608Decoder import androidx.media3.extractor.text.pgs.PgsParser
import androidx.media3.extractor.text.cea.Cea708Decoder import androidx.media3.extractor.text.ssa.SsaParser
import androidx.media3.extractor.text.dvb.DvbDecoder import androidx.media3.extractor.text.subrip.SubripParser
import androidx.media3.extractor.text.pgs.PgsDecoder import androidx.media3.extractor.text.ttml.TtmlParser
import androidx.media3.extractor.text.ssa.SsaDecoder import androidx.media3.extractor.text.tx3g.Tx3gParser
import androidx.media3.extractor.text.subrip.SubripDecoder import androidx.media3.extractor.text.webvtt.Mp4WebvttParser
import androidx.media3.extractor.text.ttml.TtmlDecoder import androidx.media3.extractor.text.webvtt.WebvttParser
import androidx.media3.extractor.text.tx3g.Tx3gDecoder import androidx.preference.PreferenceManager
import androidx.media3.extractor.text.webvtt.Mp4WebvttDecoder
import androidx.media3.extractor.text.webvtt.WebvttDecoder
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import org.mozilla.universalchardet.UniversalDetector import org.mozilla.universalchardet.UniversalDetector
import java.nio.ByteBuffer
import java.nio.charset.Charset import java.nio.charset.Charset
/** /**
* @param fallbackFormat used to create a decoder based on mimetype if the subtitle string is not * @param fallbackFormat used to create a decoder based on mimetype if the subtitle string is not
* enough to identify the subtitle format. * enough to identify the subtitle format.
**/ **/
@OptIn(UnstableApi::class) @UnstableApi
class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { class CustomDecoder(private val fallbackFormat: Format?) : SubtitleParser {
companion object { companion object {
fun updateForcedEncoding(context: Context) { fun updateForcedEncoding(context: Context) {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
@ -48,6 +47,8 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
} }
} }
/** Subtitle offset in milliseconds */
var subtitleOffset: Long = 0
private const val UTF_8 = "UTF-8" private const val UTF_8 = "UTF-8"
private const val TAG = "CustomDecoder" private const val TAG = "CustomDecoder"
private var overrideEncoding: String? = null private var overrideEncoding: String? = null
@ -85,16 +86,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
} }
} }
private var realDecoder: SubtitleDecoder? = null private var realDecoder: SubtitleParser? = null
override fun getName(): String {
return realDecoder?.name ?: this::javaClass.name
}
override fun dequeueInputBuffer(): SubtitleInputBuffer {
Log.i(TAG, "dequeueInputBuffer")
return realDecoder?.dequeueInputBuffer() ?: SubtitleInputBuffer()
}
private fun getStr(byteArray: ByteArray): Pair<String, Charset> { private fun getStr(byteArray: ByteArray): Pair<String, Charset> {
val encoding = try { val encoding = try {
@ -128,101 +120,76 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
} }
} }
private fun getStr(input: SubtitleInputBuffer): String? { private fun getSubtitleParser(data: String): SubtitleParser? {
try { // this way we read the subtitle file and decide what decoder to use instead of relying fully on mimetype
val data = input.data ?: return null //https://github.com/LagradOst/CloudStream-2/blob/ddd774ee66810137ff7bd65dae70bcf3ba2d2489/CloudStreamForms/CloudStreamForms/Script/MainChrome.cs#L388
data.position(0) val subtitleParser = when {
val fullDataArr = ByteArray(data.remaining()) // "WEBVTT" can be hidden behind invisible characters not filtered by trim
data.get(fullDataArr) data.substring(0, 10).contains("WEBVTT", ignoreCase = true) -> WebvttParser()
return trimStr(getStr(fullDataArr).first) data.startsWith("<?xml version=\"", ignoreCase = true) -> TtmlParser()
} catch (e: Exception) { (data.startsWith(
Log.e(TAG, "Failed to parse text returning plain data") "[Script Info]",
logError(e) ignoreCase = true
return null ) || data.startsWith(
} "Title:",
} ignoreCase = true
)) -> SsaParser(fallbackFormat?.initializationData)
private fun SubtitleInputBuffer.setSubtitleText(text: String) { data.startsWith("1", ignoreCase = true) -> SubripParser()
// println("Set subtitle text -----\n$text\n-----") fallbackFormat != null -> {
this.data = ByteBuffer.wrap(text.toByteArray(charset(UTF_8))) when (val mimeType = fallbackFormat.sampleMimeType) {
} MimeTypes.TEXT_VTT -> WebvttParser()
MimeTypes.TEXT_SSA -> SsaParser(fallbackFormat.initializationData)
override fun queueInputBuffer(inputBuffer: SubtitleInputBuffer) { MimeTypes.APPLICATION_MP4VTT -> Mp4WebvttParser()
Log.i(TAG, "queueInputBuffer") MimeTypes.APPLICATION_TTML -> TtmlParser()
try { MimeTypes.APPLICATION_SUBRIP -> SubripParser()
val inputString = getStr(inputBuffer) MimeTypes.APPLICATION_TX3G -> Tx3gParser(fallbackFormat.initializationData)
if (realDecoder == null && !inputString.isNullOrBlank()) { // These decoders are not converted to parsers yet
var str: String = inputString // TODO
// this way we read the subtitle file and decide what decoder to use instead of relying fully on mimetype // MimeTypes.APPLICATION_CEA608, MimeTypes.APPLICATION_MP4CEA608 -> Cea608Decoder(
Log.i(TAG, "Got data from queueInputBuffer") // mimeType,
//https://github.com/LagradOst/CloudStream-2/blob/ddd774ee66810137ff7bd65dae70bcf3ba2d2489/CloudStreamForms/CloudStreamForms/Script/MainChrome.cs#L388 // fallbackFormat.accessibilityChannel,
realDecoder = when { // Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS
str.startsWith("WEBVTT", ignoreCase = true) -> WebvttDecoder() // )
str.startsWith("<?xml version=\"", ignoreCase = true) -> TtmlDecoder() // MimeTypes.APPLICATION_CEA708 -> Cea708Decoder(
(str.startsWith( // fallbackFormat.accessibilityChannel,
"[Script Info]", // fallbackFormat.initializationData
ignoreCase = true // )
) || str.startsWith("Title:", ignoreCase = true)) -> SsaDecoder(fallbackFormat?.initializationData) MimeTypes.APPLICATION_DVBSUBS -> DvbParser(fallbackFormat.initializationData)
str.startsWith("1", ignoreCase = true) -> SubripDecoder() MimeTypes.APPLICATION_PGS -> PgsParser()
fallbackFormat != null -> {
when (val mimeType = fallbackFormat.sampleMimeType) {
MimeTypes.TEXT_VTT -> WebvttDecoder()
MimeTypes.TEXT_SSA -> SsaDecoder(fallbackFormat.initializationData)
MimeTypes.APPLICATION_MP4VTT -> Mp4WebvttDecoder()
MimeTypes.APPLICATION_TTML -> TtmlDecoder()
MimeTypes.APPLICATION_SUBRIP -> SubripDecoder()
MimeTypes.APPLICATION_TX3G -> Tx3gDecoder(fallbackFormat.initializationData)
MimeTypes.APPLICATION_CEA608, MimeTypes.APPLICATION_MP4CEA608 -> Cea608Decoder(
mimeType,
fallbackFormat.accessibilityChannel,
Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS
)
MimeTypes.APPLICATION_CEA708 -> Cea708Decoder(
fallbackFormat.accessibilityChannel,
fallbackFormat.initializationData
)
MimeTypes.APPLICATION_DVBSUBS -> DvbDecoder(fallbackFormat.initializationData)
MimeTypes.APPLICATION_PGS -> PgsDecoder()
MimeTypes.TEXT_EXOPLAYER_CUES -> ExoplayerCuesDecoder()
else -> null
}
}
else -> null else -> null
} }
}
else -> null
}
return subtitleParser
}
override fun parse(
data: ByteArray,
offset: Int,
length: Int,
outputOptions: SubtitleParser.OutputOptions,
output: Consumer<CuesWithTiming>
) {
val customOutput = Consumer<CuesWithTiming> { o ->
val updatedCues = CuesWithTiming(o.cues, o.startTimeUs - subtitleOffset.times(1000), o.durationUs)
output.accept(updatedCues)
}
Log.i(TAG, "Parse subtitle, current parser: $realDecoder")
try {
val inputString = getStr(data).first
Log.i(TAG, "Subtitle preview: ${inputString.substring(0, 30)}")
if (inputString.isNotBlank()) {
var str: String = trimStr(inputString)
realDecoder = realDecoder ?: getSubtitleParser(inputString)
Log.i( Log.i(
TAG, TAG,
"Decoder selected: $realDecoder" "Parser selected: $realDecoder"
) )
realDecoder?.let { decoder -> realDecoder?.let { decoder ->
decoder.dequeueInputBuffer()?.let { buff -> if (decoder !is SsaParser) {
if (decoder !is SsaDecoder) {
if (regexSubtitlesToRemoveCaptions)
captionRegex.forEach { rgx ->
str = str.replace(rgx, "\n")
}
if (regexSubtitlesToRemoveBloat)
bloatRegex.forEach { rgx ->
str = str.replace(rgx, "\n")
}
}
buff.setSubtitleText(str)
decoder.queueInputBuffer(buff)
Log.i(
TAG,
"Decoder queueInputBuffer successfully"
)
}
CS3IPlayer.requestSubtitleUpdate?.invoke()
}
} else {
Log.i(
TAG,
"Decoder else queueInputBuffer successfully"
)
if (!inputString.isNullOrBlank()) {
var str: String = inputString
if (realDecoder !is SsaDecoder) {
if (regexSubtitlesToRemoveCaptions) if (regexSubtitlesToRemoveCaptions)
captionRegex.forEach { rgx -> captionRegex.forEach { rgx ->
str = str.replace(rgx, "\n") str = str.replace(rgx, "\n")
@ -235,38 +202,31 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
str = str.uppercase() str = str.uppercase()
} }
} }
inputBuffer.setSubtitleText(str)
} }
val array = str.toByteArray()
realDecoder?.queueInputBuffer(inputBuffer) realDecoder?.parse(
array,
minOf(array.size, offset),
minOf(array.size, length),
outputOptions,
customOutput
)
} }
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} }
} }
override fun dequeueOutputBuffer(): SubtitleOutputBuffer? { override fun getCueReplacementBehavior(): Int {
return realDecoder?.dequeueOutputBuffer() return realDecoder?.cueReplacementBehavior ?: Format.CUE_REPLACEMENT_BEHAVIOR_REPLACE
}
override fun flush() {
realDecoder?.flush()
}
override fun release() {
realDecoder?.release()
}
override fun setPositionUs(positionUs: Long) {
realDecoder?.setPositionUs(positionUs)
} }
} }
/** See https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java */ /** See https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java */
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
class CustomSubtitleDecoderFactory : SubtitleDecoderFactory { class CustomSubtitleDecoderFactory : SubtitleDecoderFactory {
override fun supportsFormat(format: Format): Boolean { override fun supportsFormat(format: Format): Boolean {
// return SubtitleDecoderFactory.DEFAULT.supportsFormat(format)
return listOf( return listOf(
MimeTypes.TEXT_VTT, MimeTypes.TEXT_VTT,
MimeTypes.TEXT_SSA, MimeTypes.TEXT_SSA,
@ -283,7 +243,28 @@ class CustomSubtitleDecoderFactory : SubtitleDecoderFactory {
).contains(format.sampleMimeType) ).contains(format.sampleMimeType)
} }
/**
* Decoders created here persists across reset()
* Do not save state in the decoder which you want to reset (e.g subtitle offset)
**/
override fun createDecoder(format: Format): SubtitleDecoder { override fun createDecoder(format: Format): SubtitleDecoder {
return CustomDecoder(format) val parser = CustomDecoder(format)
return DelegatingSubtitleDecoder(
parser::class.simpleName + "Decoder", parser
)
} }
} }
@OptIn(UnstableApi::class)
/** We need to convert the newer SubtitleParser to an older SubtitleDecoder */
class DelegatingSubtitleDecoder(name: String, private val parser: SubtitleParser) :
SimpleSubtitleDecoder(name) {
override fun decode(data: ByteArray, length: Int, reset: Boolean): Subtitle {
if (reset) {
parser.reset()
}
return parser.parseToLegacySubtitle(data, 0, length);
}
}

View file

@ -1,33 +0,0 @@
package com.lagradost.cloudstream3.ui.player
import android.os.Looper
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.text.SubtitleDecoderFactory
import androidx.media3.exoplayer.text.TextOutput
@OptIn(UnstableApi::class)
class CustomTextRenderer(
offset: Long,
output: TextOutput?,
outputLooper: Looper?,
decoderFactory: SubtitleDecoderFactory = SubtitleDecoderFactory.DEFAULT
) : NonFinalTextRenderer(output, outputLooper, decoderFactory) {
private var offsetPositionUs: Long = 0L
init {
setRenderOffsetMs(offset)
}
fun setRenderOffsetMs(offset : Long) {
offsetPositionUs = offset * 1000L
}
fun getRenderOffsetMs() : Long {
return offsetPositionUs / 1000L
}
override fun render( positionUs: Long, elapsedRealtimeUs: Long) {
super.render(positionUs + offsetPositionUs, elapsedRealtimeUs + offsetPositionUs)
}
}

View file

@ -36,6 +36,7 @@ import androidx.core.view.isGone
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
@ -296,8 +297,12 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
} }
override fun subtitlesChanged() { override fun subtitlesChanged() {
playerBinding?.playerSubtitleOffsetBtt?.isGone = val tracks = player.getVideoTracks()
player.getCurrentPreferredSubtitle() == null val isBuiltinSubtitles = tracks.currentTextTracks.all { track ->
track.mimeType == MimeTypes.APPLICATION_MEDIA3_CUES
}
// Subtitle offset is not possible on built-in media3 tracks
playerBinding?.playerSubtitleOffsetBtt?.isGone = isBuiltinSubtitles || tracks.currentTextTracks.isEmpty()
} }
private fun restoreOrientationWithSensor(activity: Activity) { private fun restoreOrientationWithSensor(activity: Activity) {
@ -569,7 +574,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
playerRewHolder.alpha = 1f playerRewHolder.alpha = 1f
val rotateLeft = AnimationUtils.loadAnimation(context, R.anim.rotate_left) val rotateLeft = AnimationUtils.loadAnimation(context, R.anim.rotate_left)
exoRew.startAnimation(rotateLeft) playerRew.startAnimation(rotateLeft)
val goLeft = AnimationUtils.loadAnimation(context, R.anim.go_left) val goLeft = AnimationUtils.loadAnimation(context, R.anim.go_left)
goLeft.setAnimationListener(object : Animation.AnimationListener { goLeft.setAnimationListener(object : Animation.AnimationListener {
@ -602,7 +607,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
playerFfwdHolder.alpha = 1f playerFfwdHolder.alpha = 1f
val rotateRight = AnimationUtils.loadAnimation(context, R.anim.rotate_right) val rotateRight = AnimationUtils.loadAnimation(context, R.anim.rotate_right)
exoFfwd.startAnimation(rotateRight) playerFfwd.startAnimation(rotateRight)
val goRight = AnimationUtils.loadAnimation(context, R.anim.go_right) val goRight = AnimationUtils.loadAnimation(context, R.anim.go_right)
goRight.setAnimationListener(object : Animation.AnimationListener { goRight.setAnimationListener(object : Animation.AnimationListener {
@ -1535,12 +1540,12 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
showSubtitleOffsetDialog() showSubtitleOffsetDialog()
} }
exoRew.setOnClickListener { playerRew.setOnClickListener {
autoHide() autoHide()
rewind() rewind()
} }
exoFfwd.setOnClickListener { playerFfwd.setOnClickListener {
autoHide() autoHide()
fastForward() fastForward()
} }

View file

@ -168,15 +168,12 @@ interface Track {
**/ **/
val id: String? val id: String?
val label: String? val label: String?
// val isCurrentlyPlaying: Boolean
val language: String? val language: String?
} }
data class VideoTrack( data class VideoTrack(
override val id: String?, override val id: String?,
override val label: String?, override val label: String?,
// override val isCurrentlyPlaying: Boolean,
override val language: String?, override val language: String?,
val width: Int?, val width: Int?,
val height: Int?, val height: Int?,
@ -185,15 +182,24 @@ data class VideoTrack(
data class AudioTrack( data class AudioTrack(
override val id: String?, override val id: String?,
override val label: String?, override val label: String?,
// override val isCurrentlyPlaying: Boolean,
override val language: String?, override val language: String?,
) : Track ) : Track
data class TextTrack(
override val id: String?,
override val label: String?,
override val language: String?,
val mimeType: String?,
) : Track
data class CurrentTracks( data class CurrentTracks(
val currentVideoTrack: VideoTrack?, val currentVideoTrack: VideoTrack?,
val currentAudioTrack: AudioTrack?, val currentAudioTrack: AudioTrack?,
val currentTextTracks: List<TextTrack>,
val allVideoTracks: List<VideoTrack>, val allVideoTracks: List<VideoTrack>,
val allAudioTracks: List<AudioTrack>, val allAudioTracks: List<AudioTrack>,
val allTextTracks: List<TextTrack>,
) )
class InvalidFileException(msg: String) : Exception(msg) class InvalidFileException(msg: String) : Exception(msg)

View file

@ -1,457 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lagradost.cloudstream3.ui.player;
import static androidx.media3.common.text.Cue.DIMEN_UNSET;
import static androidx.media3.common.text.Cue.LINE_TYPE_NUMBER;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.BaseRenderer;
import androidx.media3.exoplayer.FormatHolder;
import androidx.media3.exoplayer.RendererCapabilities;
import androidx.media3.exoplayer.source.SampleStream;
import androidx.media3.exoplayer.text.SubtitleDecoderFactory;
import androidx.media3.exoplayer.text.TextOutput;
import androidx.media3.extractor.text.Subtitle;
import androidx.media3.extractor.text.SubtitleDecoder;
import androidx.media3.extractor.text.SubtitleDecoderException;
import androidx.media3.extractor.text.SubtitleInputBuffer;
import androidx.media3.extractor.text.SubtitleOutputBuffer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
// DO NOT CONVERT TO KOTLIN AUTOMATICALLY, IT FUCKS UP AND DOES NOT DISPLAY SUBS FOR SOME REASON
// IF YOU CHANGE THE CODE MAKE SURE YOU GET THE CUES CORRECT!
/**
* A renderer for text.
*
* <p>{@link Subtitle}s are decoded from sample data using {@link SubtitleDecoder} instances
* obtained from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s
* is delegated to a {@link TextOutput}.
*/
@OptIn(markerClass = UnstableApi.class)
public class NonFinalTextRenderer extends BaseRenderer implements Callback {
private static final String TAG = "TextRenderer";
/**
* @param trackType The track type that the renderer handles. One of the {@link C} {@code
* TRACK_TYPE_*} constants.
* @param outputHandler todo description
*/
public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) {
super(trackType);
this.outputHandler = outputHandler;
}
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
REPLACEMENT_STATE_NONE,
REPLACEMENT_STATE_SIGNAL_END_OF_STREAM,
REPLACEMENT_STATE_WAIT_END_OF_STREAM
})
private @interface ReplacementState {
}
/**
* The decoder does not need to be replaced.
*/
private static final int REPLACEMENT_STATE_NONE = 0;
/**
* The decoder needs to be replaced, but we haven't yet signaled an end of stream to the existing
* decoder. We need to do so in order to ensure that it outputs any remaining buffers before we
* release it.
*/
private static final int REPLACEMENT_STATE_SIGNAL_END_OF_STREAM = 1;
/**
* The decoder needs to be replaced, and we've signaled an end of stream to the existing decoder.
* We're waiting for the decoder to output an end of stream signal to indicate that it has output
* any remaining buffers before we release it.
*/
private static final int REPLACEMENT_STATE_WAIT_END_OF_STREAM = 2;
private static final int MSG_UPDATE_OUTPUT = 0;
@Nullable
private final Handler outputHandler;
private TextOutput output = null;
private SubtitleDecoderFactory decoderFactory = null;
private FormatHolder formatHolder = null;
private boolean inputStreamEnded;
private boolean outputStreamEnded;
private boolean waitingForKeyFrame;
private @ReplacementState int decoderReplacementState;
@Nullable
private Format streamFormat;
@Nullable
private SubtitleDecoder decoder;
@Nullable
private SubtitleInputBuffer nextInputBuffer;
@Nullable
private SubtitleOutputBuffer subtitle;
@Nullable
private SubtitleOutputBuffer nextSubtitle;
private int nextSubtitleEventIndex;
private long finalStreamEndPositionUs;
/**
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
* looper associated with the application's main thread, which can be obtained using {@link
* android.app.Activity#getMainLooper()}. Null may be passed if the output should be called
* directly on the player's internal rendering thread.
*/
public NonFinalTextRenderer(TextOutput output, @Nullable Looper outputLooper) {
this(output, outputLooper, SubtitleDecoderFactory.DEFAULT);
}
/**
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
* looper associated with the application's main thread, which can be obtained using {@link
* android.app.Activity#getMainLooper()}. Null may be passed if the output should be called
* directly on the player's internal rendering thread.
* @param decoderFactory A factory from which to obtain {@link SubtitleDecoder} instances.
*/
public NonFinalTextRenderer(
TextOutput output, @Nullable Looper outputLooper, SubtitleDecoderFactory decoderFactory) {
super(C.TRACK_TYPE_TEXT);
this.output = checkNotNull(output);
this.outputHandler =
outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this);
this.decoderFactory = decoderFactory;
formatHolder = new FormatHolder();
finalStreamEndPositionUs = C.TIME_UNSET;
}
@NonNull
@Override
public String getName() {
return TAG;
}
@Override
public @Capabilities int supportsFormat(@NonNull Format format) {
if (decoderFactory.supportsFormat(format)) {
return RendererCapabilities.create(
format.cryptoType == C.CRYPTO_TYPE_NONE ? C.FORMAT_HANDLED : C.FORMAT_UNSUPPORTED_DRM);
} else if (MimeTypes.isText(format.sampleMimeType)) {
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE);
} else {
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE);
}
}
/**
* Sets the position at which to stop rendering the current stream.
*
* <p>Must be called after {@link #setCurrentStreamFinal()}.
*
* @param streamEndPositionUs The position to stop rendering at or {@link C#LENGTH_UNSET} to
* render until the end of the current stream.
*/
// TODO(internal b/181312195): Remove this when it's no longer needed once subtitles are decoded
// on the loading side of SampleQueue.
public void setFinalStreamEndPositionUs(long streamEndPositionUs) {
checkState(isCurrentStreamFinal());
this.finalStreamEndPositionUs = streamEndPositionUs;
}
@Override
protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) {
streamFormat = formats[0];
if (decoder != null) {
decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM;
} else {
initDecoder();
}
}
@Override
protected void onPositionReset(long positionUs, boolean joining) {
clearOutput();
inputStreamEnded = false;
outputStreamEnded = false;
finalStreamEndPositionUs = C.TIME_UNSET;
if (decoderReplacementState != REPLACEMENT_STATE_NONE) {
replaceDecoder();
} else {
releaseBuffers();
checkNotNull(decoder).flush();
}
}
@Override
public void render(long positionUs, long elapsedRealtimeUs) {
if (isCurrentStreamFinal()
&& finalStreamEndPositionUs != C.TIME_UNSET
&& positionUs >= finalStreamEndPositionUs) {
releaseBuffers();
outputStreamEnded = true;
}
if (outputStreamEnded) {
return;
}
if (nextSubtitle == null) {
checkNotNull(decoder).setPositionUs(positionUs);
try {
nextSubtitle = checkNotNull(decoder).dequeueOutputBuffer();
} catch (SubtitleDecoderException e) {
handleDecoderError(e);
return;
}
}
if (getState() != STATE_STARTED) {
return;
}
boolean textRendererNeedsUpdate = false;
if (subtitle != null) {
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we
// advance to the next event.
long subtitleNextEventTimeUs = getNextEventTime();
while (subtitleNextEventTimeUs <= positionUs) {
nextSubtitleEventIndex++;
subtitleNextEventTimeUs = getNextEventTime();
textRendererNeedsUpdate = true;
}
}
if (nextSubtitle != null) {
SubtitleOutputBuffer nextSubtitle = this.nextSubtitle;
if (nextSubtitle.isEndOfStream()) {
if (!textRendererNeedsUpdate && getNextEventTime() == Long.MAX_VALUE) {
if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
replaceDecoder();
} else {
releaseBuffers();
outputStreamEnded = true;
}
}
} else if (nextSubtitle.timeUs <= positionUs) {
// Advance to the next subtitle. Sync the next event index and trigger an update.
if (subtitle != null) {
subtitle.release();
}
nextSubtitleEventIndex = nextSubtitle.getNextEventTimeIndex(positionUs);
subtitle = nextSubtitle;
this.nextSubtitle = null;
textRendererNeedsUpdate = true;
}
}
if (textRendererNeedsUpdate) {
// If textRendererNeedsUpdate then subtitle must be non-null.
checkNotNull(subtitle);
// textRendererNeedsUpdate is set and we're playing. Update the renderer.
updateOutput(subtitle.getCues(positionUs));
}
if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
return;
}
try {
while (!inputStreamEnded) {
@Nullable SubtitleInputBuffer nextInputBuffer = this.nextInputBuffer;
if (nextInputBuffer == null) {
nextInputBuffer = checkNotNull(decoder).dequeueInputBuffer();
if (nextInputBuffer == null) {
return;
}
this.nextInputBuffer = nextInputBuffer;
}
if (decoderReplacementState == REPLACEMENT_STATE_SIGNAL_END_OF_STREAM) {
nextInputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
checkNotNull(decoder).queueInputBuffer(nextInputBuffer);
this.nextInputBuffer = null;
decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM;
return;
}
// Try and read the next subtitle from the source.
@SampleStream.ReadDataResult int result = readSource(formatHolder, nextInputBuffer, /* readFlags= */ 0);
if (result == C.RESULT_BUFFER_READ) {
if (nextInputBuffer.isEndOfStream()) {
inputStreamEnded = true;
waitingForKeyFrame = false;
} else {
@Nullable Format format = formatHolder.format;
if (format == null) {
// We haven't received a format yet.
return;
}
nextInputBuffer.subsampleOffsetUs = format.subsampleOffsetUs;
nextInputBuffer.flip();
waitingForKeyFrame &= !nextInputBuffer.isKeyFrame();
}
if (!waitingForKeyFrame) {
checkNotNull(decoder).queueInputBuffer(nextInputBuffer);
this.nextInputBuffer = null;
}
} else if (result == C.RESULT_NOTHING_READ) {
return;
}
}
} catch (SubtitleDecoderException e) {
handleDecoderError(e);
}
}
@Override
protected void onDisabled() {
streamFormat = null;
finalStreamEndPositionUs = C.TIME_UNSET;
clearOutput();
releaseDecoder();
}
@Override
public boolean isEnded() {
return outputStreamEnded;
}
@Override
public boolean isReady() {
// Don't block playback whilst subtitles are loading.
// Note: To change this behavior, it will be necessary to consider [Internal: b/12949941].
return true;
}
private void releaseBuffers() {
nextInputBuffer = null;
nextSubtitleEventIndex = C.INDEX_UNSET;
if (subtitle != null) {
subtitle.release();
subtitle = null;
}
if (nextSubtitle != null) {
nextSubtitle.release();
nextSubtitle = null;
}
}
private void releaseDecoder() {
releaseBuffers();
checkNotNull(decoder).release();
decoder = null;
decoderReplacementState = REPLACEMENT_STATE_NONE;
}
private void initDecoder() {
waitingForKeyFrame = true;
decoder = decoderFactory.createDecoder(checkNotNull(streamFormat));
}
private void replaceDecoder() {
releaseDecoder();
initDecoder();
}
private long getNextEventTime() {
if (nextSubtitleEventIndex == C.INDEX_UNSET) {
return Long.MAX_VALUE;
}
checkNotNull(subtitle);
return nextSubtitleEventIndex >= subtitle.getEventTimeCount()
? Long.MAX_VALUE
: subtitle.getEventTime(nextSubtitleEventIndex);
}
private void updateOutput(List<Cue> cues) {
if (outputHandler != null) {
outputHandler.obtainMessage(MSG_UPDATE_OUTPUT, cues).sendToTarget();
} else {
invokeUpdateOutputInternal(cues);
}
}
private void clearOutput() {
updateOutput(Collections.emptyList());
}
@SuppressWarnings("unchecked")
@Override
public boolean handleMessage(Message msg) {
if (msg.what == MSG_UPDATE_OUTPUT) {
invokeUpdateOutputInternal((List<Cue>) msg.obj);
return true;
}
throw new IllegalStateException();
}
private void invokeUpdateOutputInternal(List<Cue> cues) {
// See https://github.com/google/ExoPlayer/issues/7934
// SubripDecoder texts tend to be DIMEN_UNSET which pushes up the
// subs unlike WEBVTT which creates an inconsistency
List<Cue> fixedCues = cues.stream().map(
cue -> {
Cue.Builder builder = cue.buildUpon();
if (cue.line == DIMEN_UNSET)
builder.setLine(-1f, LINE_TYPE_NUMBER);
return builder.setSize(DIMEN_UNSET).build();
}
).collect(Collectors.toList());
output.onCues(new CueGroup(fixedCues, 0L));
}
/**
* Called when {@link #decoder} throws an exception, so it can be logged and playback can
* continue.
*
* <p>Logs {@code e} and resets state to allow decoding the next sample.
*/
private void handleDecoderError(SubtitleDecoderException e) {
Log.e(TAG, "Subtitle decoding failed. streamFormat=" + streamFormat, e);
clearOutput();
replaceDecoder();
}
}

View file

@ -47,6 +47,19 @@ data class SubtitleData(
return if (origin == SubtitleOrigin.EMBEDDED_IN_VIDEO) url return if (origin == SubtitleOrigin.EMBEDDED_IN_VIDEO) url
else "$url|$name" else "$url|$name"
} }
/**
* Gets the URL, but tries to fix it if it is malformed.
*/
fun getFixedUrl(): String {
// Some extensions fail to include the protocol, this helps with that.
val fixedSubUrl = if (this.url.startsWith("//")) {
"https:${this.url}"
} else {
this.url
}
return fixedSubUrl
}
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)

View file

@ -338,13 +338,13 @@
tools:text="10" /> tools:text="10" />
<ImageButton <ImageButton
android:id="@id/exo_rew" android:id="@+id/player_rew"
android:layout_width="70dp" android:layout_width="70dp"
android:layout_height="70dp" android:layout_height="70dp"
android:layout_gravity="center" android:layout_gravity="center"
android:background="@drawable/video_tap_button_skip" android:background="@drawable/video_tap_button_skip"
android:nextFocusLeft="@id/exo_rew" android:nextFocusLeft="@id/player_rew"
android:nextFocusUp="@id/player_go_back" android:nextFocusUp="@id/player_go_back"
android:nextFocusDown="@id/player_lock" android:nextFocusDown="@id/player_lock"
android:padding="10dp" android:padding="10dp"
@ -371,9 +371,9 @@
android:layout_height="70dp" android:layout_height="70dp"
android:layout_gravity="center" android:layout_gravity="center"
android:background="@drawable/video_tap_button" android:background="@drawable/video_tap_button"
android:nextFocusLeft="@id/exo_rew" android:nextFocusLeft="@id/player_rew"
android:nextFocusRight="@id/exo_ffwd" android:nextFocusRight="@id/player_ffwd"
android:nextFocusUp="@id/player_go_back" android:nextFocusUp="@id/player_go_back"
android:nextFocusDown="@id/player_lock" android:nextFocusDown="@id/player_lock"
@ -406,13 +406,13 @@
tools:text="10" /> tools:text="10" />
<ImageButton <ImageButton
android:id="@id/exo_ffwd" android:id="@+id/player_ffwd"
android:layout_width="70dp" android:layout_width="70dp"
android:layout_height="70dp" android:layout_height="70dp"
android:layout_gravity="center" android:layout_gravity="center"
android:background="@drawable/video_tap_button_skip" android:background="@drawable/video_tap_button_skip"
android:nextFocusRight="@id/exo_rew" android:nextFocusRight="@id/player_rew"
android:nextFocusUp="@id/player_go_back" android:nextFocusUp="@id/player_go_back"
android:nextFocusDown="@id/player_lock" android:nextFocusDown="@id/player_lock"
android:padding="10dp" android:padding="10dp"

View file

@ -442,13 +442,13 @@
tools:text="10" /> tools:text="10" />
<ImageButton <ImageButton
android:id="@id/exo_rew" android:id="@id/player_rew"
android:layout_width="70dp" android:layout_width="70dp"
android:layout_height="70dp" android:layout_height="70dp"
android:layout_gravity="center" android:layout_gravity="center"
android:background="@drawable/video_tap_button_skip" android:background="@drawable/video_tap_button_skip"
android:nextFocusLeft="@id/exo_rew" android:nextFocusLeft="@id/player_rew"
android:nextFocusUp="@id/player_go_back" android:nextFocusUp="@id/player_go_back"
android:nextFocusDown="@id/player_pause_play" android:nextFocusDown="@id/player_pause_play"
android:padding="10dp" android:padding="10dp"
@ -484,13 +484,13 @@
tools:text="10" /> tools:text="10" />
<ImageButton <ImageButton
android:id="@id/exo_ffwd" android:id="@id/player_ffwd"
android:layout_width="70dp" android:layout_width="70dp"
android:layout_height="70dp" android:layout_height="70dp"
android:layout_gravity="center" android:layout_gravity="center"
android:background="@drawable/video_tap_button_skip" android:background="@drawable/video_tap_button_skip"
android:nextFocusRight="@id/exo_rew" android:nextFocusRight="@id/player_rew"
android:nextFocusUp="@id/player_go_back" android:nextFocusUp="@id/player_go_back"
android:nextFocusDown="@id/player_pause_play" android:nextFocusDown="@id/player_pause_play"
android:padding="10dp" android:padding="10dp"

View file

@ -326,13 +326,13 @@
tools:text="10" /> tools:text="10" />
<ImageButton <ImageButton
android:id="@id/exo_rew" android:id="@id/player_rew"
android:layout_width="70dp" android:layout_width="70dp"
android:layout_height="70dp" android:layout_height="70dp"
android:layout_gravity="center" android:layout_gravity="center"
android:background="@drawable/video_tap_button_skip" android:background="@drawable/video_tap_button_skip"
android:nextFocusLeft="@id/exo_rew" android:nextFocusLeft="@id/player_rew"
android:nextFocusUp="@id/player_go_back" android:nextFocusUp="@id/player_go_back"
android:nextFocusDown="@id/player_lock" android:nextFocusDown="@id/player_lock"
android:padding="10dp" android:padding="10dp"
@ -359,9 +359,9 @@
android:layout_height="70dp" android:layout_height="70dp"
android:layout_gravity="center" android:layout_gravity="center"
android:background="@drawable/video_tap_button" android:background="@drawable/video_tap_button"
android:nextFocusLeft="@id/exo_rew" android:nextFocusLeft="@id/player_rew"
android:nextFocusRight="@id/exo_ffwd" android:nextFocusRight="@id/player_ffwd"
android:nextFocusUp="@id/player_go_back" android:nextFocusUp="@id/player_go_back"
android:nextFocusDown="@id/player_lock" android:nextFocusDown="@id/player_lock"
@ -394,13 +394,13 @@
tools:text="10" /> tools:text="10" />
<ImageButton <ImageButton
android:id="@id/exo_ffwd" android:id="@id/player_ffwd"
android:layout_width="70dp" android:layout_width="70dp"
android:layout_height="70dp" android:layout_height="70dp"
android:layout_gravity="center" android:layout_gravity="center"
android:background="@drawable/video_tap_button_skip" android:background="@drawable/video_tap_button_skip"
android:nextFocusRight="@id/exo_rew" android:nextFocusRight="@id/player_rew"
android:nextFocusUp="@id/player_go_back" android:nextFocusUp="@id/player_go_back"
android:nextFocusDown="@id/player_lock" android:nextFocusDown="@id/player_lock"
android:padding="10dp" android:padding="10dp"