mirror of
				https://github.com/recloudstream/cloudstream.git
				synced 2024-08-15 01:53:11 +00:00 
			
		
		
		
	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…
	
	Add table
		Add a link
		
	
		Reference in a new issue