mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Fixed subtitle elevation again
This commit is contained in:
parent
e215747749
commit
b79e2d768f
1 changed files with 366 additions and 333 deletions
|
@ -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.
|
||||
*
|
||||
* <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;
|
||||
/**
|
||||
* @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.
|
||||
*
|
||||
* <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 {
|
||||
} 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<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);
|
||||
@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<Cue> 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.
|
||||
*
|
||||
* <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();
|
||||
}
|
||||
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<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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue