Fixed subtitle elevation again

This commit is contained in:
Blatzar 2022-12-03 02:42:16 +01:00
parent e215747749
commit b79e2d768f

View file

@ -15,6 +15,8 @@
*/ */
package com.lagradost.cloudstream3.ui.player; 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.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState; import static com.google.android.exoplayer2.util.Assertions.checkState;
import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.ElementType.TYPE_USE;
@ -23,8 +25,10 @@ import android.os.Handler;
import android.os.Handler.Callback; import android.os.Handler.Callback;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; 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.Log;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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 // 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. * A renderer for text.
* *
@ -59,365 +67,390 @@ import java.util.List;
*/ */
public class NonFinalTextRenderer extends BaseRenderer implements Callback { 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 * @param trackType The track type that the renderer handles. One of the {@link C} {@code
* TRACK_TYPE_*} constants. * TRACK_TYPE_*} constants.
* @param outputHandler * @param outputHandler
*/ */
public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) { public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) {
super(trackType); super(trackType);
this.outputHandler = outputHandler; 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.
*
* <p>Must be called after {@link #setCurrentStreamFinal()}.
*
* @param streamEndPositionUs The position to stop rendering at or {@link C#LENGTH_UNSET} to
* render until the end of the current stream.
*/
// TODO(internal b/181312195): Remove this when it's no longer needed once subtitles are decoded
// on the loading side of SampleQueue.
public void setFinalStreamEndPositionUs(long streamEndPositionUs) {
checkState(isCurrentStreamFinal());
this.finalStreamEndPositionUs = streamEndPositionUs;
}
@Override
protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) {
streamFormat = formats[0];
if (decoder != null) {
decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM;
} else {
initDecoder();
}
}
@Override
protected void onPositionReset(long positionUs, boolean joining) {
clearOutput();
inputStreamEnded = false;
outputStreamEnded = false;
finalStreamEndPositionUs = C.TIME_UNSET;
if (decoderReplacementState != REPLACEMENT_STATE_NONE) {
replaceDecoder();
} else {
releaseBuffers();
checkNotNull(decoder).flush();
}
}
@Override
public void render(long positionUs, long elapsedRealtimeUs) {
if (isCurrentStreamFinal()
&& finalStreamEndPositionUs != C.TIME_UNSET
&& positionUs >= finalStreamEndPositionUs) {
releaseBuffers();
outputStreamEnded = true;
} }
if (outputStreamEnded) { @Documented
return; @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); * The decoder does not need to be replaced.
try { */
nextSubtitle = checkNotNull(decoder).dequeueOutputBuffer(); private static final int REPLACEMENT_STATE_NONE = 0;
} catch (SubtitleDecoderException e) { /**
handleDecoderError(e); * The decoder needs to be replaced, but we haven't yet signaled an end of stream to the existing
return; * 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; @Override
if (subtitle != null) { public String getName() {
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we return TAG;
// advance to the next event.
long subtitleNextEventTimeUs = getNextEventTime();
while (subtitleNextEventTimeUs <= positionUs) {
nextSubtitleEventIndex++;
subtitleNextEventTimeUs = getNextEventTime();
textRendererNeedsUpdate = true;
}
} }
if (nextSubtitle != null) {
SubtitleOutputBuffer nextSubtitle = this.nextSubtitle; @Override
if (nextSubtitle.isEndOfStream()) { public @Capabilities int supportsFormat(Format format) {
if (!textRendererNeedsUpdate && getNextEventTime() == Long.MAX_VALUE) { if (decoderFactory.supportsFormat(format)) {
if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) { return RendererCapabilities.create(
format.cryptoType == C.CRYPTO_TYPE_NONE ? C.FORMAT_HANDLED : C.FORMAT_UNSUPPORTED_DRM);
} else if (MimeTypes.isText(format.sampleMimeType)) {
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE);
} else {
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE);
}
}
/**
* Sets the position at which to stop rendering the current stream.
*
* <p>Must be called after {@link #setCurrentStreamFinal()}.
*
* @param streamEndPositionUs The position to stop rendering at or {@link C#LENGTH_UNSET} to
* render until the end of the current stream.
*/
// TODO(internal b/181312195): Remove this when it's no longer needed once subtitles are decoded
// on the loading side of SampleQueue.
public void setFinalStreamEndPositionUs(long streamEndPositionUs) {
checkState(isCurrentStreamFinal());
this.finalStreamEndPositionUs = streamEndPositionUs;
}
@Override
protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) {
streamFormat = formats[0];
if (decoder != null) {
decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM;
} else {
initDecoder();
}
}
@Override
protected void onPositionReset(long positionUs, boolean joining) {
clearOutput();
inputStreamEnded = false;
outputStreamEnded = false;
finalStreamEndPositionUs = C.TIME_UNSET;
if (decoderReplacementState != REPLACEMENT_STATE_NONE) {
replaceDecoder(); replaceDecoder();
} else { } else {
releaseBuffers();
checkNotNull(decoder).flush();
}
}
@Override
public void render(long positionUs, long elapsedRealtimeUs) {
if (isCurrentStreamFinal()
&& finalStreamEndPositionUs != C.TIME_UNSET
&& positionUs >= finalStreamEndPositionUs) {
releaseBuffers(); releaseBuffers();
outputStreamEnded = true; 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 (outputStreamEnded) {
// 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; return;
}
this.nextInputBuffer = nextInputBuffer;
} }
if (decoderReplacementState == REPLACEMENT_STATE_SIGNAL_END_OF_STREAM) {
nextInputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); if (nextSubtitle == null) {
checkNotNull(decoder).queueInputBuffer(nextInputBuffer); checkNotNull(decoder).setPositionUs(positionUs);
this.nextInputBuffer = null; try {
decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM; nextSubtitle = checkNotNull(decoder).dequeueOutputBuffer();
return; } catch (SubtitleDecoderException e) {
} handleDecoderError(e);
// Try and read the next subtitle from the source. return;
@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) { if (getState() != STATE_STARTED) {
handleDecoderError(e); 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 @Override
protected void onDisabled() { protected void onDisabled() {
streamFormat = null; streamFormat = null;
finalStreamEndPositionUs = C.TIME_UNSET; finalStreamEndPositionUs = C.TIME_UNSET;
clearOutput(); clearOutput();
releaseDecoder(); 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(); @Override
nextSubtitle = null; public boolean isEnded() {
return outputStreamEnded;
} }
}
private void releaseDecoder() { @Override
releaseBuffers(); public boolean isReady() {
checkNotNull(decoder).release(); // Don't block playback whilst subtitles are loading.
decoder = null; // Note: To change this behavior, it will be necessary to consider [Internal: b/12949941].
decoderReplacementState = REPLACEMENT_STATE_NONE;
}
private void initDecoder() {
waitingForKeyFrame = true;
decoder = decoderFactory.createDecoder(checkNotNull(streamFormat));
}
private void replaceDecoder() {
releaseDecoder();
initDecoder();
}
private long getNextEventTime() {
if (nextSubtitleEventIndex == C.INDEX_UNSET) {
return Long.MAX_VALUE;
}
checkNotNull(subtitle);
return nextSubtitleEventIndex >= subtitle.getEventTimeCount()
? Long.MAX_VALUE
: subtitle.getEventTime(nextSubtitleEventIndex);
}
private void updateOutput(List<Cue> cues) {
if (outputHandler != null) {
outputHandler.obtainMessage(MSG_UPDATE_OUTPUT, cues).sendToTarget();
} else {
invokeUpdateOutputInternal(cues);
}
}
private void clearOutput() {
updateOutput(Collections.emptyList());
}
@SuppressWarnings("unchecked")
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_UPDATE_OUTPUT:
invokeUpdateOutputInternal((List<Cue>) msg.obj);
return true; return true;
default:
throw new IllegalStateException();
} }
}
private void invokeUpdateOutputInternal(List<Cue> cues) { private void releaseBuffers() {
output.onCues(cues); nextInputBuffer = null;
output.onCues(new CueGroup(cues)); nextSubtitleEventIndex = C.INDEX_UNSET;
} if (subtitle != null) {
subtitle.release();
subtitle = null;
}
if (nextSubtitle != null) {
nextSubtitle.release();
nextSubtitle = null;
}
}
/** private void releaseDecoder() {
* Called when {@link #decoder} throws an exception, so it can be logged and playback can releaseBuffers();
* continue. checkNotNull(decoder).release();
* decoder = null;
* <p>Logs {@code e} and resets state to allow decoding the next sample. decoderReplacementState = REPLACEMENT_STATE_NONE;
*/ }
private void handleDecoderError(SubtitleDecoderException e) {
Log.e(TAG, "Subtitle decoding failed. streamFormat=" + streamFormat, e); private void initDecoder() {
clearOutput(); waitingForKeyFrame = true;
replaceDecoder(); decoder = decoderFactory.createDecoder(checkNotNull(streamFormat));
} }
private void replaceDecoder() {
releaseDecoder();
initDecoder();
}
private long getNextEventTime() {
if (nextSubtitleEventIndex == C.INDEX_UNSET) {
return Long.MAX_VALUE;
}
checkNotNull(subtitle);
return nextSubtitleEventIndex >= subtitle.getEventTimeCount()
? Long.MAX_VALUE
: subtitle.getEventTime(nextSubtitleEventIndex);
}
private void updateOutput(List<Cue> cues) {
if (outputHandler != null) {
outputHandler.obtainMessage(MSG_UPDATE_OUTPUT, cues).sendToTarget();
} else {
invokeUpdateOutputInternal(cues);
}
}
private void clearOutput() {
updateOutput(Collections.emptyList());
}
@SuppressWarnings("unchecked")
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_UPDATE_OUTPUT:
invokeUpdateOutputInternal((List<Cue>) msg.obj);
return true;
default:
throw new IllegalStateException();
}
}
private void invokeUpdateOutputInternal(List<Cue> cues) {
// See https://github.com/google/ExoPlayer/issues/7934
// SubripDecoder texts tend to be DIMEN_UNSET which pushes up the
// subs unlike WEBVTT which creates an inconsistency
List<Cue> fixedCues = cues.stream().map(
cue -> {
Cue.Builder builder = cue.buildUpon();
if (cue.line == DIMEN_UNSET)
builder.setLine(-1f, LINE_TYPE_NUMBER);
return builder.setSize(DIMEN_UNSET).build();
}
).collect(Collectors.toList());
output.onCues(fixedCues);
output.onCues(new CueGroup(fixedCues));
}
/**
* Called when {@link #decoder} throws an exception, so it can be logged and playback can
* continue.
*
* <p>Logs {@code e} and resets state to allow decoding the next sample.
*/
private void handleDecoderError(SubtitleDecoderException e) {
Log.e(TAG, "Subtitle decoding failed. streamFormat=" + streamFormat, e);
clearOutput();
replaceDecoder();
}
} }