| /* |
| * Copyright (C) 2019 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.google.android.exoplayer2.video; |
| |
| import android.os.Handler; |
| import android.os.SystemClock; |
| import android.view.Surface; |
| import androidx.annotation.CallSuper; |
| 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.ExoPlaybackException; |
| import com.google.android.exoplayer2.Format; |
| import com.google.android.exoplayer2.FormatHolder; |
| import com.google.android.exoplayer2.RendererCapabilities; |
| import com.google.android.exoplayer2.decoder.DecoderCounters; |
| import com.google.android.exoplayer2.decoder.DecoderInputBuffer; |
| import com.google.android.exoplayer2.decoder.SimpleDecoder; |
| import com.google.android.exoplayer2.drm.DrmSession; |
| import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; |
| import com.google.android.exoplayer2.drm.DrmSessionManager; |
| import com.google.android.exoplayer2.drm.ExoMediaCrypto; |
| import com.google.android.exoplayer2.util.Assertions; |
| import com.google.android.exoplayer2.util.TimedValueQueue; |
| import com.google.android.exoplayer2.util.TraceUtil; |
| import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher; |
| import java.lang.annotation.Documented; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| |
| /** Decodes and renders video using a {@link SimpleDecoder}. */ |
| public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { |
| |
| /** Decoder reinitialization states. */ |
| @Documented |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({ |
| REINITIALIZATION_STATE_NONE, |
| REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM, |
| REINITIALIZATION_STATE_WAIT_END_OF_STREAM |
| }) |
| private @interface ReinitializationState {} |
| /** The decoder does not need to be re-initialized. */ |
| private static final int REINITIALIZATION_STATE_NONE = 0; |
| /** |
| * The input format has changed in a way that requires the decoder to be re-initialized, 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 REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1; |
| /** |
| * The input format has changed in a way that requires the decoder to be re-initialized, 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 REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2; |
| |
| private final long allowedJoiningTimeMs; |
| private final int maxDroppedFramesToNotify; |
| private final boolean playClearSamplesWithoutKeys; |
| private final EventDispatcher eventDispatcher; |
| private final TimedValueQueue<Format> formatQueue; |
| private final DecoderInputBuffer flagsOnlyBuffer; |
| private final DrmSessionManager<ExoMediaCrypto> drmSessionManager; |
| |
| private Format inputFormat; |
| private Format outputFormat; |
| private SimpleDecoder< |
| VideoDecoderInputBuffer, |
| ? extends VideoDecoderOutputBuffer, |
| ? extends VideoDecoderException> |
| decoder; |
| private VideoDecoderInputBuffer inputBuffer; |
| private VideoDecoderOutputBuffer outputBuffer; |
| @Nullable private Surface surface; |
| @Nullable private VideoDecoderOutputBufferRenderer outputBufferRenderer; |
| @C.VideoOutputMode private int outputMode; |
| |
| @Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession; |
| @Nullable private DrmSession<ExoMediaCrypto> sourceDrmSession; |
| |
| @ReinitializationState private int decoderReinitializationState; |
| private boolean decoderReceivedBuffers; |
| |
| private boolean renderedFirstFrame; |
| private long initialPositionUs; |
| private long joiningDeadlineMs; |
| private boolean waitingForKeys; |
| private boolean waitingForFirstSampleInFormat; |
| |
| private boolean inputStreamEnded; |
| private boolean outputStreamEnded; |
| private int reportedWidth; |
| private int reportedHeight; |
| |
| private long droppedFrameAccumulationStartTimeMs; |
| private int droppedFrames; |
| private int consecutiveDroppedFrameCount; |
| private int buffersInCodecCount; |
| private long lastRenderTimeUs; |
| private long outputStreamOffsetUs; |
| |
| /** Decoder event counters used for debugging purposes. */ |
| protected DecoderCounters decoderCounters; |
| |
| /** |
| * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer |
| * can attempt to seamlessly join an ongoing playback. |
| * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be |
| * null if delivery of events is not required. |
| * @param eventListener A listener of events. May be null if delivery of events is not required. |
| * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between |
| * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. |
| * @param drmSessionManager For use with encrypted media. May be null if support for encrypted |
| * media is not required. |
| * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. |
| * For example a media file may start with a short clear region so as to allow playback to |
| * begin in parallel with key acquisition. This parameter specifies whether the renderer is |
| * permitted to play clear regions of encrypted media files before {@code drmSessionManager} |
| * has obtained the keys necessary to decrypt encrypted regions of the media. |
| */ |
| protected SimpleDecoderVideoRenderer( |
| long allowedJoiningTimeMs, |
| @Nullable Handler eventHandler, |
| @Nullable VideoRendererEventListener eventListener, |
| int maxDroppedFramesToNotify, |
| @Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, |
| boolean playClearSamplesWithoutKeys) { |
| super(C.TRACK_TYPE_VIDEO); |
| this.allowedJoiningTimeMs = allowedJoiningTimeMs; |
| this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; |
| this.drmSessionManager = drmSessionManager; |
| this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; |
| joiningDeadlineMs = C.TIME_UNSET; |
| clearReportedVideoSize(); |
| formatQueue = new TimedValueQueue<>(); |
| flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); |
| eventDispatcher = new EventDispatcher(eventHandler, eventListener); |
| decoderReinitializationState = REINITIALIZATION_STATE_NONE; |
| outputMode = C.VIDEO_OUTPUT_MODE_NONE; |
| } |
| |
| // BaseRenderer implementation. |
| |
| @Override |
| @Capabilities |
| public final int supportsFormat(Format format) { |
| return supportsFormatInternal(drmSessionManager, format); |
| } |
| |
| @Override |
| public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { |
| if (outputStreamEnded) { |
| return; |
| } |
| |
| if (inputFormat == null) { |
| // We don't have a format yet, so try and read one. |
| FormatHolder formatHolder = getFormatHolder(); |
| flagsOnlyBuffer.clear(); |
| int result = readSource(formatHolder, flagsOnlyBuffer, true); |
| if (result == C.RESULT_FORMAT_READ) { |
| onInputFormatChanged(formatHolder); |
| } else if (result == C.RESULT_BUFFER_READ) { |
| // End of stream read having not read a format. |
| Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); |
| inputStreamEnded = true; |
| outputStreamEnded = true; |
| return; |
| } else { |
| // We still don't have a format and can't make progress without one. |
| return; |
| } |
| } |
| |
| // If we don't have a decoder yet, we need to instantiate one. |
| maybeInitDecoder(); |
| |
| if (decoder != null) { |
| try { |
| // Rendering loop. |
| TraceUtil.beginSection("drainAndFeed"); |
| while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} |
| while (feedInputBuffer()) {} |
| TraceUtil.endSection(); |
| } catch (VideoDecoderException e) { |
| throw createRendererException(e, inputFormat); |
| } |
| decoderCounters.ensureUpdated(); |
| } |
| } |
| |
| @Override |
| public boolean isEnded() { |
| return outputStreamEnded; |
| } |
| |
| @Override |
| public boolean isReady() { |
| if (waitingForKeys) { |
| return false; |
| } |
| if (inputFormat != null |
| && (isSourceReady() || outputBuffer != null) |
| && (renderedFirstFrame || !hasOutput())) { |
| // Ready. If we were joining then we've now joined, so clear the joining deadline. |
| joiningDeadlineMs = C.TIME_UNSET; |
| return true; |
| } else if (joiningDeadlineMs == C.TIME_UNSET) { |
| // Not joining. |
| return false; |
| } else if (SystemClock.elapsedRealtime() < joiningDeadlineMs) { |
| // Joining and still within the joining deadline. |
| return true; |
| } else { |
| // The joining deadline has been exceeded. Give up and clear the deadline. |
| joiningDeadlineMs = C.TIME_UNSET; |
| return false; |
| } |
| } |
| |
| // Protected methods. |
| |
| @Override |
| protected void onEnabled(boolean joining) throws ExoPlaybackException { |
| decoderCounters = new DecoderCounters(); |
| eventDispatcher.enabled(decoderCounters); |
| } |
| |
| @Override |
| protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { |
| inputStreamEnded = false; |
| outputStreamEnded = false; |
| clearRenderedFirstFrame(); |
| initialPositionUs = C.TIME_UNSET; |
| consecutiveDroppedFrameCount = 0; |
| if (decoder != null) { |
| flushDecoder(); |
| } |
| if (joining) { |
| setJoiningDeadlineMs(); |
| } else { |
| joiningDeadlineMs = C.TIME_UNSET; |
| } |
| formatQueue.clear(); |
| } |
| |
| @Override |
| protected void onStarted() { |
| droppedFrames = 0; |
| droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); |
| lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; |
| } |
| |
| @Override |
| protected void onStopped() { |
| joiningDeadlineMs = C.TIME_UNSET; |
| maybeNotifyDroppedFrames(); |
| } |
| |
| @Override |
| protected void onDisabled() { |
| inputFormat = null; |
| waitingForKeys = false; |
| clearReportedVideoSize(); |
| clearRenderedFirstFrame(); |
| try { |
| setSourceDrmSession(null); |
| releaseDecoder(); |
| } finally { |
| eventDispatcher.disabled(decoderCounters); |
| } |
| } |
| |
| @Override |
| protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { |
| outputStreamOffsetUs = offsetUs; |
| super.onStreamChanged(formats, offsetUs); |
| } |
| |
| /** |
| * Called when a decoder has been created and configured. |
| * |
| * <p>The default implementation is a no-op. |
| * |
| * @param name The name of the decoder that was initialized. |
| * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization |
| * finished. |
| * @param initializationDurationMs The time taken to initialize the decoder, in milliseconds. |
| */ |
| @CallSuper |
| protected void onDecoderInitialized( |
| String name, long initializedTimestampMs, long initializationDurationMs) { |
| eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); |
| } |
| |
| /** |
| * Flushes the decoder. |
| * |
| * @throws ExoPlaybackException If an error occurs reinitializing a decoder. |
| */ |
| @CallSuper |
| protected void flushDecoder() throws ExoPlaybackException { |
| waitingForKeys = false; |
| buffersInCodecCount = 0; |
| if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { |
| releaseDecoder(); |
| maybeInitDecoder(); |
| } else { |
| inputBuffer = null; |
| if (outputBuffer != null) { |
| outputBuffer.release(); |
| outputBuffer = null; |
| } |
| decoder.flush(); |
| decoderReceivedBuffers = false; |
| } |
| } |
| |
| /** Releases the decoder. */ |
| @CallSuper |
| protected void releaseDecoder() { |
| inputBuffer = null; |
| outputBuffer = null; |
| decoderReinitializationState = REINITIALIZATION_STATE_NONE; |
| decoderReceivedBuffers = false; |
| buffersInCodecCount = 0; |
| if (decoder != null) { |
| decoder.release(); |
| decoder = null; |
| decoderCounters.decoderReleaseCount++; |
| } |
| setDecoderDrmSession(null); |
| } |
| |
| /** |
| * Called when a new format is read from the upstream source. |
| * |
| * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}. |
| * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder. |
| */ |
| @CallSuper |
| @SuppressWarnings("unchecked") |
| protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { |
| waitingForFirstSampleInFormat = true; |
| Format newFormat = Assertions.checkNotNull(formatHolder.format); |
| if (formatHolder.includesDrmSession) { |
| setSourceDrmSession((DrmSession<ExoMediaCrypto>) formatHolder.drmSession); |
| } else { |
| sourceDrmSession = |
| getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession); |
| } |
| inputFormat = newFormat; |
| |
| if (sourceDrmSession != decoderDrmSession) { |
| if (decoderReceivedBuffers) { |
| // Signal end of stream and wait for any final output buffers before re-initialization. |
| decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; |
| } else { |
| // There aren't any final output buffers, so release the decoder immediately. |
| releaseDecoder(); |
| maybeInitDecoder(); |
| } |
| } |
| |
| eventDispatcher.inputFormatChanged(inputFormat); |
| } |
| |
| /** |
| * Called immediately before an input buffer is queued into the decoder. |
| * |
| * <p>The default implementation is a no-op. |
| * |
| * @param buffer The buffer that will be queued. |
| */ |
| protected void onQueueInputBuffer(VideoDecoderInputBuffer buffer) { |
| // Do nothing. |
| } |
| |
| /** |
| * Called when an output buffer is successfully processed. |
| * |
| * @param presentationTimeUs The timestamp associated with the output buffer. |
| */ |
| @CallSuper |
| protected void onProcessedOutputBuffer(long presentationTimeUs) { |
| buffersInCodecCount--; |
| } |
| |
| /** |
| * Returns whether the buffer being processed should be dropped. |
| * |
| * @param earlyUs The time until the buffer should be presented in microseconds. A negative value |
| * indicates that the buffer is late. |
| * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, |
| * measured at the start of the current iteration of the rendering loop. |
| */ |
| protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) { |
| return isBufferLate(earlyUs); |
| } |
| |
| /** |
| * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after |
| * the current playback position, if possible. |
| * |
| * @param earlyUs The time until the current buffer should be presented in microseconds. A |
| * negative value indicates that the buffer is late. |
| * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, |
| * measured at the start of the current iteration of the rendering loop. |
| */ |
| protected boolean shouldDropBuffersToKeyframe(long earlyUs, long elapsedRealtimeUs) { |
| return isBufferVeryLate(earlyUs); |
| } |
| |
| /** |
| * Returns whether to force rendering an output buffer. |
| * |
| * @param earlyUs The time until the current buffer should be presented in microseconds. A |
| * negative value indicates that the buffer is late. |
| * @param elapsedSinceLastRenderUs The elapsed time since the last output buffer was rendered, in |
| * microseconds. |
| * @return Returns whether to force rendering an output buffer. |
| */ |
| protected boolean shouldForceRenderOutputBuffer(long earlyUs, long elapsedSinceLastRenderUs) { |
| return isBufferLate(earlyUs) && elapsedSinceLastRenderUs > 100000; |
| } |
| |
| /** |
| * Skips the specified output buffer and releases it. |
| * |
| * @param outputBuffer The output buffer to skip. |
| */ |
| protected void skipOutputBuffer(VideoDecoderOutputBuffer outputBuffer) { |
| decoderCounters.skippedOutputBufferCount++; |
| outputBuffer.release(); |
| } |
| |
| /** |
| * Drops the specified output buffer and releases it. |
| * |
| * @param outputBuffer The output buffer to drop. |
| */ |
| protected void dropOutputBuffer(VideoDecoderOutputBuffer outputBuffer) { |
| updateDroppedBufferCounters(1); |
| outputBuffer.release(); |
| } |
| |
| /** |
| * Drops frames from the current output buffer to the next keyframe at or before the playback |
| * position. If no such keyframe exists, as the playback position is inside the same group of |
| * pictures as the buffer being processed, returns {@code false}. Returns {@code true} otherwise. |
| * |
| * @param positionUs The current playback position, in microseconds. |
| * @return Whether any buffers were dropped. |
| * @throws ExoPlaybackException If an error occurs flushing the decoder. |
| */ |
| protected boolean maybeDropBuffersToKeyframe(long positionUs) throws ExoPlaybackException { |
| int droppedSourceBufferCount = skipSource(positionUs); |
| if (droppedSourceBufferCount == 0) { |
| return false; |
| } |
| decoderCounters.droppedToKeyframeCount++; |
| // We dropped some buffers to catch up, so update the decoder counters and flush the decoder, |
| // which releases all pending buffers buffers including the current output buffer. |
| updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount); |
| flushDecoder(); |
| return true; |
| } |
| |
| /** |
| * Updates decoder counters to reflect that {@code droppedBufferCount} additional buffers were |
| * dropped. |
| * |
| * @param droppedBufferCount The number of additional dropped buffers. |
| */ |
| protected void updateDroppedBufferCounters(int droppedBufferCount) { |
| decoderCounters.droppedBufferCount += droppedBufferCount; |
| droppedFrames += droppedBufferCount; |
| consecutiveDroppedFrameCount += droppedBufferCount; |
| decoderCounters.maxConsecutiveDroppedBufferCount = |
| Math.max(consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedBufferCount); |
| if (maxDroppedFramesToNotify > 0 && droppedFrames >= maxDroppedFramesToNotify) { |
| maybeNotifyDroppedFrames(); |
| } |
| } |
| |
| /** |
| * Returns the {@link Capabilities} for the given {@link Format}. |
| * |
| * @param drmSessionManager The renderer's {@link DrmSessionManager}. |
| * @param format The format, which has a video {@link Format#sampleMimeType}. |
| * @return The {@link Capabilities} for this {@link Format}. |
| * @see RendererCapabilities#supportsFormat(Format) |
| */ |
| @Capabilities |
| protected abstract int supportsFormatInternal( |
| @Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format); |
| |
| /** |
| * Creates a decoder for the given format. |
| * |
| * @param format The format for which a decoder is required. |
| * @param mediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted content. |
| * May be null and can be ignored if decoder does not handle encrypted content. |
| * @return The decoder. |
| * @throws VideoDecoderException If an error occurred creating a suitable decoder. |
| */ |
| protected abstract SimpleDecoder< |
| VideoDecoderInputBuffer, |
| ? extends VideoDecoderOutputBuffer, |
| ? extends VideoDecoderException> |
| createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) |
| throws VideoDecoderException; |
| |
| /** |
| * Renders the specified output buffer. |
| * |
| * <p>The implementation of this method takes ownership of the output buffer and is responsible |
| * for calling {@link VideoDecoderOutputBuffer#release()} either immediately or in the future. |
| * |
| * @param outputBuffer {@link VideoDecoderOutputBuffer} to render. |
| * @param presentationTimeUs Presentation time in microseconds. |
| * @param outputFormat Output {@link Format}. |
| * @throws VideoDecoderException If an error occurs when rendering the output buffer. |
| */ |
| protected void renderOutputBuffer( |
| VideoDecoderOutputBuffer outputBuffer, long presentationTimeUs, Format outputFormat) |
| throws VideoDecoderException { |
| lastRenderTimeUs = C.msToUs(SystemClock.elapsedRealtime() * 1000); |
| int bufferMode = outputBuffer.mode; |
| boolean renderSurface = bufferMode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && surface != null; |
| boolean renderYuv = bufferMode == C.VIDEO_OUTPUT_MODE_YUV && outputBufferRenderer != null; |
| if (!renderYuv && !renderSurface) { |
| dropOutputBuffer(outputBuffer); |
| } else { |
| maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height); |
| if (renderYuv) { |
| outputBufferRenderer.setOutputBuffer(outputBuffer); |
| } else { |
| renderOutputBufferToSurface(outputBuffer, surface); |
| } |
| consecutiveDroppedFrameCount = 0; |
| decoderCounters.renderedOutputBufferCount++; |
| maybeNotifyRenderedFirstFrame(); |
| } |
| } |
| |
| /** |
| * Renders the specified output buffer to the passed surface. |
| * |
| * <p>The implementation of this method takes ownership of the output buffer and is responsible |
| * for calling {@link VideoDecoderOutputBuffer#release()} either immediately or in the future. |
| * |
| * @param outputBuffer {@link VideoDecoderOutputBuffer} to render. |
| * @param surface Output {@link Surface}. |
| * @throws VideoDecoderException If an error occurs when rendering the output buffer. |
| */ |
| protected abstract void renderOutputBufferToSurface( |
| VideoDecoderOutputBuffer outputBuffer, Surface surface) throws VideoDecoderException; |
| |
| /** |
| * Sets output surface. |
| * |
| * @param surface Surface. |
| */ |
| protected final void setOutputSurface(@Nullable Surface surface) { |
| if (this.surface != surface) { |
| // The output has changed. |
| this.surface = surface; |
| if (surface != null) { |
| outputBufferRenderer = null; |
| outputMode = C.VIDEO_OUTPUT_MODE_SURFACE_YUV; |
| if (decoder != null) { |
| setDecoderOutputMode(outputMode); |
| } |
| onOutputChanged(); |
| } else { |
| // The output has been removed. We leave the outputMode of the underlying decoder unchanged |
| // in anticipation that a subsequent output will likely be of the same type. |
| outputMode = C.VIDEO_OUTPUT_MODE_NONE; |
| onOutputRemoved(); |
| } |
| } else if (surface != null) { |
| // The output is unchanged and non-null. |
| onOutputReset(); |
| } |
| } |
| |
| /** |
| * Sets output buffer renderer. |
| * |
| * @param outputBufferRenderer Output buffer renderer. |
| */ |
| protected final void setOutputBufferRenderer( |
| @Nullable VideoDecoderOutputBufferRenderer outputBufferRenderer) { |
| if (this.outputBufferRenderer != outputBufferRenderer) { |
| // The output has changed. |
| this.outputBufferRenderer = outputBufferRenderer; |
| if (outputBufferRenderer != null) { |
| surface = null; |
| outputMode = C.VIDEO_OUTPUT_MODE_YUV; |
| if (decoder != null) { |
| setDecoderOutputMode(outputMode); |
| } |
| onOutputChanged(); |
| } else { |
| // The output has been removed. We leave the outputMode of the underlying decoder unchanged |
| // in anticipation that a subsequent output will likely be of the same type. |
| outputMode = C.VIDEO_OUTPUT_MODE_NONE; |
| onOutputRemoved(); |
| } |
| } else if (outputBufferRenderer != null) { |
| // The output is unchanged and non-null. |
| onOutputReset(); |
| } |
| } |
| |
| /** |
| * Sets output mode of the decoder. |
| * |
| * @param outputMode Output mode. |
| */ |
| protected abstract void setDecoderOutputMode(@C.VideoOutputMode int outputMode); |
| |
| // Internal methods. |
| |
| private void setSourceDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) { |
| DrmSession.replaceSession(sourceDrmSession, session); |
| sourceDrmSession = session; |
| } |
| |
| private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) { |
| DrmSession.replaceSession(decoderDrmSession, session); |
| decoderDrmSession = session; |
| } |
| |
| private void maybeInitDecoder() throws ExoPlaybackException { |
| if (decoder != null) { |
| return; |
| } |
| |
| setDecoderDrmSession(sourceDrmSession); |
| |
| ExoMediaCrypto mediaCrypto = null; |
| if (decoderDrmSession != null) { |
| mediaCrypto = decoderDrmSession.getMediaCrypto(); |
| if (mediaCrypto == null) { |
| DrmSessionException drmError = decoderDrmSession.getError(); |
| if (drmError != null) { |
| // Continue for now. We may be able to avoid failure if the session recovers, or if a new |
| // input format causes the session to be replaced before it's used. |
| } else { |
| // The drm session isn't open yet. |
| return; |
| } |
| } |
| } |
| |
| try { |
| long decoderInitializingTimestamp = SystemClock.elapsedRealtime(); |
| decoder = createDecoder(inputFormat, mediaCrypto); |
| setDecoderOutputMode(outputMode); |
| long decoderInitializedTimestamp = SystemClock.elapsedRealtime(); |
| onDecoderInitialized( |
| decoder.getName(), |
| decoderInitializedTimestamp, |
| decoderInitializedTimestamp - decoderInitializingTimestamp); |
| decoderCounters.decoderInitCount++; |
| } catch (VideoDecoderException e) { |
| throw createRendererException(e, inputFormat); |
| } |
| } |
| |
| private boolean feedInputBuffer() throws VideoDecoderException, ExoPlaybackException { |
| if (decoder == null |
| || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM |
| || inputStreamEnded) { |
| // We need to reinitialize the decoder or the input stream has ended. |
| return false; |
| } |
| |
| if (inputBuffer == null) { |
| inputBuffer = decoder.dequeueInputBuffer(); |
| if (inputBuffer == null) { |
| return false; |
| } |
| } |
| |
| if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) { |
| inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); |
| decoder.queueInputBuffer(inputBuffer); |
| inputBuffer = null; |
| decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM; |
| return false; |
| } |
| |
| int result; |
| FormatHolder formatHolder = getFormatHolder(); |
| if (waitingForKeys) { |
| // We've already read an encrypted sample into buffer, and are waiting for keys. |
| result = C.RESULT_BUFFER_READ; |
| } else { |
| result = readSource(formatHolder, inputBuffer, false); |
| } |
| |
| if (result == C.RESULT_NOTHING_READ) { |
| return false; |
| } |
| if (result == C.RESULT_FORMAT_READ) { |
| onInputFormatChanged(formatHolder); |
| return true; |
| } |
| if (inputBuffer.isEndOfStream()) { |
| inputStreamEnded = true; |
| decoder.queueInputBuffer(inputBuffer); |
| inputBuffer = null; |
| return false; |
| } |
| boolean bufferEncrypted = inputBuffer.isEncrypted(); |
| waitingForKeys = shouldWaitForKeys(bufferEncrypted); |
| if (waitingForKeys) { |
| return false; |
| } |
| if (waitingForFirstSampleInFormat) { |
| formatQueue.add(inputBuffer.timeUs, inputFormat); |
| waitingForFirstSampleInFormat = false; |
| } |
| inputBuffer.flip(); |
| inputBuffer.colorInfo = inputFormat.colorInfo; |
| onQueueInputBuffer(inputBuffer); |
| decoder.queueInputBuffer(inputBuffer); |
| buffersInCodecCount++; |
| decoderReceivedBuffers = true; |
| decoderCounters.inputBufferCount++; |
| inputBuffer = null; |
| return true; |
| } |
| |
| /** |
| * Attempts to dequeue an output buffer from the decoder and, if successful, passes it to {@link |
| * #processOutputBuffer(long, long)}. |
| * |
| * @param positionUs The player's current position. |
| * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, |
| * measured at the start of the current iteration of the rendering loop. |
| * @return Whether it may be possible to drain more output data. |
| * @throws ExoPlaybackException If an error occurs draining the output buffer. |
| */ |
| private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) |
| throws ExoPlaybackException, VideoDecoderException { |
| if (outputBuffer == null) { |
| outputBuffer = decoder.dequeueOutputBuffer(); |
| if (outputBuffer == null) { |
| return false; |
| } |
| decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; |
| buffersInCodecCount -= outputBuffer.skippedOutputBufferCount; |
| } |
| |
| if (outputBuffer.isEndOfStream()) { |
| if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { |
| // We're waiting to re-initialize the decoder, and have now processed all final buffers. |
| releaseDecoder(); |
| maybeInitDecoder(); |
| } else { |
| outputBuffer.release(); |
| outputBuffer = null; |
| outputStreamEnded = true; |
| } |
| return false; |
| } |
| |
| boolean processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs); |
| if (processedOutputBuffer) { |
| onProcessedOutputBuffer(outputBuffer.timeUs); |
| outputBuffer = null; |
| } |
| return processedOutputBuffer; |
| } |
| |
| /** |
| * Processes {@link #outputBuffer} by rendering it, skipping it or doing nothing, and returns |
| * whether it may be possible to process another output buffer. |
| * |
| * @param positionUs The player's current position. |
| * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, |
| * measured at the start of the current iteration of the rendering loop. |
| * @return Whether it may be possible to drain another output buffer. |
| * @throws ExoPlaybackException If an error occurs processing the output buffer. |
| */ |
| private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs) |
| throws ExoPlaybackException, VideoDecoderException { |
| if (initialPositionUs == C.TIME_UNSET) { |
| initialPositionUs = positionUs; |
| } |
| |
| long earlyUs = outputBuffer.timeUs - positionUs; |
| if (!hasOutput()) { |
| // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. |
| if (isBufferLate(earlyUs)) { |
| skipOutputBuffer(outputBuffer); |
| return true; |
| } |
| return false; |
| } |
| |
| long presentationTimeUs = outputBuffer.timeUs - outputStreamOffsetUs; |
| Format format = formatQueue.pollFloor(presentationTimeUs); |
| if (format != null) { |
| outputFormat = format; |
| } |
| |
| long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000; |
| boolean isStarted = getState() == STATE_STARTED; |
| if (!renderedFirstFrame |
| || (isStarted |
| && shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) { |
| renderOutputBuffer(outputBuffer, presentationTimeUs, outputFormat); |
| return true; |
| } |
| |
| if (!isStarted || positionUs == initialPositionUs) { |
| return false; |
| } |
| |
| if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs) |
| && maybeDropBuffersToKeyframe(positionUs)) { |
| return false; |
| } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { |
| dropOutputBuffer(outputBuffer); |
| return true; |
| } |
| |
| if (earlyUs < 30000) { |
| renderOutputBuffer(outputBuffer, presentationTimeUs, outputFormat); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private boolean hasOutput() { |
| return outputMode != C.VIDEO_OUTPUT_MODE_NONE; |
| } |
| |
| private void onOutputChanged() { |
| // If we know the video size, report it again immediately. |
| maybeRenotifyVideoSizeChanged(); |
| // We haven't rendered to the new output yet. |
| clearRenderedFirstFrame(); |
| if (getState() == STATE_STARTED) { |
| setJoiningDeadlineMs(); |
| } |
| } |
| |
| private void onOutputRemoved() { |
| clearReportedVideoSize(); |
| clearRenderedFirstFrame(); |
| } |
| |
| private void onOutputReset() { |
| // The output is unchanged and non-null. If we know the video size and/or have already |
| // rendered to the output, report these again immediately. |
| maybeRenotifyVideoSizeChanged(); |
| maybeRenotifyRenderedFirstFrame(); |
| } |
| |
| private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { |
| if (decoderDrmSession == null |
| || (!bufferEncrypted |
| && (playClearSamplesWithoutKeys || decoderDrmSession.playClearSamplesWithoutKeys()))) { |
| return false; |
| } |
| @DrmSession.State int drmSessionState = decoderDrmSession.getState(); |
| if (drmSessionState == DrmSession.STATE_ERROR) { |
| throw createRendererException(decoderDrmSession.getError(), inputFormat); |
| } |
| return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; |
| } |
| |
| private void setJoiningDeadlineMs() { |
| joiningDeadlineMs = |
| allowedJoiningTimeMs > 0 |
| ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) |
| : C.TIME_UNSET; |
| } |
| |
| private void clearRenderedFirstFrame() { |
| renderedFirstFrame = false; |
| } |
| |
| private void maybeNotifyRenderedFirstFrame() { |
| if (!renderedFirstFrame) { |
| renderedFirstFrame = true; |
| eventDispatcher.renderedFirstFrame(surface); |
| } |
| } |
| |
| private void maybeRenotifyRenderedFirstFrame() { |
| if (renderedFirstFrame) { |
| eventDispatcher.renderedFirstFrame(surface); |
| } |
| } |
| |
| private void clearReportedVideoSize() { |
| reportedWidth = Format.NO_VALUE; |
| reportedHeight = Format.NO_VALUE; |
| } |
| |
| private void maybeNotifyVideoSizeChanged(int width, int height) { |
| if (reportedWidth != width || reportedHeight != height) { |
| reportedWidth = width; |
| reportedHeight = height; |
| eventDispatcher.videoSizeChanged( |
| width, height, /* unappliedRotationDegrees= */ 0, /* pixelWidthHeightRatio= */ 1); |
| } |
| } |
| |
| private void maybeRenotifyVideoSizeChanged() { |
| if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) { |
| eventDispatcher.videoSizeChanged( |
| reportedWidth, |
| reportedHeight, |
| /* unappliedRotationDegrees= */ 0, |
| /* pixelWidthHeightRatio= */ 1); |
| } |
| } |
| |
| private void maybeNotifyDroppedFrames() { |
| if (droppedFrames > 0) { |
| long now = SystemClock.elapsedRealtime(); |
| long elapsedMs = now - droppedFrameAccumulationStartTimeMs; |
| eventDispatcher.droppedFrames(droppedFrames, elapsedMs); |
| droppedFrames = 0; |
| droppedFrameAccumulationStartTimeMs = now; |
| } |
| } |
| |
| private static boolean isBufferLate(long earlyUs) { |
| // Class a buffer as late if it should have been presented more than 30 ms ago. |
| return earlyUs < -30000; |
| } |
| |
| private static boolean isBufferVeryLate(long earlyUs) { |
| // Class a buffer as very late if it should have been presented more than 500 ms ago. |
| return earlyUs < -500000; |
| } |
| } |