forked from recloudstream/cloudstream
Fixed subtitles on new exoplayer version
This commit is contained in:
parent
3d3c85a1ad
commit
6308fd0fec
2 changed files with 423 additions and 383 deletions
|
@ -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.
|
||||||
|
*
|
||||||
|
* <p>{@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.
|
||||||
|
*
|
||||||
|
* <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) {
|
||||||
|
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<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) {
|
||||||
|
output.onCues(cues);
|
||||||
|
output.onCues(new CueGroup(cues));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Format>, 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<Cue>) {
|
|
||||||
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<Cue>)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun invokeUpdateOutputInternal(cues: List<Cue>) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue