From 6308fd0fec126831702b9e3309b6990b71ebefed Mon Sep 17 00:00:00 2001 From: Blatzar <46196380+Blatzar@users.noreply.github.com> Date: Tue, 1 Nov 2022 23:27:42 +0100 Subject: [PATCH] Fixed subtitles on new exoplayer version --- .../ui/player/NonFinalTextRenderer.java | 423 ++++++++++++++++++ .../ui/player/NonFinalTextRenderer.kt | 383 ---------------- 2 files changed, 423 insertions(+), 383 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java new file mode 100644 index 00000000..8602ce25 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.lagradost.cloudstream3.ui.player; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; +import static java.lang.annotation.ElementType.TYPE_USE; + +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.Looper; +import android.os.Message; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.BaseRenderer; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.CueGroup; +import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.text.SubtitleDecoder; +import com.google.android.exoplayer2.text.SubtitleDecoderException; +import com.google.android.exoplayer2.text.SubtitleDecoderFactory; +import com.google.android.exoplayer2.text.SubtitleInputBuffer; +import com.google.android.exoplayer2.text.SubtitleOutputBuffer; +import com.google.android.exoplayer2.text.TextOutput; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collections; +import java.util.List; +// DO NOT CONVERT TO KOTLIN AUTOMATICALLY, IT FUCKS UP AND DOES NOT DISPLAY SUBS FOR SOME REASON +/** + * A renderer for text. + * + *
{@link Subtitle}s are decoded from sample data using {@link SubtitleDecoder} instances + * obtained from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s + * is delegated to a {@link TextOutput}. + */ +public class NonFinalTextRenderer extends BaseRenderer implements Callback { + + private static final String TAG = "TextRenderer"; + + /** + * @param trackType The track type that the renderer handles. One of the {@link C} {@code + * TRACK_TYPE_*} constants. + * @param outputHandler + */ + public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) { + super(trackType); + this.outputHandler = outputHandler; + } + + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({ + REPLACEMENT_STATE_NONE, + REPLACEMENT_STATE_SIGNAL_END_OF_STREAM, + REPLACEMENT_STATE_WAIT_END_OF_STREAM + }) + private @interface ReplacementState {} + /** The decoder does not need to be replaced. */ + private static final int REPLACEMENT_STATE_NONE = 0; + /** + * The decoder needs to be replaced, but we haven't yet signaled an end of stream to the existing + * decoder. We need to do so in order to ensure that it outputs any remaining buffers before we + * release it. + */ + private static final int REPLACEMENT_STATE_SIGNAL_END_OF_STREAM = 1; + /** + * The decoder needs to be replaced, and we've signaled an end of stream to the existing decoder. + * We're waiting for the decoder to output an end of stream signal to indicate that it has output + * any remaining buffers before we release it. + */ + private static final int REPLACEMENT_STATE_WAIT_END_OF_STREAM = 2; + + private static final int MSG_UPDATE_OUTPUT = 0; + + @Nullable private final Handler outputHandler; + private TextOutput output = null; + private SubtitleDecoderFactory decoderFactory = null; + private FormatHolder formatHolder = null; + + private boolean inputStreamEnded; + private boolean outputStreamEnded; + private boolean waitingForKeyFrame; + private @ReplacementState int decoderReplacementState; + @Nullable private Format streamFormat; + @Nullable private SubtitleDecoder decoder; + @Nullable private SubtitleInputBuffer nextInputBuffer; + @Nullable private SubtitleOutputBuffer subtitle; + @Nullable private SubtitleOutputBuffer nextSubtitle; + private int nextSubtitleEventIndex; + private long finalStreamEndPositionUs; + + /** + * @param output The output. + * @param outputLooper The looper associated with the thread on which the output should be called. + * If the output makes use of standard Android UI components, then this should normally be the + * looper associated with the application's main thread, which can be obtained using {@link + * android.app.Activity#getMainLooper()}. Null may be passed if the output should be called + * directly on the player's internal rendering thread. + */ + public NonFinalTextRenderer(TextOutput output, @Nullable Looper outputLooper) { + this(output, outputLooper, SubtitleDecoderFactory.DEFAULT); + } + + /** + * @param output The output. + * @param outputLooper The looper associated with the thread on which the output should be called. + * If the output makes use of standard Android UI components, then this should normally be the + * looper associated with the application's main thread, which can be obtained using {@link + * android.app.Activity#getMainLooper()}. Null may be passed if the output should be called + * directly on the player's internal rendering thread. + * @param decoderFactory A factory from which to obtain {@link SubtitleDecoder} instances. + */ + public NonFinalTextRenderer( + TextOutput output, @Nullable Looper outputLooper, SubtitleDecoderFactory decoderFactory) { + super(C.TRACK_TYPE_TEXT); + this.output = checkNotNull(output); + this.outputHandler = + outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this); + this.decoderFactory = decoderFactory; + formatHolder = new FormatHolder(); + finalStreamEndPositionUs = C.TIME_UNSET; + } + + @Override + public String getName() { + return TAG; + } + + @Override + public @Capabilities int supportsFormat(Format format) { + if (decoderFactory.supportsFormat(format)) { + return RendererCapabilities.create( + format.cryptoType == C.CRYPTO_TYPE_NONE ? C.FORMAT_HANDLED : C.FORMAT_UNSUPPORTED_DRM); + } else if (MimeTypes.isText(format.sampleMimeType)) { + return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE); + } else { + return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE); + } + } + + /** + * Sets the position at which to stop rendering the current stream. + * + *
Must be called after {@link #setCurrentStreamFinal()}.
+ *
+ * @param streamEndPositionUs The position to stop rendering at or {@link C#LENGTH_UNSET} to
+ * render until the end of the current stream.
+ */
+ // TODO(internal b/181312195): Remove this when it's no longer needed once subtitles are decoded
+ // on the loading side of SampleQueue.
+ public void setFinalStreamEndPositionUs(long streamEndPositionUs) {
+ checkState(isCurrentStreamFinal());
+ this.finalStreamEndPositionUs = streamEndPositionUs;
+ }
+
+ @Override
+ protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) {
+ streamFormat = formats[0];
+ if (decoder != null) {
+ decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM;
+ } else {
+ initDecoder();
+ }
+ }
+
+ @Override
+ protected void onPositionReset(long positionUs, boolean joining) {
+ clearOutput();
+ inputStreamEnded = false;
+ outputStreamEnded = false;
+ finalStreamEndPositionUs = C.TIME_UNSET;
+ if (decoderReplacementState != REPLACEMENT_STATE_NONE) {
+ replaceDecoder();
+ } else {
+ releaseBuffers();
+ checkNotNull(decoder).flush();
+ }
+ }
+
+ @Override
+ public void render(long positionUs, long elapsedRealtimeUs) {
+ if (isCurrentStreamFinal()
+ && finalStreamEndPositionUs != C.TIME_UNSET
+ && positionUs >= finalStreamEndPositionUs) {
+ releaseBuffers();
+ outputStreamEnded = true;
+ }
+
+ if (outputStreamEnded) {
+ return;
+ }
+
+ if (nextSubtitle == null) {
+ checkNotNull(decoder).setPositionUs(positionUs);
+ try {
+ nextSubtitle = checkNotNull(decoder).dequeueOutputBuffer();
+ } catch (SubtitleDecoderException e) {
+ handleDecoderError(e);
+ return;
+ }
+ }
+
+ if (getState() != STATE_STARTED) {
+ return;
+ }
+
+ boolean textRendererNeedsUpdate = false;
+ if (subtitle != null) {
+ // We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we
+ // advance to the next event.
+ long subtitleNextEventTimeUs = getNextEventTime();
+ while (subtitleNextEventTimeUs <= positionUs) {
+ nextSubtitleEventIndex++;
+ subtitleNextEventTimeUs = getNextEventTime();
+ textRendererNeedsUpdate = true;
+ }
+ }
+ if (nextSubtitle != null) {
+ SubtitleOutputBuffer nextSubtitle = this.nextSubtitle;
+ if (nextSubtitle.isEndOfStream()) {
+ if (!textRendererNeedsUpdate && getNextEventTime() == Long.MAX_VALUE) {
+ if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
+ replaceDecoder();
+ } else {
+ releaseBuffers();
+ outputStreamEnded = true;
+ }
+ }
+ } else if (nextSubtitle.timeUs <= positionUs) {
+ // Advance to the next subtitle. Sync the next event index and trigger an update.
+ if (subtitle != null) {
+ subtitle.release();
+ }
+ nextSubtitleEventIndex = nextSubtitle.getNextEventTimeIndex(positionUs);
+ subtitle = nextSubtitle;
+ this.nextSubtitle = null;
+ textRendererNeedsUpdate = true;
+ }
+ }
+
+ if (textRendererNeedsUpdate) {
+ // If textRendererNeedsUpdate then subtitle must be non-null.
+ checkNotNull(subtitle);
+ // textRendererNeedsUpdate is set and we're playing. Update the renderer.
+ updateOutput(subtitle.getCues(positionUs));
+ }
+
+ if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
+ return;
+ }
+
+ try {
+ while (!inputStreamEnded) {
+ @Nullable SubtitleInputBuffer nextInputBuffer = this.nextInputBuffer;
+ if (nextInputBuffer == null) {
+ nextInputBuffer = checkNotNull(decoder).dequeueInputBuffer();
+ if (nextInputBuffer == null) {
+ return;
+ }
+ this.nextInputBuffer = nextInputBuffer;
+ }
+ if (decoderReplacementState == REPLACEMENT_STATE_SIGNAL_END_OF_STREAM) {
+ nextInputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ checkNotNull(decoder).queueInputBuffer(nextInputBuffer);
+ this.nextInputBuffer = null;
+ decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM;
+ return;
+ }
+ // Try and read the next subtitle from the source.
+ @ReadDataResult int result = readSource(formatHolder, nextInputBuffer, /* readFlags= */ 0);
+ if (result == C.RESULT_BUFFER_READ) {
+ if (nextInputBuffer.isEndOfStream()) {
+ inputStreamEnded = true;
+ waitingForKeyFrame = false;
+ } else {
+ @Nullable Format format = formatHolder.format;
+ if (format == null) {
+ // We haven't received a format yet.
+ return;
+ }
+ nextInputBuffer.subsampleOffsetUs = format.subsampleOffsetUs;
+ nextInputBuffer.flip();
+ waitingForKeyFrame &= !nextInputBuffer.isKeyFrame();
+ }
+ if (!waitingForKeyFrame) {
+ checkNotNull(decoder).queueInputBuffer(nextInputBuffer);
+ this.nextInputBuffer = null;
+ }
+ } else if (result == C.RESULT_NOTHING_READ) {
+ return;
+ }
+ }
+ } catch (SubtitleDecoderException e) {
+ handleDecoderError(e);
+ }
+ }
+
+ @Override
+ protected void onDisabled() {
+ streamFormat = null;
+ finalStreamEndPositionUs = C.TIME_UNSET;
+ clearOutput();
+ releaseDecoder();
+ }
+
+ @Override
+ public boolean isEnded() {
+ return outputStreamEnded;
+ }
+
+ @Override
+ public boolean isReady() {
+ // Don't block playback whilst subtitles are loading.
+ // Note: To change this behavior, it will be necessary to consider [Internal: b/12949941].
+ return true;
+ }
+
+ private void releaseBuffers() {
+ nextInputBuffer = null;
+ nextSubtitleEventIndex = C.INDEX_UNSET;
+ if (subtitle != null) {
+ subtitle.release();
+ subtitle = null;
+ }
+ if (nextSubtitle != null) {
+ nextSubtitle.release();
+ nextSubtitle = null;
+ }
+ }
+
+ private void releaseDecoder() {
+ releaseBuffers();
+ checkNotNull(decoder).release();
+ decoder = null;
+ decoderReplacementState = REPLACEMENT_STATE_NONE;
+ }
+
+ private void initDecoder() {
+ waitingForKeyFrame = true;
+ decoder = decoderFactory.createDecoder(checkNotNull(streamFormat));
+ }
+
+ private void replaceDecoder() {
+ releaseDecoder();
+ initDecoder();
+ }
+
+ private long getNextEventTime() {
+ if (nextSubtitleEventIndex == C.INDEX_UNSET) {
+ return Long.MAX_VALUE;
+ }
+ checkNotNull(subtitle);
+ return nextSubtitleEventIndex >= subtitle.getEventTimeCount()
+ ? Long.MAX_VALUE
+ : subtitle.getEventTime(nextSubtitleEventIndex);
+ }
+
+ private void updateOutput(List Logs {@code e} and resets state to allow decoding the next sample.
+ */
+ private void handleDecoderError(SubtitleDecoderException e) {
+ Log.e(TAG, "Subtitle decoding failed. streamFormat=" + streamFormat, e);
+ clearOutput();
+ replaceDecoder();
+ }
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.kt
deleted file mode 100644
index 7acb7c34..00000000
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.kt
+++ /dev/null
@@ -1,383 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.lagradost.cloudstream3.ui.player
-
-import android.os.Handler
-import android.os.Looper
-import android.os.Message
-import androidx.annotation.IntDef
-import com.google.android.exoplayer2.*
-import com.google.android.exoplayer2.source.SampleStream.ReadDataResult
-import com.google.android.exoplayer2.text.*
-import com.google.android.exoplayer2.text.Cue.DIMEN_UNSET
-import com.google.android.exoplayer2.text.Cue.LINE_TYPE_NUMBER
-import com.google.android.exoplayer2.util.Assertions
-import com.google.android.exoplayer2.util.Log
-import com.google.android.exoplayer2.util.MimeTypes
-import com.google.android.exoplayer2.util.Util
-
-/**
- * A renderer for text.
- *
- *
- * [Subtitle]s are decoded from sample data using [SubtitleDecoder] instances
- * obtained from a [SubtitleDecoderFactory]. The actual rendering of the subtitle [Cue]s
- * is delegated to a [TextOutput].
- */
-open class NonFinalTextRenderer @JvmOverloads constructor(
- output: TextOutput?,
- outputLooper: Looper?,
- private val decoderFactory: SubtitleDecoderFactory = SubtitleDecoderFactory.DEFAULT
-) :
- BaseRenderer(C.TRACK_TYPE_TEXT), Handler.Callback {
- @MustBeDocumented
- @kotlin.annotation.Retention(AnnotationRetention.SOURCE)
- @IntDef(
- REPLACEMENT_STATE_NONE,
- REPLACEMENT_STATE_SIGNAL_END_OF_STREAM,
- REPLACEMENT_STATE_WAIT_END_OF_STREAM
- )
- private annotation class ReplacementState
-
- private val outputHandler: Handler? = if (outputLooper == null) null else Util.createHandler(
- outputLooper, /* callback= */
- this
- )
- private val output: TextOutput = Assertions.checkNotNull(output)
- private val formatHold: FormatHolder = FormatHolder()
- private var inputStreamEnded = false
- private var outputStreamEnded = false
- private var waitingForKeyFrame = false
-
- @ReplacementState
- private var decoderReplacementState = 0
- private var streamFormat: Format? = null
- private var decoder: SubtitleDecoder? = null
- private var nextInputBuffer: SubtitleInputBuffer? = null
- private var subtitle: SubtitleOutputBuffer? = null
- private var nextSubtitle: SubtitleOutputBuffer? = null
- private var nextSubtitleEventIndex = 0
- private var finalStreamEndPositionUs: Long
- override fun getName(): String {
- return TAG
- }
-
-// @RendererCapabilities.Capabilities
- override fun supportsFormat(format: Format): Int {
- return if (decoderFactory.supportsFormat(format)) {
- RendererCapabilities.create(
- if (format.cryptoType == C.CRYPTO_TYPE_NONE) C.FORMAT_HANDLED else C.FORMAT_UNSUPPORTED_DRM
- )
- } else if (MimeTypes.isText(format.sampleMimeType)) {
- RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE)
- } else {
- RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE)
- }
- }
-
- /**
- * Sets the position at which to stop rendering the current stream.
- *
- *
- * Must be called after [.setCurrentStreamFinal].
- *
- * @param streamEndPositionUs The position to stop rendering at or [C.LENGTH_UNSET] to
- * render until the end of the current stream.
- */
-
- override fun onStreamChanged(formats: Array