mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
subtitle offset, fixed #36
This commit is contained in:
parent
cd6c79b961
commit
a1a5af6570
9 changed files with 636 additions and 7 deletions
|
@ -85,6 +85,10 @@ abstract class AbstractPlayerFragment(
|
|||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
open fun subtitlesChanged() {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
private fun keepScreenOn(on: Boolean) {
|
||||
if (on) {
|
||||
activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
|
@ -325,7 +329,7 @@ abstract class AbstractPlayerFragment(
|
|||
SKIP_OP_VIDEO_PERCENTAGE,
|
||||
PRELOAD_NEXT_EPISODE_PERCENTAGE,
|
||||
NEXT_WATCH_EPISODE_PERCENTAGE,
|
||||
)
|
||||
), subtitlesUpdates = ::subtitlesChanged
|
||||
)
|
||||
|
||||
if (player is CS3IPlayer) {
|
||||
|
|
|
@ -74,6 +74,7 @@ class CS3IPlayer : IPlayer {
|
|||
private var updateIsPlaying: ((Pair<CSPlayerLoading, CSPlayerLoading>) -> Unit)? = null
|
||||
private var requestAutoFocus: (() -> Unit)? = null
|
||||
private var playerError: ((Exception) -> Unit)? = null
|
||||
private var subtitlesUpdates: (() -> Unit)? = null
|
||||
|
||||
/** width x height */
|
||||
private var playerDimensionsLoaded: ((Pair<Int, Int>) -> Unit)? = null
|
||||
|
@ -100,7 +101,8 @@ class CS3IPlayer : IPlayer {
|
|||
requestedListeningPercentages: List<Int>?,
|
||||
playerPositionChanged: ((Pair<Long, Long>) -> Unit)?,
|
||||
nextEpisode: (() -> Unit)?,
|
||||
prevEpisode: (() -> Unit)?
|
||||
prevEpisode: (() -> Unit)?,
|
||||
subtitlesUpdates: (() -> Unit)?
|
||||
) {
|
||||
this.playerUpdated = playerUpdated
|
||||
this.updateIsPlaying = updateIsPlaying
|
||||
|
@ -111,6 +113,7 @@ class CS3IPlayer : IPlayer {
|
|||
this.playerPositionChanged = playerPositionChanged
|
||||
this.nextEpisode = nextEpisode
|
||||
this.prevEpisode = prevEpisode
|
||||
this.subtitlesUpdates = subtitlesUpdates
|
||||
}
|
||||
|
||||
// I know, this is not a perfect solution, however it works for fixing subs
|
||||
|
@ -215,6 +218,17 @@ class CS3IPlayer : IPlayer {
|
|||
} ?: false
|
||||
}
|
||||
|
||||
var currentSubtitleOffset : Long = 0
|
||||
|
||||
override fun setSubtitleOffset(offset: Long) {
|
||||
currentSubtitleOffset = offset
|
||||
currentTextRenderer?.setRenderOffsetMs(offset)
|
||||
}
|
||||
|
||||
override fun getSubtitleOffset(): Long {
|
||||
return currentSubtitleOffset//currentTextRenderer?.getRenderOffsetMs() ?: currentSubtitleOffset
|
||||
}
|
||||
|
||||
override fun getCurrentPreferredSubtitle(): SubtitleData? {
|
||||
return subtitleHelper.getAllSubtitles().firstOrNull { sub ->
|
||||
exoPlayerSelectedTracks.any {
|
||||
|
@ -251,6 +265,7 @@ class CS3IPlayer : IPlayer {
|
|||
|
||||
exoPlayer?.release()
|
||||
simpleCache?.release()
|
||||
currentTextRenderer = null
|
||||
|
||||
exoPlayer = null
|
||||
simpleCache = null
|
||||
|
@ -395,7 +410,7 @@ class CS3IPlayer : IPlayer {
|
|||
return trackSelector
|
||||
}
|
||||
|
||||
var currentTextRenderer: TextRenderer? = null
|
||||
var currentTextRenderer: CustomTextRenderer? = null
|
||||
|
||||
private fun buildExoPlayer(
|
||||
context: Context,
|
||||
|
@ -404,6 +419,7 @@ class CS3IPlayer : IPlayer {
|
|||
currentWindow: Int,
|
||||
playbackPosition: Long,
|
||||
playBackSpeed: Float,
|
||||
subtitleOffset : Long,
|
||||
playWhenReady: Boolean = true,
|
||||
cacheFactory: CacheDataSource.Factory? = null,
|
||||
trackSelector: TrackSelector? = null,
|
||||
|
@ -419,7 +435,8 @@ class CS3IPlayer : IPlayer {
|
|||
metadataRendererOutput
|
||||
).map {
|
||||
if (it is TextRenderer) {
|
||||
currentTextRenderer = TextRenderer(
|
||||
currentTextRenderer = CustomTextRenderer(
|
||||
subtitleOffset,
|
||||
textRendererOutput,
|
||||
eventHandler.looper,
|
||||
CustomSubtitleDecoderFactory()
|
||||
|
@ -534,7 +551,8 @@ class CS3IPlayer : IPlayer {
|
|||
playbackPosition,
|
||||
playBackSpeed,
|
||||
playWhenReady = isPlaying, // this keep the current state of the player
|
||||
cacheFactory = cacheFactory
|
||||
cacheFactory = cacheFactory,
|
||||
subtitleOffset = currentSubtitleOffset
|
||||
)
|
||||
|
||||
requestSubtitleUpdate = ::reloadSubs
|
||||
|
@ -558,6 +576,7 @@ class CS3IPlayer : IPlayer {
|
|||
override fun onTracksInfoChanged(tracksInfo: TracksInfo) {
|
||||
exoPlayerSelectedTracks =
|
||||
tracksInfo.trackGroupInfos.mapNotNull { it.trackGroup.getFormat(0).language?.let { lang -> lang to it.isSelected } }
|
||||
subtitlesUpdates?.invoke()
|
||||
super.onTracksInfoChanged(tracksInfo)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import android.os.Looper
|
||||
import com.google.android.exoplayer2.text.SubtitleDecoderFactory
|
||||
import com.google.android.exoplayer2.text.TextOutput
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import android.media.AudioManager
|
|||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.text.Editable
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
|
@ -20,11 +21,16 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHO
|
|||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.graphics.blue
|
||||
import androidx.core.graphics.green
|
||||
import androidx.core.graphics.red
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
|
@ -35,6 +41,7 @@ import com.lagradost.cloudstream3.mvvm.logError
|
|||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getNavigationBarHeight
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||
|
@ -71,6 +78,19 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
protected var doubleTapEnabled = false
|
||||
protected var doubleTapPauseEnabled = true
|
||||
|
||||
protected var subtitleDelay
|
||||
set(value) = try {
|
||||
player.setSubtitleOffset(-value)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
get() = try {
|
||||
-player.getSubtitleOffset()
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
0L
|
||||
}
|
||||
|
||||
//private var useSystemBrightness = false
|
||||
protected var useTrueSystemBrightness = true
|
||||
private val fullscreenNotch = true //TODO SETTING
|
||||
|
@ -196,6 +216,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
player_top_holder?.startAnimation(fadeAnimation)
|
||||
}
|
||||
|
||||
override fun subtitlesChanged() {
|
||||
player_subtitle_offset_btt?.isGone = player.getCurrentPreferredSubtitle() == null
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
activity?.hideSystemUI()
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
|
@ -241,6 +265,77 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
player.seekTime(85000) // skip 85s
|
||||
}
|
||||
|
||||
private fun showSubtitleOffsetDialog() {
|
||||
context?.let { ctx ->
|
||||
val builder =
|
||||
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
|
||||
.setView(R.layout.subtitle_offset)
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
|
||||
val beforeOffset = subtitleDelay
|
||||
|
||||
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
|
||||
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!!
|
||||
val input = dialog.findViewById<EditText>(R.id.subtitle_offset_input)!!
|
||||
val sub = dialog.findViewById<ImageView>(R.id.subtitle_offset_subtract)!!
|
||||
val add = dialog.findViewById<ImageView>(R.id.subtitle_offset_add)!!
|
||||
val subTitle = dialog.findViewById<TextView>(R.id.subtitle_offset_sub_title)!!
|
||||
|
||||
input.doOnTextChanged { text, _, _, _ ->
|
||||
text?.toString()?.toLongOrNull()?.let {
|
||||
subtitleDelay = it
|
||||
when {
|
||||
it > 0L -> {
|
||||
context?.getString(R.string.subtitle_offset_extra_hint_later_format)
|
||||
?.format(it)
|
||||
}
|
||||
it < 0L -> {
|
||||
context?.getString(R.string.subtitle_offset_extra_hint_before_format)
|
||||
?.format(-it)
|
||||
}
|
||||
it == 0L -> {
|
||||
context?.getString(R.string.subtitle_offset_extra_hint_none_format)
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}?.let { str ->
|
||||
subTitle.text = str
|
||||
}
|
||||
}
|
||||
}
|
||||
input.text = Editable.Factory.getInstance()?.newEditable(beforeOffset.toString())
|
||||
|
||||
val buttonChange = 100L
|
||||
|
||||
fun changeBy(by: Long) {
|
||||
val current = (input.text?.toString()?.toLongOrNull() ?: 0) + by
|
||||
input.text = Editable.Factory.getInstance()?.newEditable(current.toString())
|
||||
}
|
||||
|
||||
add.setOnClickListener {
|
||||
changeBy(buttonChange)
|
||||
}
|
||||
|
||||
sub.setOnClickListener {
|
||||
changeBy(-buttonChange)
|
||||
}
|
||||
|
||||
dialog.setOnDismissListener {
|
||||
activity?.hideSystemUI()
|
||||
}
|
||||
applyButton.setOnClickListener {
|
||||
dialog.dismissSafe(activity)
|
||||
player.seekTime(1L)
|
||||
}
|
||||
cancelButton.setOnClickListener {
|
||||
subtitleDelay = beforeOffset
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSpeedDialog() {
|
||||
val speedsText =
|
||||
listOf(
|
||||
|
@ -1055,6 +1150,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
toggleLock()
|
||||
}
|
||||
|
||||
player_subtitle_offset_btt?.setOnClickListener {
|
||||
showSubtitleOffsetDialog()
|
||||
}
|
||||
|
||||
exo_rew?.setOnClickListener {
|
||||
autoHide()
|
||||
rewind()
|
||||
|
|
|
@ -71,6 +71,9 @@ interface IPlayer {
|
|||
fun seekTime(time: Long)
|
||||
fun seekTo(time: Long)
|
||||
|
||||
fun getSubtitleOffset() : Long // in ms
|
||||
fun setSubtitleOffset(offset : Long) // in ms
|
||||
|
||||
fun initCallbacks(
|
||||
playerUpdated: (Any?) -> Unit, // attach player to view
|
||||
updateIsPlaying: ((Pair<CSPlayerLoading, CSPlayerLoading>) -> Unit)? = null, // (wasPlaying, isPlaying)
|
||||
|
@ -81,6 +84,7 @@ interface IPlayer {
|
|||
playerPositionChanged: ((Pair<Long, Long>) -> Unit)? = null,// (position, duration) this is used to update UI based of the current time
|
||||
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
|
||||
subtitlesUpdates: (() -> Unit)? = null, // callback from player to inform that subtitles have updated in some way
|
||||
)
|
||||
|
||||
fun updateSubtitleStyle(style: SaveCaptionStyle)
|
||||
|
|
|
@ -0,0 +1,369 @@
|
|||
/*
|
||||
* 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 android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Message
|
||||
import androidx.annotation.IntDef
|
||||
import com.google.android.exoplayer2.*
|
||||
import com.google.android.exoplayer2.source.SampleStream.ReadDataResult
|
||||
import com.google.android.exoplayer2.text.*
|
||||
import com.google.android.exoplayer2.util.Assertions
|
||||
import com.google.android.exoplayer2.util.Log
|
||||
import com.google.android.exoplayer2.util.MimeTypes
|
||||
import com.google.android.exoplayer2.util.Util
|
||||
|
||||
/**
|
||||
* A renderer for text.
|
||||
*
|
||||
*
|
||||
* [Subtitle]s are decoded from sample data using [SubtitleDecoder] instances
|
||||
* obtained from a [SubtitleDecoderFactory]. The actual rendering of the subtitle [Cue]s
|
||||
* is delegated to a [TextOutput].
|
||||
*/
|
||||
open class NonFinalTextRenderer @JvmOverloads constructor(
|
||||
output: TextOutput?,
|
||||
outputLooper: Looper?,
|
||||
private val decoderFactory: SubtitleDecoderFactory = SubtitleDecoderFactory.DEFAULT
|
||||
) :
|
||||
BaseRenderer(C.TRACK_TYPE_TEXT), Handler.Callback {
|
||||
@MustBeDocumented
|
||||
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
|
||||
@IntDef(
|
||||
REPLACEMENT_STATE_NONE,
|
||||
REPLACEMENT_STATE_SIGNAL_END_OF_STREAM,
|
||||
REPLACEMENT_STATE_WAIT_END_OF_STREAM
|
||||
)
|
||||
private annotation class ReplacementState
|
||||
|
||||
private val outputHandler: Handler? = if (outputLooper == null) null else Util.createHandler(
|
||||
outputLooper, /* callback= */
|
||||
this
|
||||
)
|
||||
private val output: TextOutput = Assertions.checkNotNull(output)
|
||||
private val formatHold: FormatHolder = FormatHolder()
|
||||
private var inputStreamEnded = false
|
||||
private var outputStreamEnded = false
|
||||
private var waitingForKeyFrame = false
|
||||
|
||||
@ReplacementState
|
||||
private var decoderReplacementState = 0
|
||||
private var streamFormat: Format? = null
|
||||
private var decoder: SubtitleDecoder? = null
|
||||
private var nextInputBuffer: SubtitleInputBuffer? = null
|
||||
private var subtitle: SubtitleOutputBuffer? = null
|
||||
private var nextSubtitle: SubtitleOutputBuffer? = null
|
||||
private var nextSubtitleEventIndex = 0
|
||||
private var finalStreamEndPositionUs: Long
|
||||
override fun getName(): String {
|
||||
return TAG
|
||||
}
|
||||
|
||||
@RendererCapabilities.Capabilities
|
||||
override fun supportsFormat(format: Format): Int {
|
||||
return if (decoderFactory.supportsFormat(format)) {
|
||||
RendererCapabilities.create(
|
||||
if (format.cryptoType == C.CRYPTO_TYPE_NONE) C.FORMAT_HANDLED else C.FORMAT_UNSUPPORTED_DRM
|
||||
)
|
||||
} else if (MimeTypes.isText(format.sampleMimeType)) {
|
||||
RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE)
|
||||
} else {
|
||||
RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the position at which to stop rendering the current stream.
|
||||
*
|
||||
*
|
||||
* Must be called after [.setCurrentStreamFinal].
|
||||
*
|
||||
* @param streamEndPositionUs The position to stop rendering at or [C.LENGTH_UNSET] to
|
||||
* render until the end of the current stream.
|
||||
*/
|
||||
|
||||
override fun onStreamChanged(formats: Array<Format>, startPositionUs: Long, offsetUs: Long) {
|
||||
streamFormat = formats[0]
|
||||
if (decoder != null) {
|
||||
decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM
|
||||
} else {
|
||||
initDecoder()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPositionReset(positionUs: Long, joining: Boolean) {
|
||||
clearOutput()
|
||||
inputStreamEnded = false
|
||||
outputStreamEnded = false
|
||||
finalStreamEndPositionUs = C.TIME_UNSET
|
||||
if (decoderReplacementState != REPLACEMENT_STATE_NONE) {
|
||||
replaceDecoder()
|
||||
} else {
|
||||
releaseBuffers()
|
||||
Assertions.checkNotNull(decoder).flush()
|
||||
}
|
||||
}
|
||||
|
||||
override fun render(positionUs: Long, elapsedRealtimeUs: Long) {
|
||||
if (isCurrentStreamFinal
|
||||
&& finalStreamEndPositionUs != C.TIME_UNSET && positionUs >= finalStreamEndPositionUs
|
||||
) {
|
||||
releaseBuffers()
|
||||
outputStreamEnded = true
|
||||
}
|
||||
if (outputStreamEnded) {
|
||||
return
|
||||
}
|
||||
if (nextSubtitle == null) {
|
||||
Assertions.checkNotNull(decoder).setPositionUs(positionUs)
|
||||
nextSubtitle = try {
|
||||
Assertions.checkNotNull(decoder).dequeueOutputBuffer()
|
||||
} catch (e: SubtitleDecoderException) {
|
||||
handleDecoderError(e)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (state != STATE_STARTED) {
|
||||
return
|
||||
}
|
||||
var textRendererNeedsUpdate = false
|
||||
if (subtitle != null) {
|
||||
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we
|
||||
// advance to the next event.
|
||||
var subtitleNextEventTimeUs = nextEventTime
|
||||
while (subtitleNextEventTimeUs <= positionUs) {
|
||||
nextSubtitleEventIndex++
|
||||
subtitleNextEventTimeUs = nextEventTime
|
||||
textRendererNeedsUpdate = true
|
||||
}
|
||||
}
|
||||
if (nextSubtitle != null) {
|
||||
val nextSubtitle = nextSubtitle
|
||||
if (nextSubtitle!!.isEndOfStream) {
|
||||
if (!textRendererNeedsUpdate && nextEventTime == 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.
|
||||
Assertions.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) {
|
||||
var nextInputBuffer = nextInputBuffer
|
||||
if (nextInputBuffer == null) {
|
||||
nextInputBuffer = Assertions.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)
|
||||
Assertions.checkNotNull(decoder).queueInputBuffer(nextInputBuffer)
|
||||
this.nextInputBuffer = null
|
||||
decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM
|
||||
return
|
||||
}
|
||||
// Try and read the next subtitle from the source.
|
||||
@ReadDataResult val result =
|
||||
readSource(formatHold, nextInputBuffer, /* readFlags= */0)
|
||||
if (result == C.RESULT_BUFFER_READ) {
|
||||
if (nextInputBuffer.isEndOfStream) {
|
||||
inputStreamEnded = true
|
||||
waitingForKeyFrame = false
|
||||
} else {
|
||||
val format = formatHold.format
|
||||
?: // We haven't received a format yet.
|
||||
return
|
||||
nextInputBuffer.subsampleOffsetUs = format.subsampleOffsetUs
|
||||
nextInputBuffer.flip()
|
||||
waitingForKeyFrame = waitingForKeyFrame and !nextInputBuffer.isKeyFrame
|
||||
}
|
||||
if (!waitingForKeyFrame) {
|
||||
Assertions.checkNotNull(decoder).queueInputBuffer(nextInputBuffer)
|
||||
this.nextInputBuffer = null
|
||||
}
|
||||
} else if (result == C.RESULT_NOTHING_READ) {
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (e: SubtitleDecoderException) {
|
||||
handleDecoderError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDisabled() {
|
||||
streamFormat = null
|
||||
finalStreamEndPositionUs = C.TIME_UNSET
|
||||
clearOutput()
|
||||
releaseDecoder()
|
||||
}
|
||||
|
||||
override fun isEnded(): Boolean {
|
||||
return outputStreamEnded
|
||||
}
|
||||
|
||||
override fun isReady(): Boolean {
|
||||
// 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 fun releaseBuffers() {
|
||||
nextInputBuffer = null
|
||||
nextSubtitleEventIndex = C.INDEX_UNSET
|
||||
if (subtitle != null) {
|
||||
subtitle!!.release()
|
||||
subtitle = null
|
||||
}
|
||||
if (nextSubtitle != null) {
|
||||
nextSubtitle!!.release()
|
||||
nextSubtitle = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun releaseDecoder() {
|
||||
releaseBuffers()
|
||||
Assertions.checkNotNull(decoder).release()
|
||||
decoder = null
|
||||
decoderReplacementState = REPLACEMENT_STATE_NONE
|
||||
}
|
||||
|
||||
private fun initDecoder() {
|
||||
waitingForKeyFrame = true
|
||||
decoder = decoderFactory.createDecoder(Assertions.checkNotNull(streamFormat))
|
||||
}
|
||||
|
||||
private fun replaceDecoder() {
|
||||
releaseDecoder()
|
||||
initDecoder()
|
||||
}
|
||||
|
||||
private val nextEventTime: Long
|
||||
get() {
|
||||
if (nextSubtitleEventIndex == C.INDEX_UNSET) {
|
||||
return Long.MAX_VALUE
|
||||
}
|
||||
Assertions.checkNotNull(subtitle)
|
||||
return if (nextSubtitleEventIndex >= subtitle!!.eventTimeCount) Long.MAX_VALUE else subtitle!!.getEventTime(
|
||||
nextSubtitleEventIndex
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateOutput(cues: List<Cue>) {
|
||||
if (outputHandler != null) {
|
||||
outputHandler.obtainMessage(MSG_UPDATE_OUTPUT, cues).sendToTarget()
|
||||
} else {
|
||||
invokeUpdateOutputInternal(cues)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearOutput() {
|
||||
updateOutput(emptyList())
|
||||
}
|
||||
|
||||
override fun handleMessage(msg: Message): Boolean {
|
||||
return when (msg.what) {
|
||||
MSG_UPDATE_OUTPUT -> {
|
||||
invokeUpdateOutputInternal(msg.obj as List<Cue>)
|
||||
true
|
||||
}
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
private fun invokeUpdateOutputInternal(cues: List<Cue>) {
|
||||
output.onCues(cues)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when [.decoder] throws an exception, so it can be logged and playback can
|
||||
* continue.
|
||||
*
|
||||
*
|
||||
* Logs `e` and resets state to allow decoding the next sample.
|
||||
*/
|
||||
private fun handleDecoderError(e: SubtitleDecoderException) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"Subtitle decoding failed. streamFormat=$streamFormat", e
|
||||
)
|
||||
clearOutput()
|
||||
replaceDecoder()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "TextRenderer"
|
||||
|
||||
/** The decoder does not need to be replaced. */
|
||||
private const val 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 const val 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 const val REPLACEMENT_STATE_WAIT_END_OF_STREAM = 2
|
||||
private const val MSG_UPDATE_OUTPUT = 0
|
||||
}
|
||||
/**
|
||||
* @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 [ ][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 [SubtitleDecoder] instances.
|
||||
*/
|
||||
/**
|
||||
* @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 [ ][android.app.Activity.getMainLooper]. Null may be passed if the output should be called
|
||||
* directly on the player's internal rendering thread.
|
||||
*/
|
||||
init {
|
||||
finalStreamEndPositionUs = C.TIME_UNSET
|
||||
}
|
||||
}
|
|
@ -508,15 +508,26 @@
|
|||
style="@style/VideoButton"
|
||||
android:nextFocusLeft="@id/player_resize_btt"
|
||||
|
||||
android:nextFocusRight="@id/player_sources_btt"
|
||||
android:nextFocusRight="@id/player_subtitle_offset_btt"
|
||||
app:icon="@drawable/ic_baseline_speed_24"
|
||||
tools:text="Speed" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/player_sources_btt"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:id="@+id/player_subtitle_offset_btt"
|
||||
style="@style/VideoButton"
|
||||
android:nextFocusLeft="@id/player_speed_btt"
|
||||
|
||||
android:nextFocusRight="@id/player_sources_btt"
|
||||
app:icon="@drawable/ic_outline_subtitles_24"
|
||||
android:text="@string/subtitle_offset"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/player_sources_btt"
|
||||
style="@style/VideoButton"
|
||||
android:nextFocusLeft="@id/player_subtitle_offset_btt"
|
||||
|
||||
android:nextFocusRight="@id/player_skip_op"
|
||||
android:text="@string/video_source"
|
||||
app:icon="@drawable/ic_baseline_playlist_play_24" />
|
||||
|
|
86
app/src/main/res/layout/subtitle_offset.xml
Normal file
86
app/src/main/res/layout/subtitle_offset.xml
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical" xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="300dp"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<TextView
|
||||
style="@style/WatchHeaderText"
|
||||
android:layout_margin="0dp"
|
||||
android:paddingTop="10dp"
|
||||
android:text="@string/subtitle_offset_title"
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subtitle_offset_sub_title"
|
||||
android:layout_gravity="center"
|
||||
tools:text="@string/subtitle_offset_extra_hint_none_format"
|
||||
android:textColor="?attr/grayTextColor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/subtitle_offset_subtract"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:padding="10dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/baseline_remove_24"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
app:tint="?attr/white"
|
||||
tools:ignore="ContentDescription" />
|
||||
<EditText
|
||||
android:layout_weight="20"
|
||||
android:id="@+id/subtitle_offset_input"
|
||||
android:inputType="numberSigned"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="@string/subtitle_offset_hint"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/subtitle_offset_add"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:padding="10dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_baseline_add_24"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
app:tint="?attr/white"
|
||||
tools:ignore="ContentDescription" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_gravity="bottom"
|
||||
android:gravity="bottom|end"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
style="@style/WhiteButton"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:visibility="visible"
|
||||
android:text="@string/sort_apply"
|
||||
android:id="@+id/apply_btt"
|
||||
android:layout_width="wrap_content">
|
||||
|
||||
<requestFocus />
|
||||
</com.google.android.material.button.MaterialButton>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
style="@style/BlackButton"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/sort_cancel"
|
||||
android:id="@+id/cancel_btt"
|
||||
android:layout_width="wrap_content" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
|
@ -377,6 +377,13 @@
|
|||
<string name="subtitles_depressed">Depressed</string>
|
||||
<string name="subtitles_shadow">Shadow</string>
|
||||
<string name="subtitles_raised">Raised</string>
|
||||
<string name="subtitle_offset">Sync subs</string>
|
||||
<string name="subtitle_offset_hint">1000ms</string>
|
||||
<string name="subtitle_offset_title">Subtitle delay</string>
|
||||
<string name="subtitle_offset_extra_hint_later_format">Use this if the subtitles are shown %dms too early</string>
|
||||
<string name="subtitle_offset_extra_hint_before_format">Use this if subtitles are shown %dms too late</string>
|
||||
<string name="subtitle_offset_extra_hint_none_format">No subtitle delay</string>
|
||||
|
||||
<!--
|
||||
Example text (pangram) can optionally be translated; if you do, include all the letters in the alphabet,
|
||||
see:
|
||||
|
|
Loading…
Reference in a new issue