From b79e2d768f4bdb39aaa3406b499b3edb5bfec8e2 Mon Sep 17 00:00:00 2001 From: Blatzar <46196380+Blatzar@users.noreply.github.com> Date: Sat, 3 Dec 2022 02:42:16 +0100 Subject: [PATCH] Fixed subtitle elevation again --- .../ui/player/NonFinalTextRenderer.java | 699 +++++++++--------- 1 file changed, 366 insertions(+), 333 deletions(-) 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 index 8602ce25..ce39a841 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java @@ -15,6 +15,8 @@ */ package com.lagradost.cloudstream3.ui.player; +import static com.google.android.exoplayer2.text.Cue.DIMEN_UNSET; +import static com.google.android.exoplayer2.text.Cue.LINE_TYPE_NUMBER; 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; @@ -23,8 +25,10 @@ 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; @@ -43,13 +47,17 @@ 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; +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. * @@ -59,365 +67,390 @@ import java.util.List; */ public class NonFinalTextRenderer extends BaseRenderer implements Callback { - private static final String TAG = "TextRenderer"; + 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; + /** + * @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; } - if (outputStreamEnded) { - return; + @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 { } - if (nextSubtitle == null) { - checkNotNull(decoder).setPositionUs(positionUs); - try { - nextSubtitle = checkNotNull(decoder).dequeueOutputBuffer(); - } catch (SubtitleDecoderException e) { - handleDecoderError(e); - return; - } + /** + * 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); } - if (getState() != STATE_STARTED) { - return; + /** + * @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; } - 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; - } + @Override + public String getName() { + return TAG; } - if (nextSubtitle != null) { - SubtitleOutputBuffer nextSubtitle = this.nextSubtitle; - if (nextSubtitle.isEndOfStream()) { - if (!textRendererNeedsUpdate && getNextEventTime() == Long.MAX_VALUE) { - if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) { + + @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 { + } else { + releaseBuffers(); + checkNotNull(decoder).flush(); + } + } + + @Override + public void render(long positionUs, long elapsedRealtimeUs) { + if (isCurrentStreamFinal() + && finalStreamEndPositionUs != C.TIME_UNSET + && positionUs >= finalStreamEndPositionUs) { 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) { + if (outputStreamEnded) { 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; + + if (nextSubtitle == null) { + checkNotNull(decoder).setPositionUs(positionUs); + try { + nextSubtitle = checkNotNull(decoder).dequeueOutputBuffer(); + } catch (SubtitleDecoderException e) { + handleDecoderError(e); + 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); + + 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; + @Override + protected void onDisabled() { + streamFormat = null; + finalStreamEndPositionUs = C.TIME_UNSET; + clearOutput(); + releaseDecoder(); } - if (nextSubtitle != null) { - nextSubtitle.release(); - nextSubtitle = null; + + @Override + public boolean isEnded() { + return outputStreamEnded; } - } - 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); + @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; - default: - throw new IllegalStateException(); } - } - private void invokeUpdateOutputInternal(List cues) { - output.onCues(cues); - output.onCues(new CueGroup(cues)); - } + private void releaseBuffers() { + nextInputBuffer = null; + nextSubtitleEventIndex = C.INDEX_UNSET; + if (subtitle != null) { + subtitle.release(); + subtitle = null; + } + if (nextSubtitle != null) { + nextSubtitle.release(); + nextSubtitle = null; + } + } - /** - * 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(); - } + 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) { + // 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 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(fixedCues); + output.onCues(new CueGroup(fixedCues)); + } + + /** + * 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(); + } }