diff options
Diffstat (limited to '')
-rw-r--r-- | mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java | 1873 |
1 files changed, 1873 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java new file mode 100644 index 0000000000..1627b70a28 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -0,0 +1,1873 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.video; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Point; +import android.media.MediaCodec; +import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaCodecInfo.CodecProfileLevel; +import android.media.MediaCrypto; +import android.media.MediaFormat; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.util.Pair; +import android.view.Surface; +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.PlayerMessage.Target; +import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecSelector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaFormatUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TraceUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; + +/** + * Decodes and renders video using {@link MediaCodec}. + * + * <p>This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)} + * on the playback thread: + * + * <ul> + * <li>Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload + * should be the target {@link Surface}, or null. + * <li>Message with type {@link C#MSG_SET_SCALING_MODE} to set the video scaling mode. The message + * payload should be one of the integer scaling modes in {@link C.VideoScalingMode}. Note that + * the scaling mode only applies if the {@link Surface} targeted by this renderer is owned by + * a {@link android.view.SurfaceView}. + * </ul> + */ +public class MediaCodecVideoRenderer extends MediaCodecRenderer { + + private static final String TAG = "MediaCodecVideoRenderer"; + private static final String KEY_CROP_LEFT = "crop-left"; + private static final String KEY_CROP_RIGHT = "crop-right"; + private static final String KEY_CROP_BOTTOM = "crop-bottom"; + private static final String KEY_CROP_TOP = "crop-top"; + + // Long edge length in pixels for standard video formats, in decreasing in order. + private static final int[] STANDARD_LONG_EDGE_VIDEO_PX = new int[] { + 1920, 1600, 1440, 1280, 960, 854, 640, 540, 480}; + + // Generally there is zero or one pending output stream offset. We track more offsets to allow for + // pending output streams that have fewer frames than the codec latency. + private static final int MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT = 10; + /** + * Scale factor for the initial maximum input size used to configure the codec in non-adaptive + * playbacks. See {@link #getCodecMaxValues(MediaCodecInfo, Format, Format[])}. + */ + private static final float INITIAL_FORMAT_MAX_INPUT_SIZE_SCALE_FACTOR = 1.5f; + + /** Magic frame render timestamp that indicates the EOS in tunneling mode. */ + private static final long TUNNELING_EOS_PRESENTATION_TIME_US = Long.MAX_VALUE; + + /** A {@link DecoderException} with additional surface information. */ + public static final class VideoDecoderException extends DecoderException { + + /** The {@link System#identityHashCode(Object)} of the surface when the exception occurred. */ + public final int surfaceIdentityHashCode; + + /** Whether the surface was valid when the exception occurred. */ + public final boolean isSurfaceValid; + + public VideoDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo, @Nullable Surface surface) { + super(cause, codecInfo); + surfaceIdentityHashCode = System.identityHashCode(surface); + isSurfaceValid = surface == null || surface.isValid(); + } + } + + private static boolean evaluatedDeviceNeedsSetOutputSurfaceWorkaround; + private static boolean deviceNeedsSetOutputSurfaceWorkaround; + + private final Context context; + private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper; + private final EventDispatcher eventDispatcher; + private final long allowedJoiningTimeMs; + private final int maxDroppedFramesToNotify; + private final boolean deviceNeedsNoPostProcessWorkaround; + private final long[] pendingOutputStreamOffsetsUs; + private final long[] pendingOutputStreamSwitchTimesUs; + + private CodecMaxValues codecMaxValues; + private boolean codecNeedsSetOutputSurfaceWorkaround; + private boolean codecHandlesHdr10PlusOutOfBandMetadata; + + private Surface surface; + private Surface dummySurface; + @C.VideoScalingMode + private int scalingMode; + private boolean renderedFirstFrame; + private long initialPositionUs; + private long joiningDeadlineMs; + private long droppedFrameAccumulationStartTimeMs; + private int droppedFrames; + private int consecutiveDroppedFrameCount; + private int buffersInCodecCount; + private long lastRenderTimeUs; + + private int pendingRotationDegrees; + private float pendingPixelWidthHeightRatio; + @Nullable private MediaFormat currentMediaFormat; + private int currentWidth; + private int currentHeight; + private int currentUnappliedRotationDegrees; + private float currentPixelWidthHeightRatio; + private int reportedWidth; + private int reportedHeight; + private int reportedUnappliedRotationDegrees; + private float reportedPixelWidthHeightRatio; + + private boolean tunneling; + private int tunnelingAudioSessionId; + /* package */ @Nullable OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener; + + private long lastInputTimeUs; + private long outputStreamOffsetUs; + private int pendingOutputStreamOffsetCount; + @Nullable private VideoFrameMetadataListener frameMetadataListener; + + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + */ + public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector) { + this(context, mediaCodecSelector, 0); + } + + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + */ + public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, + long allowedJoiningTimeMs) { + this( + context, + mediaCodecSelector, + allowedJoiningTimeMs, + /* eventHandler= */ null, + /* eventListener= */ null, + /* maxDroppedFramesToNotify= */ -1); + } + + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @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)}. + */ + @SuppressWarnings("deprecation") + public MediaCodecVideoRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + long allowedJoiningTimeMs, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify) { + this( + context, + mediaCodecSelector, + allowedJoiningTimeMs, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + eventHandler, + eventListener, + maxDroppedFramesToNotify); + } + + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content 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. + * @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)}. + * @deprecated Use {@link #MediaCodecVideoRenderer(Context, MediaCodecSelector, long, boolean, + * Handler, VideoRendererEventListener, int)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. + */ + @Deprecated + @SuppressWarnings("deprecation") + public MediaCodecVideoRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + long allowedJoiningTimeMs, + @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, + boolean playClearSamplesWithoutKeys, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify) { + this( + context, + mediaCodecSelector, + allowedJoiningTimeMs, + drmSessionManager, + playClearSamplesWithoutKeys, + /* enableDecoderFallback= */ false, + eventHandler, + eventListener, + maxDroppedFramesToNotify); + } + + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. + * @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)}. + */ + @SuppressWarnings("deprecation") + public MediaCodecVideoRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + long allowedJoiningTimeMs, + boolean enableDecoderFallback, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify) { + this( + context, + mediaCodecSelector, + allowedJoiningTimeMs, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + enableDecoderFallback, + eventHandler, + eventListener, + maxDroppedFramesToNotify); + } + + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content 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. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. + * @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)}. + * @deprecated Use {@link #MediaCodecVideoRenderer(Context, MediaCodecSelector, long, boolean, + * Handler, VideoRendererEventListener, int)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. + */ + @Deprecated + public MediaCodecVideoRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + long allowedJoiningTimeMs, + @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, + boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify) { + super( + C.TRACK_TYPE_VIDEO, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + enableDecoderFallback, + /* assumedMinimumCodecOperatingRate= */ 30); + this.allowedJoiningTimeMs = allowedJoiningTimeMs; + this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; + this.context = context.getApplicationContext(); + frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(this.context); + eventDispatcher = new EventDispatcher(eventHandler, eventListener); + deviceNeedsNoPostProcessWorkaround = deviceNeedsNoPostProcessWorkaround(); + pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; + pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; + outputStreamOffsetUs = C.TIME_UNSET; + lastInputTimeUs = C.TIME_UNSET; + joiningDeadlineMs = C.TIME_UNSET; + currentWidth = Format.NO_VALUE; + currentHeight = Format.NO_VALUE; + currentPixelWidthHeightRatio = Format.NO_VALUE; + pendingPixelWidthHeightRatio = Format.NO_VALUE; + scalingMode = C.VIDEO_SCALING_MODE_DEFAULT; + clearReportedVideoSize(); + } + + @Override + @Capabilities + protected int supportsFormat( + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, + Format format) + throws DecoderQueryException { + String mimeType = format.sampleMimeType; + if (!MimeTypes.isVideo(mimeType)) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); + } + @Nullable DrmInitData drmInitData = format.drmInitData; + // Assume encrypted content requires secure decoders. + boolean requiresSecureDecryption = drmInitData != null; + List<MediaCodecInfo> decoderInfos = + getDecoderInfos( + mediaCodecSelector, + format, + requiresSecureDecryption, + /* requiresTunnelingDecoder= */ false); + if (requiresSecureDecryption && decoderInfos.isEmpty()) { + // No secure decoders are available. Fall back to non-secure decoders. + decoderInfos = + getDecoderInfos( + mediaCodecSelector, + format, + /* requiresSecureDecoder= */ false, + /* requiresTunnelingDecoder= */ false); + } + if (decoderInfos.isEmpty()) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); + } + boolean supportsFormatDrm = + drmInitData == null + || FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType) + || (format.exoMediaCryptoType == null + && supportsFormatDrm(drmSessionManager, drmInitData)); + if (!supportsFormatDrm) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); + } + // Check capabilities for the first decoder in the list, which takes priority. + MediaCodecInfo decoderInfo = decoderInfos.get(0); + boolean isFormatSupported = decoderInfo.isFormatSupported(format); + @AdaptiveSupport + int adaptiveSupport = + decoderInfo.isSeamlessAdaptationSupported(format) + ? ADAPTIVE_SEAMLESS + : ADAPTIVE_NOT_SEAMLESS; + @TunnelingSupport int tunnelingSupport = TUNNELING_NOT_SUPPORTED; + if (isFormatSupported) { + List<MediaCodecInfo> tunnelingDecoderInfos = + getDecoderInfos( + mediaCodecSelector, + format, + requiresSecureDecryption, + /* requiresTunnelingDecoder= */ true); + if (!tunnelingDecoderInfos.isEmpty()) { + MediaCodecInfo tunnelingDecoderInfo = tunnelingDecoderInfos.get(0); + if (tunnelingDecoderInfo.isFormatSupported(format) + && tunnelingDecoderInfo.isSeamlessAdaptationSupported(format)) { + tunnelingSupport = TUNNELING_SUPPORTED; + } + } + } + @FormatSupport + int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; + return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport); + } + + @Override + protected List<MediaCodecInfo> getDecoderInfos( + MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) + throws DecoderQueryException { + return getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder, tunneling); + } + + private static List<MediaCodecInfo> getDecoderInfos( + MediaCodecSelector mediaCodecSelector, + Format format, + boolean requiresSecureDecoder, + boolean requiresTunnelingDecoder) + throws DecoderQueryException { + @Nullable String mimeType = format.sampleMimeType; + if (mimeType == null) { + return Collections.emptyList(); + } + List<MediaCodecInfo> decoderInfos = + mediaCodecSelector.getDecoderInfos( + mimeType, requiresSecureDecoder, requiresTunnelingDecoder); + decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format); + if (MimeTypes.VIDEO_DOLBY_VISION.equals(mimeType)) { + // Fall back to H.264/AVC or H.265/HEVC for the relevant DV profiles. + @Nullable + Pair<Integer, Integer> codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); + if (codecProfileAndLevel != null) { + int profile = codecProfileAndLevel.first; + if (profile == CodecProfileLevel.DolbyVisionProfileDvheDtr + || profile == CodecProfileLevel.DolbyVisionProfileDvheSt) { + decoderInfos.addAll( + mediaCodecSelector.getDecoderInfos( + MimeTypes.VIDEO_H265, requiresSecureDecoder, requiresTunnelingDecoder)); + } else if (profile == CodecProfileLevel.DolbyVisionProfileDvavSe) { + decoderInfos.addAll( + mediaCodecSelector.getDecoderInfos( + MimeTypes.VIDEO_H264, requiresSecureDecoder, requiresTunnelingDecoder)); + } + } + } + return Collections.unmodifiableList(decoderInfos); + } + + @Override + protected void onEnabled(boolean joining) throws ExoPlaybackException { + super.onEnabled(joining); + int oldTunnelingAudioSessionId = tunnelingAudioSessionId; + tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; + tunneling = tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET; + if (tunnelingAudioSessionId != oldTunnelingAudioSessionId) { + releaseCodec(); + } + eventDispatcher.enabled(decoderCounters); + frameReleaseTimeHelper.enable(); + } + + @Override + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + if (outputStreamOffsetUs == C.TIME_UNSET) { + outputStreamOffsetUs = offsetUs; + } else { + if (pendingOutputStreamOffsetCount == pendingOutputStreamOffsetsUs.length) { + Log.w(TAG, "Too many stream changes, so dropping offset: " + + pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]); + } else { + pendingOutputStreamOffsetCount++; + } + pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1] = offsetUs; + pendingOutputStreamSwitchTimesUs[pendingOutputStreamOffsetCount - 1] = lastInputTimeUs; + } + super.onStreamChanged(formats, offsetUs); + } + + @Override + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + super.onPositionReset(positionUs, joining); + clearRenderedFirstFrame(); + initialPositionUs = C.TIME_UNSET; + consecutiveDroppedFrameCount = 0; + lastInputTimeUs = C.TIME_UNSET; + if (pendingOutputStreamOffsetCount != 0) { + outputStreamOffsetUs = pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]; + pendingOutputStreamOffsetCount = 0; + } + if (joining) { + setJoiningDeadlineMs(); + } else { + joiningDeadlineMs = C.TIME_UNSET; + } + } + + @Override + public boolean isReady() { + if (super.isReady() && (renderedFirstFrame || (dummySurface != null && surface == dummySurface) + || getCodec() == null || tunneling)) { + // 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; + } + } + + @Override + protected void onStarted() { + super.onStarted(); + droppedFrames = 0; + droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); + lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; + } + + @Override + protected void onStopped() { + joiningDeadlineMs = C.TIME_UNSET; + maybeNotifyDroppedFrames(); + super.onStopped(); + } + + @Override + protected void onDisabled() { + lastInputTimeUs = C.TIME_UNSET; + outputStreamOffsetUs = C.TIME_UNSET; + pendingOutputStreamOffsetCount = 0; + currentMediaFormat = null; + clearReportedVideoSize(); + clearRenderedFirstFrame(); + frameReleaseTimeHelper.disable(); + tunnelingOnFrameRenderedListener = null; + try { + super.onDisabled(); + } finally { + eventDispatcher.disabled(decoderCounters); + } + } + + @Override + protected void onReset() { + try { + super.onReset(); + } finally { + if (dummySurface != null) { + if (surface == dummySurface) { + surface = null; + } + dummySurface.release(); + dummySurface = null; + } + } + } + + @Override + public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { + if (messageType == C.MSG_SET_SURFACE) { + setSurface((Surface) message); + } else if (messageType == C.MSG_SET_SCALING_MODE) { + scalingMode = (Integer) message; + MediaCodec codec = getCodec(); + if (codec != null) { + codec.setVideoScalingMode(scalingMode); + } + } else if (messageType == C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) { + frameMetadataListener = (VideoFrameMetadataListener) message; + } else { + super.handleMessage(messageType, message); + } + } + + private void setSurface(Surface surface) throws ExoPlaybackException { + if (surface == null) { + // Use a dummy surface if possible. + if (dummySurface != null) { + surface = dummySurface; + } else { + MediaCodecInfo codecInfo = getCodecInfo(); + if (codecInfo != null && shouldUseDummySurface(codecInfo)) { + dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure); + surface = dummySurface; + } + } + } + // We only need to update the codec if the surface has changed. + if (this.surface != surface) { + this.surface = surface; + @State int state = getState(); + MediaCodec codec = getCodec(); + if (codec != null) { + if (Util.SDK_INT >= 23 && surface != null && !codecNeedsSetOutputSurfaceWorkaround) { + setOutputSurfaceV23(codec, surface); + } else { + releaseCodec(); + maybeInitCodec(); + } + } + if (surface != null && surface != dummySurface) { + // If we know the video size, report it again immediately. + maybeRenotifyVideoSizeChanged(); + // We haven't rendered to the new surface yet. + clearRenderedFirstFrame(); + if (state == STATE_STARTED) { + setJoiningDeadlineMs(); + } + } else { + // The surface has been removed. + clearReportedVideoSize(); + clearRenderedFirstFrame(); + } + } else if (surface != null && surface != dummySurface) { + // The surface is set and unchanged. If we know the video size and/or have already rendered to + // the surface, report these again immediately. + maybeRenotifyVideoSizeChanged(); + maybeRenotifyRenderedFirstFrame(); + } + } + + @Override + protected boolean shouldInitCodec(MediaCodecInfo codecInfo) { + return surface != null || shouldUseDummySurface(codecInfo); + } + + @Override + protected boolean getCodecNeedsEosPropagation() { + // Since API 23, onFrameRenderedListener allows for detection of the renderer EOS. + return tunneling && Util.SDK_INT < 23; + } + + @Override + protected void configureCodec( + MediaCodecInfo codecInfo, + MediaCodec codec, + Format format, + @Nullable MediaCrypto crypto, + float codecOperatingRate) { + String codecMimeType = codecInfo.codecMimeType; + codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); + MediaFormat mediaFormat = + getMediaFormat( + format, + codecMimeType, + codecMaxValues, + codecOperatingRate, + deviceNeedsNoPostProcessWorkaround, + tunnelingAudioSessionId); + if (surface == null) { + Assertions.checkState(shouldUseDummySurface(codecInfo)); + if (dummySurface == null) { + dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure); + } + surface = dummySurface; + } + codec.configure(mediaFormat, surface, crypto, 0); + if (Util.SDK_INT >= 23 && tunneling) { + tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec); + } + } + + @Override + protected @KeepCodecResult int canKeepCodec( + MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + if (codecInfo.isSeamlessAdaptationSupported( + oldFormat, newFormat, /* isNewFormatComplete= */ true) + && newFormat.width <= codecMaxValues.width + && newFormat.height <= codecMaxValues.height + && getMaxInputSize(codecInfo, newFormat) <= codecMaxValues.inputSize) { + return oldFormat.initializationDataEquals(newFormat) + ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION + : KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION; + } + return KEEP_CODEC_RESULT_NO; + } + + @CallSuper + @Override + protected void releaseCodec() { + try { + super.releaseCodec(); + } finally { + buffersInCodecCount = 0; + } + } + + @CallSuper + @Override + protected boolean flushOrReleaseCodec() { + try { + return super.flushOrReleaseCodec(); + } finally { + buffersInCodecCount = 0; + } + } + + @Override + protected float getCodecOperatingRateV23( + float operatingRate, Format format, Format[] streamFormats) { + // Use the highest known stream frame-rate up front, to avoid having to reconfigure the codec + // should an adaptive switch to that stream occur. + float maxFrameRate = -1; + for (Format streamFormat : streamFormats) { + float streamFrameRate = streamFormat.frameRate; + if (streamFrameRate != Format.NO_VALUE) { + maxFrameRate = Math.max(maxFrameRate, streamFrameRate); + } + } + return maxFrameRate == -1 ? CODEC_OPERATING_RATE_UNSET : (maxFrameRate * operatingRate); + } + + @Override + protected void onCodecInitialized(String name, long initializedTimestampMs, + long initializationDurationMs) { + eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); + codecNeedsSetOutputSurfaceWorkaround = codecNeedsSetOutputSurfaceWorkaround(name); + codecHandlesHdr10PlusOutOfBandMetadata = + Assertions.checkNotNull(getCodecInfo()).isHdr10PlusOutOfBandMetadataSupported(); + } + + @Override + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + super.onInputFormatChanged(formatHolder); + Format newFormat = formatHolder.format; + eventDispatcher.inputFormatChanged(newFormat); + pendingPixelWidthHeightRatio = newFormat.pixelWidthHeightRatio; + pendingRotationDegrees = newFormat.rotationDegrees; + } + + /** + * Called immediately before an input buffer is queued into the codec. + * + * @param buffer The buffer to be queued. + */ + @CallSuper + @Override + protected void onQueueInputBuffer(DecoderInputBuffer buffer) { + // In tunneling mode the device may do frame rate conversion, so in general we can't keep track + // of the number of buffers in the codec. + if (!tunneling) { + buffersInCodecCount++; + } + lastInputTimeUs = Math.max(buffer.timeUs, lastInputTimeUs); + if (Util.SDK_INT < 23 && tunneling) { + // In tunneled mode before API 23 we don't have a way to know when the buffer is output, so + // treat it as if it were output immediately. + onProcessedTunneledBuffer(buffer.timeUs); + } + } + + @Override + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputMediaFormat) { + currentMediaFormat = outputMediaFormat; + boolean hasCrop = + outputMediaFormat.containsKey(KEY_CROP_RIGHT) + && outputMediaFormat.containsKey(KEY_CROP_LEFT) + && outputMediaFormat.containsKey(KEY_CROP_BOTTOM) + && outputMediaFormat.containsKey(KEY_CROP_TOP); + int width = + hasCrop + ? outputMediaFormat.getInteger(KEY_CROP_RIGHT) + - outputMediaFormat.getInteger(KEY_CROP_LEFT) + + 1 + : outputMediaFormat.getInteger(MediaFormat.KEY_WIDTH); + int height = + hasCrop + ? outputMediaFormat.getInteger(KEY_CROP_BOTTOM) + - outputMediaFormat.getInteger(KEY_CROP_TOP) + + 1 + : outputMediaFormat.getInteger(MediaFormat.KEY_HEIGHT); + processOutputFormat(codec, width, height); + } + + @Override + protected void handleInputBufferSupplementalData(DecoderInputBuffer buffer) + throws ExoPlaybackException { + if (!codecHandlesHdr10PlusOutOfBandMetadata) { + return; + } + ByteBuffer data = Assertions.checkNotNull(buffer.supplementalData); + if (data.remaining() >= 7) { + // Check for HDR10+ out-of-band metadata. See User_data_registered_itu_t_t35 in ST 2094-40. + byte ituTT35CountryCode = data.get(); + int ituTT35TerminalProviderCode = data.getShort(); + int ituTT35TerminalProviderOrientedCode = data.getShort(); + byte applicationIdentifier = data.get(); + byte applicationVersion = data.get(); + data.position(0); + if (ituTT35CountryCode == (byte) 0xB5 + && ituTT35TerminalProviderCode == 0x003C + && ituTT35TerminalProviderOrientedCode == 0x0001 + && applicationIdentifier == 4 + && applicationVersion == 0) { + // The metadata size may vary so allocate a new array every time. This is not too + // inefficient because the metadata is only a few tens of bytes. + byte[] hdr10PlusInfo = new byte[data.remaining()]; + data.get(hdr10PlusInfo); + data.position(0); + // If codecHandlesHdr10PlusOutOfBandMetadata is true, this is an API 29 or later build. + setHdr10PlusInfoV29(getCodec(), hdr10PlusInfo); + } + } + } + + @Override + protected boolean processOutputBuffer( + long positionUs, + long elapsedRealtimeUs, + MediaCodec codec, + ByteBuffer buffer, + int bufferIndex, + int bufferFlags, + long bufferPresentationTimeUs, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, + Format format) + throws ExoPlaybackException { + if (initialPositionUs == C.TIME_UNSET) { + initialPositionUs = positionUs; + } + + long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs; + + if (isDecodeOnlyBuffer && !isLastBuffer) { + skipOutputBuffer(codec, bufferIndex, presentationTimeUs); + return true; + } + + long earlyUs = bufferPresentationTimeUs - positionUs; + if (surface == dummySurface) { + // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. + if (isBufferLate(earlyUs)) { + skipOutputBuffer(codec, bufferIndex, presentationTimeUs); + return true; + } + return false; + } + + long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000; + long elapsedSinceLastRenderUs = elapsedRealtimeNowUs - lastRenderTimeUs; + boolean isStarted = getState() == STATE_STARTED; + // Don't force output until we joined and the position reached the current stream. + boolean forceRenderOutputBuffer = + joiningDeadlineMs == C.TIME_UNSET + && positionUs >= outputStreamOffsetUs + && (!renderedFirstFrame + || (isStarted && shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastRenderUs))); + if (forceRenderOutputBuffer) { + long releaseTimeNs = System.nanoTime(); + notifyFrameMetadataListener(presentationTimeUs, releaseTimeNs, format, currentMediaFormat); + if (Util.SDK_INT >= 21) { + renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, releaseTimeNs); + } else { + renderOutputBuffer(codec, bufferIndex, presentationTimeUs); + } + return true; + } + + if (!isStarted || positionUs == initialPositionUs) { + return false; + } + + // Fine-grained adjustment of earlyUs based on the elapsed time since the start of the current + // iteration of the rendering loop. + long elapsedSinceStartOfLoopUs = elapsedRealtimeNowUs - elapsedRealtimeUs; + earlyUs -= elapsedSinceStartOfLoopUs; + + // Compute the buffer's desired release time in nanoseconds. + long systemTimeNs = System.nanoTime(); + long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000); + + // Apply a timestamp adjustment, if there is one. + long adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime( + bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs); + earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; + + boolean treatDroppedBuffersAsSkipped = joiningDeadlineMs != C.TIME_UNSET; + if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastBuffer) + && maybeDropBuffersToKeyframe( + codec, bufferIndex, presentationTimeUs, positionUs, treatDroppedBuffersAsSkipped)) { + return false; + } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs, isLastBuffer)) { + if (treatDroppedBuffersAsSkipped) { + skipOutputBuffer(codec, bufferIndex, presentationTimeUs); + } else { + dropOutputBuffer(codec, bufferIndex, presentationTimeUs); + } + return true; + } + + if (Util.SDK_INT >= 21) { + // Let the underlying framework time the release. + if (earlyUs < 50000) { + notifyFrameMetadataListener( + presentationTimeUs, adjustedReleaseTimeNs, format, currentMediaFormat); + renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, adjustedReleaseTimeNs); + return true; + } + } else { + // We need to time the release ourselves. + if (earlyUs < 30000) { + if (earlyUs > 11000) { + // We're a little too early to render the frame. Sleep until the frame can be rendered. + // Note: The 11ms threshold was chosen fairly arbitrarily. + try { + // Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms. + Thread.sleep((earlyUs - 10000) / 1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + notifyFrameMetadataListener( + presentationTimeUs, adjustedReleaseTimeNs, format, currentMediaFormat); + renderOutputBuffer(codec, bufferIndex, presentationTimeUs); + return true; + } + } + + // We're either not playing, or it's not time to render the frame yet. + return false; + } + + private void processOutputFormat(MediaCodec codec, int width, int height) { + currentWidth = width; + currentHeight = height; + currentPixelWidthHeightRatio = pendingPixelWidthHeightRatio; + if (Util.SDK_INT >= 21) { + // On API level 21 and above the decoder applies the rotation when rendering to the surface. + // Hence currentUnappliedRotation should always be 0. For 90 and 270 degree rotations, we need + // to flip the width, height and pixel aspect ratio to reflect the rotation that was applied. + if (pendingRotationDegrees == 90 || pendingRotationDegrees == 270) { + int rotatedHeight = currentWidth; + currentWidth = currentHeight; + currentHeight = rotatedHeight; + currentPixelWidthHeightRatio = 1 / currentPixelWidthHeightRatio; + } + } else { + // On API level 20 and below the decoder does not apply the rotation. + currentUnappliedRotationDegrees = pendingRotationDegrees; + } + // Must be applied each time the output MediaFormat changes. + codec.setVideoScalingMode(scalingMode); + } + + private void notifyFrameMetadataListener( + long presentationTimeUs, long releaseTimeNs, Format format, MediaFormat mediaFormat) { + if (frameMetadataListener != null) { + frameMetadataListener.onVideoFrameAboutToBeRendered( + presentationTimeUs, releaseTimeNs, format, mediaFormat); + } + } + + /** + * Returns the offset that should be subtracted from {@code bufferPresentationTimeUs} in {@link + * #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, long, boolean, boolean, + * Format)} to get the playback position with respect to the media. + */ + protected long getOutputStreamOffsetUs() { + return outputStreamOffsetUs; + } + + /** Called when a buffer was processed in tunneling mode. */ + protected void onProcessedTunneledBuffer(long presentationTimeUs) { + @Nullable Format format = updateOutputFormatForTime(presentationTimeUs); + if (format != null) { + processOutputFormat(getCodec(), format.width, format.height); + } + maybeNotifyVideoSizeChanged(); + decoderCounters.renderedOutputBufferCount++; + maybeNotifyRenderedFirstFrame(); + onProcessedOutputBuffer(presentationTimeUs); + } + + /** Called when a output EOS was received in tunneling mode. */ + private void onProcessedTunneledEndOfStream() { + setPendingOutputEndOfStream(); + } + + /** + * Called when an output buffer is successfully processed. + * + * @param presentationTimeUs The timestamp associated with the output buffer. + */ + @CallSuper + @Override + protected void onProcessedOutputBuffer(long presentationTimeUs) { + if (!tunneling) { + buffersInCodecCount--; + } + while (pendingOutputStreamOffsetCount != 0 + && presentationTimeUs >= pendingOutputStreamSwitchTimesUs[0]) { + outputStreamOffsetUs = pendingOutputStreamOffsetsUs[0]; + pendingOutputStreamOffsetCount--; + System.arraycopy( + pendingOutputStreamOffsetsUs, + /* srcPos= */ 1, + pendingOutputStreamOffsetsUs, + /* destPos= */ 0, + pendingOutputStreamOffsetCount); + System.arraycopy( + pendingOutputStreamSwitchTimesUs, + /* srcPos= */ 1, + pendingOutputStreamSwitchTimesUs, + /* destPos= */ 0, + pendingOutputStreamOffsetCount); + clearRenderedFirstFrame(); + } + } + + /** + * 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. + * @param isLastBuffer Whether the buffer is the last buffer in the current stream. + */ + protected boolean shouldDropOutputBuffer( + long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { + return isBufferLate(earlyUs) && !isLastBuffer; + } + + /** + * 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. + * @param isLastBuffer Whether the buffer is the last buffer in the current stream. + */ + protected boolean shouldDropBuffersToKeyframe( + long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { + return isBufferVeryLate(earlyUs) && !isLastBuffer; + } + + /** + * 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) { + // Force render late buffers every 100ms to avoid frozen video effect. + return isBufferLate(earlyUs) && elapsedSinceLastRenderUs > 100000; + } + + /** + * Skips the output buffer with the specified index. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to skip. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + */ + protected void skipOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) { + TraceUtil.beginSection("skipVideoBuffer"); + codec.releaseOutputBuffer(index, false); + TraceUtil.endSection(); + decoderCounters.skippedOutputBufferCount++; + } + + /** + * Drops the output buffer with the specified index. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to drop. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + */ + protected void dropOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) { + TraceUtil.beginSection("dropVideoBuffer"); + codec.releaseOutputBuffer(index, false); + TraceUtil.endSection(); + updateDroppedBufferCounters(1); + } + + /** + * 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 codec The codec that owns the output buffer. + * @param index The index of the output buffer to drop. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + * @param positionUs The current playback position, in microseconds. + * @param treatDroppedBuffersAsSkipped Whether dropped buffers should be treated as intentionally + * skipped. + * @return Whether any buffers were dropped. + * @throws ExoPlaybackException If an error occurs flushing the codec. + */ + protected boolean maybeDropBuffersToKeyframe( + MediaCodec codec, + int index, + long presentationTimeUs, + long positionUs, + boolean treatDroppedBuffersAsSkipped) + 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 codec, + // which releases all pending buffers buffers including the current output buffer. + int totalDroppedBufferCount = buffersInCodecCount + droppedSourceBufferCount; + if (treatDroppedBuffersAsSkipped) { + decoderCounters.skippedOutputBufferCount += totalDroppedBufferCount; + } else { + updateDroppedBufferCounters(totalDroppedBufferCount); + } + flushOrReinitializeCodec(); + 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(); + } + } + + /** + * Renders the output buffer with the specified index. This method is only called if the platform + * API version of the device is less than 21. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to drop. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + */ + protected void renderOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) { + maybeNotifyVideoSizeChanged(); + TraceUtil.beginSection("releaseOutputBuffer"); + codec.releaseOutputBuffer(index, true); + TraceUtil.endSection(); + lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; + decoderCounters.renderedOutputBufferCount++; + consecutiveDroppedFrameCount = 0; + maybeNotifyRenderedFirstFrame(); + } + + /** + * Renders the output buffer with the specified index. This method is only called if the platform + * API version of the device is 21 or later. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to drop. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + * @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds. + */ + @TargetApi(21) + protected void renderOutputBufferV21( + MediaCodec codec, int index, long presentationTimeUs, long releaseTimeNs) { + maybeNotifyVideoSizeChanged(); + TraceUtil.beginSection("releaseOutputBuffer"); + codec.releaseOutputBuffer(index, releaseTimeNs); + TraceUtil.endSection(); + lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; + decoderCounters.renderedOutputBufferCount++; + consecutiveDroppedFrameCount = 0; + maybeNotifyRenderedFirstFrame(); + } + + private boolean shouldUseDummySurface(MediaCodecInfo codecInfo) { + return Util.SDK_INT >= 23 + && !tunneling + && !codecNeedsSetOutputSurfaceWorkaround(codecInfo.name) + && (!codecInfo.secure || DummySurface.isSecureSupported(context)); + } + + private void setJoiningDeadlineMs() { + joiningDeadlineMs = allowedJoiningTimeMs > 0 + ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; + } + + private void clearRenderedFirstFrame() { + renderedFirstFrame = false; + // The first frame notification is triggered by renderOutputBuffer or renderOutputBufferV21 for + // non-tunneled playback, onQueueInputBuffer for tunneled playback prior to API level 23, and + // OnFrameRenderedListenerV23.onFrameRenderedListener for tunneled playback on API level 23 and + // above. + if (Util.SDK_INT >= 23 && tunneling) { + MediaCodec codec = getCodec(); + // If codec is null then the listener will be instantiated in configureCodec. + if (codec != null) { + tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec); + } + } + } + + /* package */ 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; + reportedPixelWidthHeightRatio = Format.NO_VALUE; + reportedUnappliedRotationDegrees = Format.NO_VALUE; + } + + private void maybeNotifyVideoSizeChanged() { + if ((currentWidth != Format.NO_VALUE || currentHeight != Format.NO_VALUE) + && (reportedWidth != currentWidth || reportedHeight != currentHeight + || reportedUnappliedRotationDegrees != currentUnappliedRotationDegrees + || reportedPixelWidthHeightRatio != currentPixelWidthHeightRatio)) { + eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees, + currentPixelWidthHeightRatio); + reportedWidth = currentWidth; + reportedHeight = currentHeight; + reportedUnappliedRotationDegrees = currentUnappliedRotationDegrees; + reportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; + } + } + + private void maybeRenotifyVideoSizeChanged() { + if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) { + eventDispatcher.videoSizeChanged(reportedWidth, reportedHeight, + reportedUnappliedRotationDegrees, reportedPixelWidthHeightRatio); + } + } + + 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; + } + + @TargetApi(29) + private static void setHdr10PlusInfoV29(MediaCodec codec, byte[] hdr10PlusInfo) { + Bundle codecParameters = new Bundle(); + codecParameters.putByteArray(MediaCodec.PARAMETER_KEY_HDR10_PLUS_INFO, hdr10PlusInfo); + codec.setParameters(codecParameters); + } + + @TargetApi(23) + private static void setOutputSurfaceV23(MediaCodec codec, Surface surface) { + codec.setOutputSurface(surface); + } + + @TargetApi(21) + private static void configureTunnelingV21(MediaFormat mediaFormat, int tunnelingAudioSessionId) { + mediaFormat.setFeatureEnabled(CodecCapabilities.FEATURE_TunneledPlayback, true); + mediaFormat.setInteger(MediaFormat.KEY_AUDIO_SESSION_ID, tunnelingAudioSessionId); + } + + /** + * Returns the framework {@link MediaFormat} that should be used to configure the decoder. + * + * @param format The {@link Format} of media. + * @param codecMimeType The MIME type handled by the codec. + * @param codecMaxValues Codec max values that should be used when configuring the decoder. + * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if + * no codec operating rate should be set. + * @param deviceNeedsNoPostProcessWorkaround Whether the device is known to do post processing by + * default that isn't compatible with ExoPlayer. + * @param tunnelingAudioSessionId The audio session id to use for tunneling, or {@link + * C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. + * @return The framework {@link MediaFormat} that should be used to configure the decoder. + */ + @SuppressLint("InlinedApi") + protected MediaFormat getMediaFormat( + Format format, + String codecMimeType, + CodecMaxValues codecMaxValues, + float codecOperatingRate, + boolean deviceNeedsNoPostProcessWorkaround, + int tunnelingAudioSessionId) { + MediaFormat mediaFormat = new MediaFormat(); + // Set format parameters that should always be set. + mediaFormat.setString(MediaFormat.KEY_MIME, codecMimeType); + mediaFormat.setInteger(MediaFormat.KEY_WIDTH, format.width); + mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, format.height); + MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); + // Set format parameters that may be unset. + MediaFormatUtil.maybeSetFloat(mediaFormat, MediaFormat.KEY_FRAME_RATE, format.frameRate); + MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees); + MediaFormatUtil.maybeSetColorInfo(mediaFormat, format.colorInfo); + if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { + // Some phones require the profile to be set on the codec. + // See https://github.com/google/ExoPlayer/pull/5438. + Pair<Integer, Integer> codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); + if (codecProfileAndLevel != null) { + MediaFormatUtil.maybeSetInteger( + mediaFormat, MediaFormat.KEY_PROFILE, codecProfileAndLevel.first); + } + } + // Set codec max values. + mediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); + mediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height); + MediaFormatUtil.maybeSetInteger( + mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize); + // Set codec configuration values. + if (Util.SDK_INT >= 23) { + mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */); + if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET) { + mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate); + } + } + if (deviceNeedsNoPostProcessWorkaround) { + mediaFormat.setInteger("no-post-process", 1); + mediaFormat.setInteger("auto-frc", 0); + } + if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { + configureTunnelingV21(mediaFormat, tunnelingAudioSessionId); + } + return mediaFormat; + } + + /** + * Returns {@link CodecMaxValues} suitable for configuring a codec for {@code format} in a way + * that will allow possible adaptation to other compatible formats in {@code streamFormats}. + * + * @param codecInfo Information about the {@link MediaCodec} being configured. + * @param format The {@link Format} for which the codec is being configured. + * @param streamFormats The possible stream formats. + * @return Suitable {@link CodecMaxValues}. + */ + protected CodecMaxValues getCodecMaxValues( + MediaCodecInfo codecInfo, Format format, Format[] streamFormats) { + int maxWidth = format.width; + int maxHeight = format.height; + int maxInputSize = getMaxInputSize(codecInfo, format); + if (streamFormats.length == 1) { + // The single entry in streamFormats must correspond to the format for which the codec is + // being configured. + if (maxInputSize != Format.NO_VALUE) { + int codecMaxInputSize = + getCodecMaxInputSize(codecInfo, format.sampleMimeType, format.width, format.height); + if (codecMaxInputSize != Format.NO_VALUE) { + // Scale up the initial video decoder maximum input size so playlist item transitions with + // small increases in maximum sample size don't require reinitialization. This only makes + // a difference if the exact maximum sample sizes are known from the container. + int scaledMaxInputSize = + (int) (maxInputSize * INITIAL_FORMAT_MAX_INPUT_SIZE_SCALE_FACTOR); + // Avoid exceeding the maximum expected for the codec. + maxInputSize = Math.min(scaledMaxInputSize, codecMaxInputSize); + } + } + return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); + } + boolean haveUnknownDimensions = false; + for (Format streamFormat : streamFormats) { + if (codecInfo.isSeamlessAdaptationSupported( + format, streamFormat, /* isNewFormatComplete= */ false)) { + haveUnknownDimensions |= + (streamFormat.width == Format.NO_VALUE || streamFormat.height == Format.NO_VALUE); + maxWidth = Math.max(maxWidth, streamFormat.width); + maxHeight = Math.max(maxHeight, streamFormat.height); + maxInputSize = Math.max(maxInputSize, getMaxInputSize(codecInfo, streamFormat)); + } + } + if (haveUnknownDimensions) { + Log.w(TAG, "Resolutions unknown. Codec max resolution: " + maxWidth + "x" + maxHeight); + Point codecMaxSize = getCodecMaxSize(codecInfo, format); + if (codecMaxSize != null) { + maxWidth = Math.max(maxWidth, codecMaxSize.x); + maxHeight = Math.max(maxHeight, codecMaxSize.y); + maxInputSize = + Math.max( + maxInputSize, + getCodecMaxInputSize(codecInfo, format.sampleMimeType, maxWidth, maxHeight)); + Log.w(TAG, "Codec max resolution adjusted to: " + maxWidth + "x" + maxHeight); + } + } + return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); + } + + @Override + protected DecoderException createDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo) { + return new VideoDecoderException(cause, codecInfo, surface); + } + + /** + * Returns a maximum video size to use when configuring a codec for {@code format} in a way that + * will allow possible adaptation to other compatible formats that are expected to have the same + * aspect ratio, but whose sizes are unknown. + * + * @param codecInfo Information about the {@link MediaCodec} being configured. + * @param format The {@link Format} for which the codec is being configured. + * @return The maximum video size to use, or null if the size of {@code format} should be used. + */ + private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) { + boolean isVerticalVideo = format.height > format.width; + int formatLongEdgePx = isVerticalVideo ? format.height : format.width; + int formatShortEdgePx = isVerticalVideo ? format.width : format.height; + float aspectRatio = (float) formatShortEdgePx / formatLongEdgePx; + for (int longEdgePx : STANDARD_LONG_EDGE_VIDEO_PX) { + int shortEdgePx = (int) (longEdgePx * aspectRatio); + if (longEdgePx <= formatLongEdgePx || shortEdgePx <= formatShortEdgePx) { + // Don't return a size not larger than the format for which the codec is being configured. + return null; + } else if (Util.SDK_INT >= 21) { + Point alignedSize = codecInfo.alignVideoSizeV21(isVerticalVideo ? shortEdgePx : longEdgePx, + isVerticalVideo ? longEdgePx : shortEdgePx); + float frameRate = format.frameRate; + if (codecInfo.isVideoSizeAndRateSupportedV21(alignedSize.x, alignedSize.y, frameRate)) { + return alignedSize; + } + } else { + try { + // Conservatively assume the codec requires 16px width and height alignment. + longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16; + shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16; + if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) { + return new Point( + isVerticalVideo ? shortEdgePx : longEdgePx, + isVerticalVideo ? longEdgePx : shortEdgePx); + } + } catch (DecoderQueryException e) { + // We tried our best. Give up! + return null; + } + } + } + return null; + } + + /** + * Returns a maximum input buffer size for a given {@link MediaCodec} and {@link Format}. + * + * @param codecInfo Information about the {@link MediaCodec} being configured. + * @param format The format. + * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not + * be determined. + */ + private static int getMaxInputSize(MediaCodecInfo codecInfo, Format format) { + if (format.maxInputSize != Format.NO_VALUE) { + // The format defines an explicit maximum input size. Add the total size of initialization + // data buffers, as they may need to be queued in the same input buffer as the largest sample. + int totalInitializationDataSize = 0; + int initializationDataCount = format.initializationData.size(); + for (int i = 0; i < initializationDataCount; i++) { + totalInitializationDataSize += format.initializationData.get(i).length; + } + return format.maxInputSize + totalInitializationDataSize; + } else { + // Calculated maximum input sizes are overestimates, so it's not necessary to add the size of + // initialization data. + return getCodecMaxInputSize(codecInfo, format.sampleMimeType, format.width, format.height); + } + } + + /** + * Returns a maximum input size for a given codec, MIME type, width and height. + * + * @param codecInfo Information about the {@link MediaCodec} being configured. + * @param sampleMimeType The format mime type. + * @param width The width in pixels. + * @param height The height in pixels. + * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be + * determined. + */ + private static int getCodecMaxInputSize( + MediaCodecInfo codecInfo, String sampleMimeType, int width, int height) { + if (width == Format.NO_VALUE || height == Format.NO_VALUE) { + // We can't infer a maximum input size without video dimensions. + return Format.NO_VALUE; + } + + // Attempt to infer a maximum input size from the format. + int maxPixels; + int minCompressionRatio; + switch (sampleMimeType) { + case MimeTypes.VIDEO_H263: + case MimeTypes.VIDEO_MP4V: + maxPixels = width * height; + minCompressionRatio = 2; + break; + case MimeTypes.VIDEO_H264: + if ("BRAVIA 4K 2015".equals(Util.MODEL) // Sony Bravia 4K + || ("Amazon".equals(Util.MANUFACTURER) + && ("KFSOWI".equals(Util.MODEL) // Kindle Soho + || ("AFTS".equals(Util.MODEL) && codecInfo.secure)))) { // Fire TV Gen 2 + // Use the default value for cases where platform limitations may prevent buffers of the + // calculated maximum input size from being allocated. + return Format.NO_VALUE; + } + // Round up width/height to an integer number of macroblocks. + maxPixels = Util.ceilDivide(width, 16) * Util.ceilDivide(height, 16) * 16 * 16; + minCompressionRatio = 2; + break; + case MimeTypes.VIDEO_VP8: + // VPX does not specify a ratio so use the values from the platform's SoftVPX.cpp. + maxPixels = width * height; + minCompressionRatio = 2; + break; + case MimeTypes.VIDEO_H265: + case MimeTypes.VIDEO_VP9: + maxPixels = width * height; + minCompressionRatio = 4; + break; + default: + // Leave the default max input size. + return Format.NO_VALUE; + } + // Estimate the maximum input size assuming three channel 4:2:0 subsampled input frames. + return (maxPixels * 3) / (2 * minCompressionRatio); + } + + /** + * Returns whether the device is known to do post processing by default that isn't compatible with + * ExoPlayer. + * + * @return Whether the device is known to do post processing by default that isn't compatible with + * ExoPlayer. + */ + private static boolean deviceNeedsNoPostProcessWorkaround() { + // Nvidia devices prior to M try to adjust the playback rate to better map the frame-rate of + // content to the refresh rate of the display. For example playback of 23.976fps content is + // adjusted to play at 1.001x speed when the output display is 60Hz. Unfortunately the + // implementation causes ExoPlayer's reported playback position to drift out of sync. Captions + // also lose sync [Internal: b/26453592]. Even after M, the devices may apply post processing + // operations that can modify frame output timestamps, which is incompatible with ExoPlayer's + // logic for skipping decode-only frames. + return "NVIDIA".equals(Util.MANUFACTURER); + } + + /* + * TODO: + * + * 1. Validate that Android device certification now ensures correct behavior, and add a + * corresponding SDK_INT upper bound for applying the workaround (probably SDK_INT < 26). + * 2. Determine a complete list of affected devices. + * 3. Some of the devices in this list only fail to support setOutputSurface when switching from + * a SurfaceView provided Surface to a Surface of another type (e.g. TextureView/DummySurface), + * and vice versa. One hypothesis is that setOutputSurface fails when the surfaces have + * different pixel formats. If we can find a way to query the Surface instances to determine + * whether this case applies, then we'll be able to provide a more targeted workaround. + */ + /** + * Returns whether the codec is known to implement {@link MediaCodec#setOutputSurface(Surface)} + * incorrectly. + * + * <p>If true is returned then we fall back to releasing and re-instantiating the codec instead. + * + * @param name The name of the codec. + * @return True if the device is known to implement {@link MediaCodec#setOutputSurface(Surface)} + * incorrectly. + */ + protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) { + if (name.startsWith("OMX.google")) { + // Google OMX decoders are not known to have this issue on any API level. + return false; + } + synchronized (MediaCodecVideoRenderer.class) { + if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) { + if ("dangal".equals(Util.DEVICE)) { + // Workaround for MiTV devices: + // https://github.com/google/ExoPlayer/issues/5169, + // https://github.com/google/ExoPlayer/issues/6899. + deviceNeedsSetOutputSurfaceWorkaround = true; + } else if (Util.SDK_INT <= 27 && "HWEML".equals(Util.DEVICE)) { + // Workaround for Huawei P20: + // https://github.com/google/ExoPlayer/issues/4468#issuecomment-459291645. + deviceNeedsSetOutputSurfaceWorkaround = true; + } else if (Util.SDK_INT >= 27) { + // In general, devices running API level 27 or later should be unaffected. Do nothing. + } else { + // Enable the workaround on a per-device basis. Works around: + // https://github.com/google/ExoPlayer/issues/3236, + // https://github.com/google/ExoPlayer/issues/3355, + // https://github.com/google/ExoPlayer/issues/3439, + // https://github.com/google/ExoPlayer/issues/3724, + // https://github.com/google/ExoPlayer/issues/3835, + // https://github.com/google/ExoPlayer/issues/4006, + // https://github.com/google/ExoPlayer/issues/4084, + // https://github.com/google/ExoPlayer/issues/4104, + // https://github.com/google/ExoPlayer/issues/4134, + // https://github.com/google/ExoPlayer/issues/4315, + // https://github.com/google/ExoPlayer/issues/4419, + // https://github.com/google/ExoPlayer/issues/4460, + // https://github.com/google/ExoPlayer/issues/4468, + // https://github.com/google/ExoPlayer/issues/5312, + // https://github.com/google/ExoPlayer/issues/6503. + switch (Util.DEVICE) { + case "1601": + case "1713": + case "1714": + case "A10-70F": + case "A10-70L": + case "A1601": + case "A2016a40": + case "A7000-a": + case "A7000plus": + case "A7010a48": + case "A7020a48": + case "AquaPowerM": + case "ASUS_X00AD_2": + case "Aura_Note_2": + case "BLACK-1X": + case "BRAVIA_ATV2": + case "BRAVIA_ATV3_4K": + case "C1": + case "ComioS1": + case "CP8676_I02": + case "CPH1609": + case "CPY83_I00": + case "cv1": + case "cv3": + case "deb": + case "E5643": + case "ELUGA_A3_Pro": + case "ELUGA_Note": + case "ELUGA_Prim": + case "ELUGA_Ray_X": + case "EverStar_S": + case "F3111": + case "F3113": + case "F3116": + case "F3211": + case "F3213": + case "F3215": + case "F3311": + case "flo": + case "fugu": + case "GiONEE_CBL7513": + case "GiONEE_GBL7319": + case "GIONEE_GBL7360": + case "GIONEE_SWW1609": + case "GIONEE_SWW1627": + case "GIONEE_SWW1631": + case "GIONEE_WBL5708": + case "GIONEE_WBL7365": + case "GIONEE_WBL7519": + case "griffin": + case "htc_e56ml_dtul": + case "hwALE-H": + case "HWBLN-H": + case "HWCAM-H": + case "HWVNS-H": + case "HWWAS-H": + case "i9031": + case "iball8735_9806": + case "Infinix-X572": + case "iris60": + case "itel_S41": + case "j2xlteins": + case "JGZ": + case "K50a40": + case "kate": + case "l5460": + case "le_x6": + case "LS-5017": + case "M5c": + case "manning": + case "marino_f": + case "MEIZU_M5": + case "mh": + case "mido": + case "MX6": + case "namath": + case "nicklaus_f": + case "NX541J": + case "NX573J": + case "OnePlus5T": + case "p212": + case "P681": + case "P85": + case "panell_d": + case "panell_dl": + case "panell_ds": + case "panell_dt": + case "PB2-670M": + case "PGN528": + case "PGN610": + case "PGN611": + case "Phantom6": + case "Pixi4-7_3G": + case "Pixi5-10_4G": + case "PLE": + case "PRO7S": + case "Q350": + case "Q4260": + case "Q427": + case "Q4310": + case "Q5": + case "QM16XE_U": + case "QX1": + case "santoni": + case "Slate_Pro": + case "SVP-DTV15": + case "s905x018": + case "taido_row": + case "TB3-730F": + case "TB3-730X": + case "TB3-850F": + case "TB3-850M": + case "tcl_eu": + case "V1": + case "V23GB": + case "V5": + case "vernee_M5": + case "watson": + case "whyred": + case "woods_f": + case "woods_fn": + case "X3_HK": + case "XE2X": + case "XT1663": + case "Z12_PRO": + case "Z80": + deviceNeedsSetOutputSurfaceWorkaround = true; + break; + default: + // Do nothing. + break; + } + switch (Util.MODEL) { + case "AFTA": + case "AFTN": + case "JSN-L21": + deviceNeedsSetOutputSurfaceWorkaround = true; + break; + default: + // Do nothing. + break; + } + } + evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true; + } + } + return deviceNeedsSetOutputSurfaceWorkaround; + } + + protected Surface getSurface() { + return surface; + } + + protected static final class CodecMaxValues { + + public final int width; + public final int height; + public final int inputSize; + + public CodecMaxValues(int width, int height, int inputSize) { + this.width = width; + this.height = height; + this.inputSize = inputSize; + } + + } + + @TargetApi(23) + private final class OnFrameRenderedListenerV23 + implements MediaCodec.OnFrameRenderedListener, Handler.Callback { + + private static final int HANDLE_FRAME_RENDERED = 0; + + private final Handler handler; + + public OnFrameRenderedListenerV23(MediaCodec codec) { + handler = new Handler(this); + codec.setOnFrameRenderedListener(/* listener= */ this, handler); + } + + @Override + public void onFrameRendered(MediaCodec codec, long presentationTimeUs, long nanoTime) { + // Workaround bug in MediaCodec that causes deadlock if you call directly back into the + // MediaCodec from this listener method. + // Deadlock occurs because MediaCodec calls this listener method holding a lock, + // which may also be required by calls made back into the MediaCodec. + // This was fixed in https://android-review.googlesource.com/1156807. + // + // The workaround queues the event for subsequent processing, where the lock will not be held. + if (Util.SDK_INT < 30) { + Message message = + Message.obtain( + handler, + /* what= */ HANDLE_FRAME_RENDERED, + /* arg1= */ (int) (presentationTimeUs >> 32), + /* arg2= */ (int) presentationTimeUs); + handler.sendMessageAtFrontOfQueue(message); + } else { + handleFrameRendered(presentationTimeUs); + } + } + + @Override + public boolean handleMessage(Message message) { + switch (message.what) { + case HANDLE_FRAME_RENDERED: + handleFrameRendered(Util.toLong(message.arg1, message.arg2)); + return true; + default: + return false; + } + } + + private void handleFrameRendered(long presentationTimeUs) { + if (this != tunnelingOnFrameRenderedListener) { + // Stale event. + return; + } + if (presentationTimeUs == TUNNELING_EOS_PRESENTATION_TIME_US) { + onProcessedTunneledEndOfStream(); + } else { + onProcessedTunneledBuffer(presentationTimeUs); + } + } + } +} |