diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 03bf1975..6489b665 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -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) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index e81834ab..ee2cb8e2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -74,6 +74,7 @@ class CS3IPlayer : IPlayer { private var updateIsPlaying: ((Pair) -> 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) -> Unit)? = null @@ -100,7 +101,8 @@ class CS3IPlayer : IPlayer { requestedListeningPercentages: List?, playerPositionChanged: ((Pair) -> 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) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt new file mode 100644 index 00000000..d3f4171a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index 27687d38..a3545861 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -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(R.id.apply_btt)!! + val cancelButton = dialog.findViewById(R.id.cancel_btt)!! + val input = dialog.findViewById(R.id.subtitle_offset_input)!! + val sub = dialog.findViewById(R.id.subtitle_offset_subtract)!! + val add = dialog.findViewById(R.id.subtitle_offset_add)!! + val subTitle = dialog.findViewById(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() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index 45e0123f..a3368b3a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -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) -> Unit)? = null, // (wasPlaying, isPlaying) @@ -81,6 +84,7 @@ interface IPlayer { playerPositionChanged: ((Pair) -> 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) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.kt new file mode 100644 index 00000000..863a60c5 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.kt @@ -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, 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) { + 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) + true + } + else -> throw IllegalStateException() + } + } + + private fun invokeUpdateOutputInternal(cues: List) { + 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 + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index baf87e72..ce22c3e3 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -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" /> + + diff --git a/app/src/main/res/layout/subtitle_offset.xml b/app/src/main/res/layout/subtitle_offset.xml new file mode 100644 index 00000000..729df759 --- /dev/null +++ b/app/src/main/res/layout/subtitle_offset.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2e56df88..917776fe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -377,6 +377,13 @@ Depressed Shadow Raised + Sync subs + 1000ms + Subtitle delay + Use this if the subtitles are shown %dms too early + Use this if subtitles are shown %dms too late + No subtitle delay +