diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java new file mode 100644 index 00000000..8602ce25 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java @@ -0,0 +1,423 @@ +/* + * 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 com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.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.Nullable; +import com.google.android.exoplayer2.BaseRenderer; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.CueGroup; +import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.text.SubtitleDecoder; +import com.google.android.exoplayer2.text.SubtitleDecoderException; +import com.google.android.exoplayer2.text.SubtitleDecoderFactory; +import com.google.android.exoplayer2.text.SubtitleInputBuffer; +import com.google.android.exoplayer2.text.SubtitleOutputBuffer; +import com.google.android.exoplayer2.text.TextOutput; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +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; +// DO NOT CONVERT TO KOTLIN AUTOMATICALLY, IT FUCKS UP AND DOES NOT DISPLAY SUBS FOR SOME REASON +/** + * A renderer for text. + * + *

{@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}. + */ +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 + */ + 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; + } + + @Override + public String getName() { + return TAG; + } + + @Override + public @Capabilities int supportsFormat(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. + * + *

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. + @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 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) { + switch (msg.what) { + case MSG_UPDATE_OUTPUT: + invokeUpdateOutputInternal((List) msg.obj); + return true; + default: + throw new IllegalStateException(); + } + } + + private void invokeUpdateOutputInternal(List cues) { + output.onCues(cues); + output.onCues(new CueGroup(cues)); + } + + /** + * Called when {@link #decoder} throws an exception, so it can be logged and playback can + * continue. + * + *

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(); + } +} 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 deleted file mode 100644 index 7acb7c34..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.kt +++ /dev/null @@ -1,383 +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 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.text.Cue.DIMEN_UNSET -import com.google.android.exoplayer2.text.Cue.LINE_TYPE_NUMBER -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.map { cue -> - val builder = cue.buildUpon() - - // 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 - if (cue.line == DIMEN_UNSET) - builder.setLine(-1f, LINE_TYPE_NUMBER) - - // this fixes https://github.com/LagradOst/CloudStream-3/issues/717 - builder.setSize(DIMEN_UNSET).build() - }) - } - - /** - * 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