diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec')
6 files changed, 4062 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java new file mode 100644 index 0000000000..7e38c9a173 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -0,0 +1,617 @@ +/* + * 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.mediacodec; + +import android.annotation.TargetApi; +import android.graphics.Point; +import android.media.MediaCodec; +import android.media.MediaCodecInfo.AudioCapabilities; +import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaCodecInfo.CodecProfileLevel; +import android.media.MediaCodecInfo.VideoCapabilities; +import android.util.Pair; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +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.Util; + +/** Information about a {@link MediaCodec} for a given mime type. */ +@SuppressWarnings("InlinedApi") +public final class MediaCodecInfo { + + public static final String TAG = "MediaCodecInfo"; + + /** + * The value returned by {@link #getMaxSupportedInstances()} if the upper bound on the maximum + * number of supported instances is unknown. + */ + public static final int MAX_SUPPORTED_INSTANCES_UNKNOWN = -1; + + /** + * The name of the decoder. + * <p> + * May be passed to {@link MediaCodec#createByCodecName(String)} to create an instance of the + * decoder. + */ + public final String name; + + /** The MIME type handled by the codec, or {@code null} if this is a passthrough codec. */ + @Nullable public final String mimeType; + + /** + * The MIME type that the codec uses for media of type {@link #mimeType}, or {@code null} if this + * is a passthrough codec. Equal to {@link #mimeType} unless the codec is known to use a + * non-standard MIME type alias. + */ + @Nullable public final String codecMimeType; + + /** + * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if not + * known. + */ + @Nullable public final CodecCapabilities capabilities; + + /** + * Whether the decoder supports seamless resolution switches. + * + * @see CodecCapabilities#isFeatureSupported(String) + * @see CodecCapabilities#FEATURE_AdaptivePlayback + */ + public final boolean adaptive; + + /** + * Whether the decoder supports tunneling. + * + * @see CodecCapabilities#isFeatureSupported(String) + * @see CodecCapabilities#FEATURE_TunneledPlayback + */ + public final boolean tunneling; + + /** + * Whether the decoder is secure. + * + * @see CodecCapabilities#isFeatureSupported(String) + * @see CodecCapabilities#FEATURE_SecurePlayback + */ + public final boolean secure; + + /** Whether this instance describes a passthrough codec. */ + public final boolean passthrough; + + /** + * Whether the codec is hardware accelerated. + * + * <p>This could be an approximation as the exact information is only provided in API levels 29+. + * + * @see android.media.MediaCodecInfo#isHardwareAccelerated() + */ + public final boolean hardwareAccelerated; + + /** + * Whether the codec is software only. + * + * <p>This could be an approximation as the exact information is only provided in API levels 29+. + * + * @see android.media.MediaCodecInfo#isSoftwareOnly() + */ + public final boolean softwareOnly; + + /** + * Whether the codec is from the vendor. + * + * <p>This could be an approximation as the exact information is only provided in API levels 29+. + * + * @see android.media.MediaCodecInfo#isVendor() + */ + public final boolean vendor; + + private final boolean isVideo; + + /** + * Creates an instance representing an audio passthrough decoder. + * + * @param name The name of the {@link MediaCodec}. + * @return The created instance. + */ + public static MediaCodecInfo newPassthroughInstance(String name) { + return new MediaCodecInfo( + name, + /* mimeType= */ null, + /* codecMimeType= */ null, + /* capabilities= */ null, + /* passthrough= */ true, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false); + } + + /** + * Creates an instance. + * + * @param name The name of the {@link MediaCodec}. + * @param mimeType A mime type supported by the {@link MediaCodec}. + * @param codecMimeType The MIME type that the codec uses for media of type {@code #mimeType}. + * Equal to {@code mimeType} unless the codec is known to use a non-standard MIME type alias. + * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type, or + * {@code null} if not known. + * @param hardwareAccelerated Whether the {@link MediaCodec} is hardware accelerated. + * @param softwareOnly Whether the {@link MediaCodec} is software only. + * @param vendor Whether the {@link MediaCodec} is provided by the vendor. + * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}. + * @param forceSecure Whether {@link #secure} should be forced to {@code true}. + * @return The created instance. + */ + public static MediaCodecInfo newInstance( + String name, + String mimeType, + String codecMimeType, + @Nullable CodecCapabilities capabilities, + boolean hardwareAccelerated, + boolean softwareOnly, + boolean vendor, + boolean forceDisableAdaptive, + boolean forceSecure) { + return new MediaCodecInfo( + name, + mimeType, + codecMimeType, + capabilities, + /* passthrough= */ false, + hardwareAccelerated, + softwareOnly, + vendor, + forceDisableAdaptive, + forceSecure); + } + + private MediaCodecInfo( + String name, + @Nullable String mimeType, + @Nullable String codecMimeType, + @Nullable CodecCapabilities capabilities, + boolean passthrough, + boolean hardwareAccelerated, + boolean softwareOnly, + boolean vendor, + boolean forceDisableAdaptive, + boolean forceSecure) { + this.name = Assertions.checkNotNull(name); + this.mimeType = mimeType; + this.codecMimeType = codecMimeType; + this.capabilities = capabilities; + this.passthrough = passthrough; + this.hardwareAccelerated = hardwareAccelerated; + this.softwareOnly = softwareOnly; + this.vendor = vendor; + adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); + tunneling = capabilities != null && isTunneling(capabilities); + secure = forceSecure || (capabilities != null && isSecure(capabilities)); + isVideo = MimeTypes.isVideo(mimeType); + } + + @Override + public String toString() { + return name; + } + + /** + * The profile levels supported by the decoder. + * + * @return The profile levels supported by the decoder. + */ + public CodecProfileLevel[] getProfileLevels() { + return capabilities == null || capabilities.profileLevels == null ? new CodecProfileLevel[0] + : capabilities.profileLevels; + } + + /** + * Returns an upper bound on the maximum number of supported instances, or {@link + * #MAX_SUPPORTED_INSTANCES_UNKNOWN} if unknown. Applications should not expect to operate more + * instances than the returned maximum. + * + * @see CodecCapabilities#getMaxSupportedInstances() + */ + public int getMaxSupportedInstances() { + return (Util.SDK_INT < 23 || capabilities == null) + ? MAX_SUPPORTED_INSTANCES_UNKNOWN + : getMaxSupportedInstancesV23(capabilities); + } + + /** + * Returns whether the decoder may support decoding the given {@code format}. + * + * @param format The input media format. + * @return Whether the decoder may support decoding the given {@code format}. + * @throws MediaCodecUtil.DecoderQueryException Thrown if an error occurs while querying decoders. + */ + public boolean isFormatSupported(Format format) throws MediaCodecUtil.DecoderQueryException { + if (!isCodecSupported(format)) { + return false; + } + + if (isVideo) { + if (format.width <= 0 || format.height <= 0) { + return true; + } + if (Util.SDK_INT >= 21) { + return isVideoSizeAndRateSupportedV21(format.width, format.height, format.frameRate); + } else { + boolean isFormatSupported = + format.width * format.height <= MediaCodecUtil.maxH264DecodableFrameSize(); + if (!isFormatSupported) { + logNoSupport("legacyFrameSize, " + format.width + "x" + format.height); + } + return isFormatSupported; + } + } else { // Audio + return Util.SDK_INT < 21 + || ((format.sampleRate == Format.NO_VALUE + || isAudioSampleRateSupportedV21(format.sampleRate)) + && (format.channelCount == Format.NO_VALUE + || isAudioChannelCountSupportedV21(format.channelCount))); + } + } + + /** + * Whether the decoder supports the codec of the given {@code format}. If there is insufficient + * information to decide, returns true. + * + * @param format The input media format. + * @return True if the codec of the given {@code format} is supported by the decoder. + */ + public boolean isCodecSupported(Format format) { + if (format.codecs == null || mimeType == null) { + return true; + } + String codecMimeType = MimeTypes.getMediaMimeType(format.codecs); + if (codecMimeType == null) { + return true; + } + if (!mimeType.equals(codecMimeType)) { + logNoSupport("codec.mime " + format.codecs + ", " + codecMimeType); + return false; + } + Pair<Integer, Integer> codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); + if (codecProfileAndLevel == null) { + // If we don't know any better, we assume that the profile and level are supported. + return true; + } + int profile = codecProfileAndLevel.first; + int level = codecProfileAndLevel.second; + if (!isVideo && profile != CodecProfileLevel.AACObjectXHE) { + // Some devices/builds underreport audio capabilities, so assume support except for xHE-AAC + // which may not be widely supported. See https://github.com/google/ExoPlayer/issues/5145. + return true; + } + for (CodecProfileLevel capabilities : getProfileLevels()) { + if (capabilities.profile == profile && capabilities.level >= level) { + return true; + } + } + logNoSupport("codec.profileLevel, " + format.codecs + ", " + codecMimeType); + return false; + } + + /** Whether the codec handles HDR10+ out-of-band metadata. */ + public boolean isHdr10PlusOutOfBandMetadataSupported() { + if (Util.SDK_INT >= 29 && MimeTypes.VIDEO_VP9.equals(mimeType)) { + for (CodecProfileLevel capabilities : getProfileLevels()) { + if (capabilities.profile == CodecProfileLevel.VP9Profile2HDR10Plus) { + return true; + } + } + } + return false; + } + + /** + * Returns whether it may be possible to adapt to playing a different format when the codec is + * configured to play media in the specified {@code format}. For adaptation to succeed, the codec + * must also be configured with appropriate maximum values and {@link + * #isSeamlessAdaptationSupported(Format, Format, boolean)} must return {@code true} for the + * old/new formats. + * + * @param format The format of media for which the decoder will be configured. + * @return Whether adaptation may be possible + */ + public boolean isSeamlessAdaptationSupported(Format format) { + if (isVideo) { + return adaptive; + } else { + Pair<Integer, Integer> codecProfileLevel = MediaCodecUtil.getCodecProfileAndLevel(format); + return codecProfileLevel != null && codecProfileLevel.first == CodecProfileLevel.AACObjectXHE; + } + } + + /** + * Returns whether it is possible to adapt the decoder seamlessly from {@code oldFormat} to {@code + * newFormat}. If {@code newFormat} may not be completely populated, pass {@code false} for {@code + * isNewFormatComplete}. + * + * @param oldFormat The format being decoded. + * @param newFormat The new format. + * @param isNewFormatComplete Whether {@code newFormat} is populated with format-specific + * metadata. + * @return Whether it is possible to adapt the decoder seamlessly. + */ + public boolean isSeamlessAdaptationSupported( + Format oldFormat, Format newFormat, boolean isNewFormatComplete) { + if (isVideo) { + return oldFormat.sampleMimeType.equals(newFormat.sampleMimeType) + && oldFormat.rotationDegrees == newFormat.rotationDegrees + && (adaptive + || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height)) + && ((!isNewFormatComplete && newFormat.colorInfo == null) + || Util.areEqual(oldFormat.colorInfo, newFormat.colorInfo)); + } else { + if (!MimeTypes.AUDIO_AAC.equals(mimeType) + || !oldFormat.sampleMimeType.equals(newFormat.sampleMimeType) + || oldFormat.channelCount != newFormat.channelCount + || oldFormat.sampleRate != newFormat.sampleRate) { + return false; + } + // Check the codec profile levels support adaptation. + Pair<Integer, Integer> oldCodecProfileLevel = + MediaCodecUtil.getCodecProfileAndLevel(oldFormat); + Pair<Integer, Integer> newCodecProfileLevel = + MediaCodecUtil.getCodecProfileAndLevel(newFormat); + if (oldCodecProfileLevel == null || newCodecProfileLevel == null) { + return false; + } + int oldProfile = oldCodecProfileLevel.first; + int newProfile = newCodecProfileLevel.first; + return oldProfile == CodecProfileLevel.AACObjectXHE + && newProfile == CodecProfileLevel.AACObjectXHE; + } + } + + /** + * Whether the decoder supports video with a given width, height and frame rate. + * + * <p>Must not be called if the device SDK version is less than 21. + * + * @param width Width in pixels. + * @param height Height in pixels. + * @param frameRate Optional frame rate in frames per second. Ignored if set to {@link + * Format#NO_VALUE} or any value less than or equal to 0. + * @return Whether the decoder supports video with the given width, height and frame rate. + */ + @TargetApi(21) + public boolean isVideoSizeAndRateSupportedV21(int width, int height, double frameRate) { + if (capabilities == null) { + logNoSupport("sizeAndRate.caps"); + return false; + } + VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); + if (videoCapabilities == null) { + logNoSupport("sizeAndRate.vCaps"); + return false; + } + if (!areSizeAndRateSupportedV21(videoCapabilities, width, height, frameRate)) { + if (width >= height + || !enableRotatedVerticalResolutionWorkaround(name) + || !areSizeAndRateSupportedV21(videoCapabilities, height, width, frameRate)) { + logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate); + return false; + } + logAssumedSupport("sizeAndRate.rotated, " + width + "x" + height + "x" + frameRate); + } + return true; + } + + /** + * Returns the smallest video size greater than or equal to a specified size that also satisfies + * the {@link MediaCodec}'s width and height alignment requirements. + * <p> + * Must not be called if the device SDK version is less than 21. + * + * @param width Width in pixels. + * @param height Height in pixels. + * @return The smallest video size greater than or equal to the specified size that also satisfies + * the {@link MediaCodec}'s width and height alignment requirements, or null if not a video + * codec. + */ + @TargetApi(21) + public Point alignVideoSizeV21(int width, int height) { + if (capabilities == null) { + return null; + } + VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); + if (videoCapabilities == null) { + return null; + } + return alignVideoSizeV21(videoCapabilities, width, height); + } + + /** + * Whether the decoder supports audio with a given sample rate. + * <p> + * Must not be called if the device SDK version is less than 21. + * + * @param sampleRate The sample rate in Hz. + * @return Whether the decoder supports audio with the given sample rate. + */ + @TargetApi(21) + public boolean isAudioSampleRateSupportedV21(int sampleRate) { + if (capabilities == null) { + logNoSupport("sampleRate.caps"); + return false; + } + AudioCapabilities audioCapabilities = capabilities.getAudioCapabilities(); + if (audioCapabilities == null) { + logNoSupport("sampleRate.aCaps"); + return false; + } + if (!audioCapabilities.isSampleRateSupported(sampleRate)) { + logNoSupport("sampleRate.support, " + sampleRate); + return false; + } + return true; + } + + /** + * Whether the decoder supports audio with a given channel count. + * <p> + * Must not be called if the device SDK version is less than 21. + * + * @param channelCount The channel count. + * @return Whether the decoder supports audio with the given channel count. + */ + @TargetApi(21) + public boolean isAudioChannelCountSupportedV21(int channelCount) { + if (capabilities == null) { + logNoSupport("channelCount.caps"); + return false; + } + AudioCapabilities audioCapabilities = capabilities.getAudioCapabilities(); + if (audioCapabilities == null) { + logNoSupport("channelCount.aCaps"); + return false; + } + int maxInputChannelCount = adjustMaxInputChannelCount(name, mimeType, + audioCapabilities.getMaxInputChannelCount()); + if (maxInputChannelCount < channelCount) { + logNoSupport("channelCount.support, " + channelCount); + return false; + } + return true; + } + + private void logNoSupport(String message) { + Log.d(TAG, "NoSupport [" + message + "] [" + name + ", " + mimeType + "] [" + + Util.DEVICE_DEBUG_INFO + "]"); + } + + private void logAssumedSupport(String message) { + Log.d(TAG, "AssumedSupport [" + message + "] [" + name + ", " + mimeType + "] [" + + Util.DEVICE_DEBUG_INFO + "]"); + } + + private static int adjustMaxInputChannelCount(String name, String mimeType, int maxChannelCount) { + if (maxChannelCount > 1 || (Util.SDK_INT >= 26 && maxChannelCount > 0)) { + // The maximum channel count looks like it's been set correctly. + return maxChannelCount; + } + if (MimeTypes.AUDIO_MPEG.equals(mimeType) + || MimeTypes.AUDIO_AMR_NB.equals(mimeType) + || MimeTypes.AUDIO_AMR_WB.equals(mimeType) + || MimeTypes.AUDIO_AAC.equals(mimeType) + || MimeTypes.AUDIO_VORBIS.equals(mimeType) + || MimeTypes.AUDIO_OPUS.equals(mimeType) + || MimeTypes.AUDIO_RAW.equals(mimeType) + || MimeTypes.AUDIO_FLAC.equals(mimeType) + || MimeTypes.AUDIO_ALAW.equals(mimeType) + || MimeTypes.AUDIO_MLAW.equals(mimeType) + || MimeTypes.AUDIO_MSGSM.equals(mimeType)) { + // Platform code should have set a default. + return maxChannelCount; + } + // The maximum channel count looks incorrect. Adjust it to an assumed default. + int assumedMaxChannelCount; + if (MimeTypes.AUDIO_AC3.equals(mimeType)) { + assumedMaxChannelCount = 6; + } else if (MimeTypes.AUDIO_E_AC3.equals(mimeType)) { + assumedMaxChannelCount = 16; + } else { + // Default to the platform limit, which is 30. + assumedMaxChannelCount = 30; + } + Log.w(TAG, "AssumedMaxChannelAdjustment: " + name + ", [" + maxChannelCount + " to " + + assumedMaxChannelCount + "]"); + return assumedMaxChannelCount; + } + + private static boolean isAdaptive(CodecCapabilities capabilities) { + return Util.SDK_INT >= 19 && isAdaptiveV19(capabilities); + } + + @TargetApi(19) + private static boolean isAdaptiveV19(CodecCapabilities capabilities) { + return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback); + } + + private static boolean isTunneling(CodecCapabilities capabilities) { + return Util.SDK_INT >= 21 && isTunnelingV21(capabilities); + } + + @TargetApi(21) + private static boolean isTunnelingV21(CodecCapabilities capabilities) { + return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback); + } + + private static boolean isSecure(CodecCapabilities capabilities) { + return Util.SDK_INT >= 21 && isSecureV21(capabilities); + } + + @TargetApi(21) + private static boolean isSecureV21(CodecCapabilities capabilities) { + return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback); + } + + @TargetApi(21) + private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities, int width, + int height, double frameRate) { + // Don't ever fail due to alignment. See: https://github.com/google/ExoPlayer/issues/6551. + Point alignedSize = alignVideoSizeV21(capabilities, width, height); + width = alignedSize.x; + height = alignedSize.y; + + if (frameRate == Format.NO_VALUE || frameRate <= 0) { + return capabilities.isSizeSupported(width, height); + } else { + // The signaled frame rate may be slightly higher than the actual frame rate, so we take the + // floor to avoid situations where a range check in areSizeAndRateSupported fails due to + // slightly exceeding the limits for a standard format (e.g., 1080p at 30 fps). + double floorFrameRate = Math.floor(frameRate); + return capabilities.areSizeAndRateSupported(width, height, floorFrameRate); + } + } + + @TargetApi(21) + private static Point alignVideoSizeV21(VideoCapabilities capabilities, int width, int height) { + int widthAlignment = capabilities.getWidthAlignment(); + int heightAlignment = capabilities.getHeightAlignment(); + return new Point( + Util.ceilDivide(width, widthAlignment) * widthAlignment, + Util.ceilDivide(height, heightAlignment) * heightAlignment); + } + + @TargetApi(23) + private static int getMaxSupportedInstancesV23(CodecCapabilities capabilities) { + return capabilities.getMaxSupportedInstances(); + } + + /** + * Capabilities are known to be inaccurately reported for vertical resolutions on some devices. + * [Internal ref: b/31387661]. When this workaround is enabled, we also check whether the + * capabilities indicate support if the width and height are swapped. If they do, we assume that + * the vertical resolution is also supported. + * + * @param name The name of the codec. + * @return Whether to enable the workaround. + */ + private static final boolean enableRotatedVerticalResolutionWorkaround(String name) { + if ("OMX.MTK.VIDEO.DECODER.HEVC".equals(name) && "mcv5a".equals(Util.DEVICE)) { + // See https://github.com/google/ExoPlayer/issues/6612. + return false; + } + return true; + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java new file mode 100644 index 0000000000..8d2f4574fd --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -0,0 +1,2014 @@ +/* + * 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.mediacodec; + +import android.annotation.TargetApi; +import android.media.MediaCodec; +import android.media.MediaCodec.CodecException; +import android.media.MediaCodec.CryptoException; +import android.media.MediaCrypto; +import android.media.MediaCryptoException; +import android.media.MediaFormat; +import android.os.Bundle; +import android.os.SystemClock; +import androidx.annotation.CheckResult; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.BaseRenderer; +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.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCounters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; +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.MediaCodecUtil.DecoderQueryException; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod; +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.NalUnitUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimedValueQueue; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TraceUtil; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; + +/** + * An abstract renderer that uses {@link MediaCodec} to decode samples for rendering. + */ +public abstract class MediaCodecRenderer extends BaseRenderer { + + /** Thrown when a failure occurs instantiating a decoder. */ + public static class DecoderInitializationException extends Exception { + + private static final int CUSTOM_ERROR_CODE_BASE = -50000; + private static final int NO_SUITABLE_DECODER_ERROR = CUSTOM_ERROR_CODE_BASE + 1; + private static final int DECODER_QUERY_ERROR = CUSTOM_ERROR_CODE_BASE + 2; + + /** + * The mime type for which a decoder was being initialized. + */ + public final String mimeType; + + /** + * Whether it was required that the decoder support a secure output path. + */ + public final boolean secureDecoderRequired; + + /** + * The {@link MediaCodecInfo} of the decoder that failed to initialize. Null if no suitable + * decoder was found. + */ + @Nullable public final MediaCodecInfo codecInfo; + + /** An optional developer-readable diagnostic information string. May be null. */ + @Nullable public final String diagnosticInfo; + + /** + * If the decoder failed to initialize and another decoder being used as a fallback also failed + * to initialize, the {@link DecoderInitializationException} for the fallback decoder. Null if + * there was no fallback decoder or no suitable decoders were found. + */ + @Nullable public final DecoderInitializationException fallbackDecoderInitializationException; + + public DecoderInitializationException(Format format, Throwable cause, + boolean secureDecoderRequired, int errorCode) { + this( + "Decoder init failed: [" + errorCode + "], " + format, + cause, + format.sampleMimeType, + secureDecoderRequired, + /* mediaCodecInfo= */ null, + buildCustomDiagnosticInfo(errorCode), + /* fallbackDecoderInitializationException= */ null); + } + + public DecoderInitializationException( + Format format, + Throwable cause, + boolean secureDecoderRequired, + MediaCodecInfo mediaCodecInfo) { + this( + "Decoder init failed: " + mediaCodecInfo.name + ", " + format, + cause, + format.sampleMimeType, + secureDecoderRequired, + mediaCodecInfo, + Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null, + /* fallbackDecoderInitializationException= */ null); + } + + private DecoderInitializationException( + String message, + Throwable cause, + String mimeType, + boolean secureDecoderRequired, + @Nullable MediaCodecInfo mediaCodecInfo, + @Nullable String diagnosticInfo, + @Nullable DecoderInitializationException fallbackDecoderInitializationException) { + super(message, cause); + this.mimeType = mimeType; + this.secureDecoderRequired = secureDecoderRequired; + this.codecInfo = mediaCodecInfo; + this.diagnosticInfo = diagnosticInfo; + this.fallbackDecoderInitializationException = fallbackDecoderInitializationException; + } + + @CheckResult + private DecoderInitializationException copyWithFallbackException( + DecoderInitializationException fallbackException) { + return new DecoderInitializationException( + getMessage(), + getCause(), + mimeType, + secureDecoderRequired, + codecInfo, + diagnosticInfo, + fallbackException); + } + + @TargetApi(21) + private static String getDiagnosticInfoV21(Throwable cause) { + if (cause instanceof CodecException) { + return ((CodecException) cause).getDiagnosticInfo(); + } + return null; + } + + private static String buildCustomDiagnosticInfo(int errorCode) { + String sign = errorCode < 0 ? "neg_" : ""; + return "com.google.android.exoplayer2.mediacodec.MediaCodecRenderer_" + + sign + + Math.abs(errorCode); + } + } + + /** Thrown when a failure occurs in the decoder. */ + public static class DecoderException extends Exception { + + /** The {@link MediaCodecInfo} of the decoder that failed. Null if unknown. */ + @Nullable public final MediaCodecInfo codecInfo; + + /** An optional developer-readable diagnostic information string. May be null. */ + @Nullable public final String diagnosticInfo; + + public DecoderException(Throwable cause, @Nullable MediaCodecInfo codecInfo) { + super("Decoder failed: " + (codecInfo == null ? null : codecInfo.name), cause); + this.codecInfo = codecInfo; + diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null; + } + + @TargetApi(21) + private static String getDiagnosticInfoV21(Throwable cause) { + if (cause instanceof CodecException) { + return ((CodecException) cause).getDiagnosticInfo(); + } + return null; + } + } + + /** Indicates no codec operating rate should be set. */ + protected static final float CODEC_OPERATING_RATE_UNSET = -1; + + private static final String TAG = "MediaCodecRenderer"; + + /** + * If the {@link MediaCodec} is hotswapped (i.e. replaced during playback), this is the period of + * time during which {@link #isReady()} will report true regardless of whether the new codec has + * output frames that are ready to be rendered. + * <p> + * This allows codec hotswapping to be performed seamlessly, without interrupting the playback of + * other renderers, provided the new codec is able to decode some frames within this time period. + */ + private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000; + + /** + * The possible return values for {@link #canKeepCodec(MediaCodec, MediaCodecInfo, Format, + * Format)}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + KEEP_CODEC_RESULT_NO, + KEEP_CODEC_RESULT_YES_WITH_FLUSH, + KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION, + KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION + }) + protected @interface KeepCodecResult {} + /** The codec cannot be kept. */ + protected static final int KEEP_CODEC_RESULT_NO = 0; + /** The codec can be kept, but must be flushed. */ + protected static final int KEEP_CODEC_RESULT_YES_WITH_FLUSH = 1; + /** + * The codec can be kept. It does not need to be flushed, but must be reconfigured by prefixing + * the next input buffer with the new format's configuration data. + */ + protected static final int KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION = 2; + /** The codec can be kept. It does not need to be flushed and no reconfiguration is required. */ + protected static final int KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION = 3; + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + RECONFIGURATION_STATE_NONE, + RECONFIGURATION_STATE_WRITE_PENDING, + RECONFIGURATION_STATE_QUEUE_PENDING + }) + private @interface ReconfigurationState {} + /** + * There is no pending adaptive reconfiguration work. + */ + private static final int RECONFIGURATION_STATE_NONE = 0; + /** + * Codec configuration data needs to be written into the next buffer. + */ + private static final int RECONFIGURATION_STATE_WRITE_PENDING = 1; + /** + * Codec configuration data has been written into the next buffer, but that buffer still needs to + * be returned to the codec. + */ + private static final int RECONFIGURATION_STATE_QUEUE_PENDING = 2; + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({DRAIN_STATE_NONE, DRAIN_STATE_SIGNAL_END_OF_STREAM, DRAIN_STATE_WAIT_END_OF_STREAM}) + private @interface DrainState {} + /** The codec is not being drained. */ + private static final int DRAIN_STATE_NONE = 0; + /** The codec needs to be drained, but we haven't signaled an end of stream to it yet. */ + private static final int DRAIN_STATE_SIGNAL_END_OF_STREAM = 1; + /** The codec needs to be drained, and we're waiting for it to output an end of stream. */ + private static final int DRAIN_STATE_WAIT_END_OF_STREAM = 2; + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + DRAIN_ACTION_NONE, + DRAIN_ACTION_FLUSH, + DRAIN_ACTION_UPDATE_DRM_SESSION, + DRAIN_ACTION_REINITIALIZE + }) + private @interface DrainAction {} + /** No special action should be taken. */ + private static final int DRAIN_ACTION_NONE = 0; + /** The codec should be flushed. */ + private static final int DRAIN_ACTION_FLUSH = 1; + /** The codec should be flushed and updated to use the pending DRM session. */ + private static final int DRAIN_ACTION_UPDATE_DRM_SESSION = 2; + /** The codec should be reinitialized. */ + private static final int DRAIN_ACTION_REINITIALIZE = 3; + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + ADAPTATION_WORKAROUND_MODE_NEVER, + ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION, + ADAPTATION_WORKAROUND_MODE_ALWAYS + }) + private @interface AdaptationWorkaroundMode {} + /** + * The adaptation workaround is never used. + */ + private static final int ADAPTATION_WORKAROUND_MODE_NEVER = 0; + /** + * The adaptation workaround is used when adapting between formats of the same resolution only. + */ + private static final int ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION = 1; + /** + * The adaptation workaround is always used when adapting between formats. + */ + private static final int ADAPTATION_WORKAROUND_MODE_ALWAYS = 2; + + /** + * H.264/AVC buffer to queue when using the adaptation workaround (see {@link + * #codecAdaptationWorkaroundMode(String)}. Consists of three NAL units with start codes: Baseline + * sequence/picture parameter sets and a 32 * 32 pixel IDR slice. This stream can be queued to + * force a resolution change when adapting to a new format. + */ + private static final byte[] ADAPTATION_WORKAROUND_BUFFER = + new byte[] { + 0, 0, 1, 103, 66, -64, 11, -38, 37, -112, 0, 0, 1, 104, -50, 15, 19, 32, 0, 0, 1, 101, -120, + -124, 13, -50, 113, 24, -96, 0, 47, -65, 28, 49, -61, 39, 93, 120 + }; + + private static final int ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT = 32; + + private final MediaCodecSelector mediaCodecSelector; + @Nullable private final DrmSessionManager<FrameworkMediaCrypto> drmSessionManager; + private final boolean playClearSamplesWithoutKeys; + private final boolean enableDecoderFallback; + private final float assumedMinimumCodecOperatingRate; + private final DecoderInputBuffer buffer; + private final DecoderInputBuffer flagsOnlyBuffer; + private final TimedValueQueue<Format> formatQueue; + private final ArrayList<Long> decodeOnlyPresentationTimestamps; + private final MediaCodec.BufferInfo outputBufferInfo; + + private boolean drmResourcesAcquired; + @Nullable private Format inputFormat; + private Format outputFormat; + @Nullable private DrmSession<FrameworkMediaCrypto> codecDrmSession; + @Nullable private DrmSession<FrameworkMediaCrypto> sourceDrmSession; + @Nullable private MediaCrypto mediaCrypto; + private boolean mediaCryptoRequiresSecureDecoder; + private long renderTimeLimitMs; + private float rendererOperatingRate; + @Nullable private MediaCodec codec; + @Nullable private Format codecFormat; + private float codecOperatingRate; + @Nullable private ArrayDeque<MediaCodecInfo> availableCodecInfos; + @Nullable private DecoderInitializationException preferredDecoderInitializationException; + @Nullable private MediaCodecInfo codecInfo; + @AdaptationWorkaroundMode private int codecAdaptationWorkaroundMode; + private boolean codecNeedsReconfigureWorkaround; + private boolean codecNeedsDiscardToSpsWorkaround; + private boolean codecNeedsFlushWorkaround; + private boolean codecNeedsSosFlushWorkaround; + private boolean codecNeedsEosFlushWorkaround; + private boolean codecNeedsEosOutputExceptionWorkaround; + private boolean codecNeedsMonoChannelCountWorkaround; + private boolean codecNeedsAdaptationWorkaroundBuffer; + private boolean shouldSkipAdaptationWorkaroundOutputBuffer; + private boolean codecNeedsEosPropagation; + private ByteBuffer[] inputBuffers; + private ByteBuffer[] outputBuffers; + private long codecHotswapDeadlineMs; + private int inputIndex; + private int outputIndex; + private ByteBuffer outputBuffer; + private boolean isDecodeOnlyOutputBuffer; + private boolean isLastOutputBuffer; + private boolean codecReconfigured; + @ReconfigurationState private int codecReconfigurationState; + @DrainState private int codecDrainState; + @DrainAction private int codecDrainAction; + private boolean codecReceivedBuffers; + private boolean codecReceivedEos; + private boolean codecHasOutputMediaFormat; + private long largestQueuedPresentationTimeUs; + private long lastBufferInStreamPresentationTimeUs; + private boolean inputStreamEnded; + private boolean outputStreamEnded; + private boolean waitingForKeys; + private boolean waitingForFirstSyncSample; + private boolean waitingForFirstSampleInFormat; + private boolean skipMediaCodecStopOnRelease; + private boolean pendingOutputEndOfStream; + + protected DecoderCounters decoderCounters; + + /** + * @param trackType The track type that the renderer handles. One of the {@code C.TRACK_TYPE_*} + * constants defined in {@link C}. + * @param mediaCodecSelector A decoder selector. + * @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. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is less efficient or slower + * than the primary decoder. + * @param assumedMinimumCodecOperatingRate A codec operating rate that all codecs instantiated by + * this renderer are assumed to meet implicitly (i.e. without the operating rate being set + * explicitly using {@link MediaFormat#KEY_OPERATING_RATE}). + */ + public MediaCodecRenderer( + int trackType, + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, + boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, + float assumedMinimumCodecOperatingRate) { + super(trackType); + this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector); + this.drmSessionManager = drmSessionManager; + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + this.enableDecoderFallback = enableDecoderFallback; + this.assumedMinimumCodecOperatingRate = assumedMinimumCodecOperatingRate; + buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); + formatQueue = new TimedValueQueue<>(); + decodeOnlyPresentationTimestamps = new ArrayList<>(); + outputBufferInfo = new MediaCodec.BufferInfo(); + codecReconfigurationState = RECONFIGURATION_STATE_NONE; + codecDrainState = DRAIN_STATE_NONE; + codecDrainAction = DRAIN_ACTION_NONE; + codecOperatingRate = CODEC_OPERATING_RATE_UNSET; + rendererOperatingRate = 1f; + renderTimeLimitMs = C.TIME_UNSET; + } + + /** + * Set a limit on the time a single {@link #render(long, long)} call can spend draining and + * filling the decoder. + * + * <p>This method is experimental, and will be renamed or removed in a future release. It should + * only be called before the renderer is used. + * + * @param renderTimeLimitMs The render time limit in milliseconds, or {@link C#TIME_UNSET} for no + * limit. + */ + public void experimental_setRenderTimeLimitMs(long renderTimeLimitMs) { + this.renderTimeLimitMs = renderTimeLimitMs; + } + + /** + * Skip calling {@link MediaCodec#stop()} when the underlying MediaCodec is going to be released. + * + * <p>By default, when the MediaCodecRenderer is releasing the underlying {@link MediaCodec}, it + * first calls {@link MediaCodec#stop()} and then calls {@link MediaCodec#release()}. If this + * feature is enabled, the MediaCodecRenderer will skip the call to {@link MediaCodec#stop()}. + * + * <p>This method is experimental, and will be renamed or removed in a future release. It should + * only be called before the renderer is used. + * + * @param enabled enable or disable the feature. + */ + public void experimental_setSkipMediaCodecStopOnRelease(boolean enabled) { + skipMediaCodecStopOnRelease = enabled; + } + + @Override + @AdaptiveSupport + public final int supportsMixedMimeTypeAdaptation() { + return ADAPTIVE_NOT_SEAMLESS; + } + + @Override + @Capabilities + public final int supportsFormat(Format format) throws ExoPlaybackException { + try { + return supportsFormat(mediaCodecSelector, drmSessionManager, format); + } catch (DecoderQueryException e) { + throw createRendererException(e, format); + } + } + + /** + * Returns the {@link Capabilities} for the given {@link Format}. + * + * @param mediaCodecSelector The decoder selector. + * @param drmSessionManager The renderer's {@link DrmSessionManager}. + * @param format The {@link Format}. + * @return The {@link Capabilities} for this {@link Format}. + * @throws DecoderQueryException If there was an error querying decoders. + */ + @Capabilities + protected abstract int supportsFormat( + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, + Format format) + throws DecoderQueryException; + + /** + * Returns a list of decoders that can decode media in the specified format, in priority order. + * + * @param mediaCodecSelector The decoder selector. + * @param format The {@link Format} for which a decoder is required. + * @param requiresSecureDecoder Whether a secure decoder is required. + * @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty. + * @throws DecoderQueryException Thrown if there was an error querying decoders. + */ + protected abstract List<MediaCodecInfo> getDecoderInfos( + MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) + throws DecoderQueryException; + + /** + * Configures a newly created {@link MediaCodec}. + * + * @param codecInfo Information about the {@link MediaCodec} being configured. + * @param codec The {@link MediaCodec} to configure. + * @param format The {@link Format} for which the codec is being configured. + * @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption. + * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if + * no codec operating rate should be set. + */ + protected abstract void configureCodec( + MediaCodecInfo codecInfo, + MediaCodec codec, + Format format, + @Nullable MediaCrypto crypto, + float codecOperatingRate); + + protected final void maybeInitCodec() throws ExoPlaybackException { + if (codec != null || inputFormat == null) { + // We have a codec already, or we don't have a format with which to instantiate one. + return; + } + + setCodecDrmSession(sourceDrmSession); + + String mimeType = inputFormat.sampleMimeType; + if (codecDrmSession != null) { + if (mediaCrypto == null) { + FrameworkMediaCrypto sessionMediaCrypto = codecDrmSession.getMediaCrypto(); + if (sessionMediaCrypto == null) { + DrmSessionException drmError = codecDrmSession.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; + } + } else { + try { + mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId); + } catch (MediaCryptoException e) { + throw createRendererException(e, inputFormat); + } + mediaCryptoRequiresSecureDecoder = + !sessionMediaCrypto.forceAllowInsecureDecoderComponents + && mediaCrypto.requiresSecureDecoderComponent(mimeType); + } + } + if (FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC) { + @DrmSession.State int drmSessionState = codecDrmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw createRendererException(codecDrmSession.getError(), inputFormat); + } else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) { + // Wait for keys. + return; + } + } + } + + try { + maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder); + } catch (DecoderInitializationException e) { + throw createRendererException(e, inputFormat); + } + } + + protected boolean shouldInitCodec(MediaCodecInfo codecInfo) { + return true; + } + + /** + * Returns whether the codec needs the renderer to propagate the end-of-stream signal directly, + * rather than by using an end-of-stream buffer queued to the codec. + */ + protected boolean getCodecNeedsEosPropagation() { + return false; + } + + /** + * Polls the pending output format queue for a given buffer timestamp. If a format is present, it + * is removed and returned. Otherwise returns {@code null}. Subclasses should only call this + * method if they are taking over responsibility for output format propagation (e.g., when using + * video tunneling). + */ + protected final @Nullable Format updateOutputFormatForTime(long presentationTimeUs) { + Format format = formatQueue.pollFloor(presentationTimeUs); + if (format != null) { + outputFormat = format; + } + return format; + } + + protected final MediaCodec getCodec() { + return codec; + } + + protected final @Nullable MediaCodecInfo getCodecInfo() { + return codecInfo; + } + + @Override + protected void onEnabled(boolean joining) throws ExoPlaybackException { + if (drmSessionManager != null && !drmResourcesAcquired) { + drmResourcesAcquired = true; + drmSessionManager.prepare(); + } + decoderCounters = new DecoderCounters(); + } + + @Override + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + inputStreamEnded = false; + outputStreamEnded = false; + pendingOutputEndOfStream = false; + flushOrReinitializeCodec(); + formatQueue.clear(); + } + + @Override + public final void setOperatingRate(float operatingRate) throws ExoPlaybackException { + rendererOperatingRate = operatingRate; + if (codec != null + && codecDrainAction != DRAIN_ACTION_REINITIALIZE + && getState() != STATE_DISABLED) { + updateCodecOperatingRate(); + } + } + + @Override + protected void onDisabled() { + inputFormat = null; + if (sourceDrmSession != null || codecDrmSession != null) { + // TODO: Do something better with this case. + onReset(); + } else { + flushOrReleaseCodec(); + } + } + + @Override + protected void onReset() { + try { + releaseCodec(); + } finally { + setSourceDrmSession(null); + } + if (drmSessionManager != null && drmResourcesAcquired) { + drmResourcesAcquired = false; + drmSessionManager.release(); + } + } + + protected void releaseCodec() { + availableCodecInfos = null; + codecInfo = null; + codecFormat = null; + codecHasOutputMediaFormat = false; + resetInputBuffer(); + resetOutputBuffer(); + resetCodecBuffers(); + waitingForKeys = false; + codecHotswapDeadlineMs = C.TIME_UNSET; + decodeOnlyPresentationTimestamps.clear(); + largestQueuedPresentationTimeUs = C.TIME_UNSET; + lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; + try { + if (codec != null) { + decoderCounters.decoderReleaseCount++; + try { + if (!skipMediaCodecStopOnRelease) { + codec.stop(); + } + } finally { + codec.release(); + } + } + } finally { + codec = null; + try { + if (mediaCrypto != null) { + mediaCrypto.release(); + } + } finally { + mediaCrypto = null; + mediaCryptoRequiresSecureDecoder = false; + setCodecDrmSession(null); + } + } + } + + @Override + protected void onStarted() { + // Do nothing. Overridden to remove throws clause. + } + + @Override + protected void onStopped() { + // Do nothing. Overridden to remove throws clause. + } + + @Override + public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + if (pendingOutputEndOfStream) { + pendingOutputEndOfStream = false; + processEndOfStream(); + } + try { + if (outputStreamEnded) { + renderToEndOfStream(); + return; + } + if (inputFormat == null && !readToFlagsOnlyBuffer(/* requireFormat= */ true)) { + // We still don't have a format and can't make progress without one. + return; + } + // We have a format. + maybeInitCodec(); + if (codec != null) { + long drainStartTimeMs = SystemClock.elapsedRealtime(); + TraceUtil.beginSection("drainAndFeed"); + while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} + while (feedInputBuffer() && shouldContinueFeeding(drainStartTimeMs)) {} + TraceUtil.endSection(); + } else { + decoderCounters.skippedInputBufferCount += skipSource(positionUs); + // We need to read any format changes despite not having a codec so that drmSession can be + // updated, and so that we have the most recent format should the codec be initialized. We + // may also reach the end of the stream. Note that readSource will not read a sample into a + // flags-only buffer. + readToFlagsOnlyBuffer(/* requireFormat= */ false); + } + decoderCounters.ensureUpdated(); + } catch (IllegalStateException e) { + if (isMediaCodecException(e)) { + throw createRendererException(e, inputFormat); + } + throw e; + } + } + + /** + * Flushes the codec. If flushing is not possible, the codec will be released and re-instantiated. + * This method is a no-op if the codec is {@code null}. + * + * <p>The implementation of this method calls {@link #flushOrReleaseCodec()}, and {@link + * #maybeInitCodec()} if the codec needs to be re-instantiated. + * + * @return Whether the codec was released and reinitialized, rather than being flushed. + * @throws ExoPlaybackException If an error occurs re-instantiating the codec. + */ + protected final boolean flushOrReinitializeCodec() throws ExoPlaybackException { + boolean released = flushOrReleaseCodec(); + if (released) { + maybeInitCodec(); + } + return released; + } + + /** + * Flushes the codec. If flushing is not possible, the codec will be released. This method is a + * no-op if the codec is {@code null}. + * + * @return Whether the codec was released. + */ + protected boolean flushOrReleaseCodec() { + if (codec == null) { + return false; + } + if (codecDrainAction == DRAIN_ACTION_REINITIALIZE + || codecNeedsFlushWorkaround + || (codecNeedsSosFlushWorkaround && !codecHasOutputMediaFormat) + || (codecNeedsEosFlushWorkaround && codecReceivedEos)) { + releaseCodec(); + return true; + } + + codec.flush(); + resetInputBuffer(); + resetOutputBuffer(); + codecHotswapDeadlineMs = C.TIME_UNSET; + codecReceivedEos = false; + codecReceivedBuffers = false; + waitingForFirstSyncSample = true; + codecNeedsAdaptationWorkaroundBuffer = false; + shouldSkipAdaptationWorkaroundOutputBuffer = false; + isDecodeOnlyOutputBuffer = false; + isLastOutputBuffer = false; + + waitingForKeys = false; + decodeOnlyPresentationTimestamps.clear(); + largestQueuedPresentationTimeUs = C.TIME_UNSET; + lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; + codecDrainState = DRAIN_STATE_NONE; + codecDrainAction = DRAIN_ACTION_NONE; + // Reconfiguration data sent shortly before the flush may not have been processed by the + // decoder. If the codec has been reconfigured we always send reconfiguration data again to + // guarantee that it's processed. + codecReconfigurationState = + codecReconfigured ? RECONFIGURATION_STATE_WRITE_PENDING : RECONFIGURATION_STATE_NONE; + return false; + } + + protected DecoderException createDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo) { + return new DecoderException(cause, codecInfo); + } + + /** Reads into {@link #flagsOnlyBuffer} and returns whether a {@link Format} was read. */ + private boolean readToFlagsOnlyBuffer(boolean requireFormat) throws ExoPlaybackException { + FormatHolder formatHolder = getFormatHolder(); + flagsOnlyBuffer.clear(); + int result = readSource(formatHolder, flagsOnlyBuffer, requireFormat); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder); + return true; + } else if (result == C.RESULT_BUFFER_READ && flagsOnlyBuffer.isEndOfStream()) { + inputStreamEnded = true; + processEndOfStream(); + } + return false; + } + + private void maybeInitCodecWithFallback( + MediaCrypto crypto, boolean mediaCryptoRequiresSecureDecoder) + throws DecoderInitializationException { + if (availableCodecInfos == null) { + try { + List<MediaCodecInfo> allAvailableCodecInfos = + getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder); + availableCodecInfos = new ArrayDeque<>(); + if (enableDecoderFallback) { + availableCodecInfos.addAll(allAvailableCodecInfos); + } else if (!allAvailableCodecInfos.isEmpty()) { + availableCodecInfos.add(allAvailableCodecInfos.get(0)); + } + preferredDecoderInitializationException = null; + } catch (DecoderQueryException e) { + throw new DecoderInitializationException( + inputFormat, + e, + mediaCryptoRequiresSecureDecoder, + DecoderInitializationException.DECODER_QUERY_ERROR); + } + } + + if (availableCodecInfos.isEmpty()) { + throw new DecoderInitializationException( + inputFormat, + /* cause= */ null, + mediaCryptoRequiresSecureDecoder, + DecoderInitializationException.NO_SUITABLE_DECODER_ERROR); + } + + while (codec == null) { + MediaCodecInfo codecInfo = availableCodecInfos.peekFirst(); + if (!shouldInitCodec(codecInfo)) { + return; + } + try { + initCodec(codecInfo, crypto); + } catch (Exception e) { + Log.w(TAG, "Failed to initialize decoder: " + codecInfo, e); + // This codec failed to initialize, so fall back to the next codec in the list (if any). We + // won't try to use this codec again unless there's a format change or the renderer is + // disabled and re-enabled. + availableCodecInfos.removeFirst(); + DecoderInitializationException exception = + new DecoderInitializationException( + inputFormat, e, mediaCryptoRequiresSecureDecoder, codecInfo); + if (preferredDecoderInitializationException == null) { + preferredDecoderInitializationException = exception; + } else { + preferredDecoderInitializationException = + preferredDecoderInitializationException.copyWithFallbackException(exception); + } + if (availableCodecInfos.isEmpty()) { + throw preferredDecoderInitializationException; + } + } + } + + availableCodecInfos = null; + } + + private List<MediaCodecInfo> getAvailableCodecInfos(boolean mediaCryptoRequiresSecureDecoder) + throws DecoderQueryException { + List<MediaCodecInfo> codecInfos = + getDecoderInfos(mediaCodecSelector, inputFormat, mediaCryptoRequiresSecureDecoder); + if (codecInfos.isEmpty() && mediaCryptoRequiresSecureDecoder) { + // The drm session indicates that a secure decoder is required, but the device does not + // have one. Assuming that supportsFormat indicated support for the media being played, we + // know that it does not require a secure output path. Most CDM implementations allow + // playback to proceed with a non-secure decoder in this case, so we try our luck. + codecInfos = + getDecoderInfos(mediaCodecSelector, inputFormat, /* requiresSecureDecoder= */ false); + if (!codecInfos.isEmpty()) { + Log.w( + TAG, + "Drm session requires secure decoder for " + + inputFormat.sampleMimeType + + ", but no secure decoder available. Trying to proceed with " + + codecInfos + + "."); + } + } + return codecInfos; + } + + private void initCodec(MediaCodecInfo codecInfo, MediaCrypto crypto) throws Exception { + long codecInitializingTimestamp; + long codecInitializedTimestamp; + MediaCodec codec = null; + String codecName = codecInfo.name; + + float codecOperatingRate = + Util.SDK_INT < 23 + ? CODEC_OPERATING_RATE_UNSET + : getCodecOperatingRateV23(rendererOperatingRate, inputFormat, getStreamFormats()); + if (codecOperatingRate <= assumedMinimumCodecOperatingRate) { + codecOperatingRate = CODEC_OPERATING_RATE_UNSET; + } + try { + codecInitializingTimestamp = SystemClock.elapsedRealtime(); + TraceUtil.beginSection("createCodec:" + codecName); + codec = MediaCodec.createByCodecName(codecName); + TraceUtil.endSection(); + TraceUtil.beginSection("configureCodec"); + configureCodec(codecInfo, codec, inputFormat, crypto, codecOperatingRate); + TraceUtil.endSection(); + TraceUtil.beginSection("startCodec"); + codec.start(); + TraceUtil.endSection(); + codecInitializedTimestamp = SystemClock.elapsedRealtime(); + getCodecBuffers(codec); + } catch (Exception e) { + if (codec != null) { + resetCodecBuffers(); + codec.release(); + } + throw e; + } + + this.codec = codec; + this.codecInfo = codecInfo; + this.codecOperatingRate = codecOperatingRate; + codecFormat = inputFormat; + codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName); + codecNeedsReconfigureWorkaround = codecNeedsReconfigureWorkaround(codecName); + codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, codecFormat); + codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); + codecNeedsSosFlushWorkaround = codecNeedsSosFlushWorkaround(codecName); + codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); + codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); + codecNeedsMonoChannelCountWorkaround = + codecNeedsMonoChannelCountWorkaround(codecName, codecFormat); + codecNeedsEosPropagation = + codecNeedsEosPropagationWorkaround(codecInfo) || getCodecNeedsEosPropagation(); + + resetInputBuffer(); + resetOutputBuffer(); + codecHotswapDeadlineMs = + getState() == STATE_STARTED + ? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS) + : C.TIME_UNSET; + codecReconfigured = false; + codecReconfigurationState = RECONFIGURATION_STATE_NONE; + codecReceivedEos = false; + codecReceivedBuffers = false; + largestQueuedPresentationTimeUs = C.TIME_UNSET; + lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; + codecDrainState = DRAIN_STATE_NONE; + codecDrainAction = DRAIN_ACTION_NONE; + codecNeedsAdaptationWorkaroundBuffer = false; + shouldSkipAdaptationWorkaroundOutputBuffer = false; + isDecodeOnlyOutputBuffer = false; + isLastOutputBuffer = false; + waitingForFirstSyncSample = true; + + decoderCounters.decoderInitCount++; + long elapsed = codecInitializedTimestamp - codecInitializingTimestamp; + onCodecInitialized(codecName, codecInitializedTimestamp, elapsed); + } + + private boolean shouldContinueFeeding(long drainStartTimeMs) { + return renderTimeLimitMs == C.TIME_UNSET + || SystemClock.elapsedRealtime() - drainStartTimeMs < renderTimeLimitMs; + } + + private void getCodecBuffers(MediaCodec codec) { + if (Util.SDK_INT < 21) { + inputBuffers = codec.getInputBuffers(); + outputBuffers = codec.getOutputBuffers(); + } + } + + private void resetCodecBuffers() { + if (Util.SDK_INT < 21) { + inputBuffers = null; + outputBuffers = null; + } + } + + private ByteBuffer getInputBuffer(int inputIndex) { + if (Util.SDK_INT >= 21) { + return codec.getInputBuffer(inputIndex); + } else { + return inputBuffers[inputIndex]; + } + } + + private ByteBuffer getOutputBuffer(int outputIndex) { + if (Util.SDK_INT >= 21) { + return codec.getOutputBuffer(outputIndex); + } else { + return outputBuffers[outputIndex]; + } + } + + private boolean hasOutputBuffer() { + return outputIndex >= 0; + } + + private void resetInputBuffer() { + inputIndex = C.INDEX_UNSET; + buffer.data = null; + } + + private void resetOutputBuffer() { + outputIndex = C.INDEX_UNSET; + outputBuffer = null; + } + + private void setSourceDrmSession(@Nullable DrmSession<FrameworkMediaCrypto> session) { + DrmSession.replaceSession(sourceDrmSession, session); + sourceDrmSession = session; + } + + private void setCodecDrmSession(@Nullable DrmSession<FrameworkMediaCrypto> session) { + DrmSession.replaceSession(codecDrmSession, session); + codecDrmSession = session; + } + + /** + * @return Whether it may be possible to feed more input data. + * @throws ExoPlaybackException If an error occurs feeding the input buffer. + */ + private boolean feedInputBuffer() throws ExoPlaybackException { + if (codec == null || codecDrainState == DRAIN_STATE_WAIT_END_OF_STREAM || inputStreamEnded) { + return false; + } + + if (inputIndex < 0) { + inputIndex = codec.dequeueInputBuffer(0); + if (inputIndex < 0) { + return false; + } + buffer.data = getInputBuffer(inputIndex); + buffer.clear(); + } + + if (codecDrainState == DRAIN_STATE_SIGNAL_END_OF_STREAM) { + // We need to re-initialize the codec. Send an end of stream signal to the existing codec so + // that it outputs any remaining buffers before we release it. + if (codecNeedsEosPropagation) { + // Do nothing. + } else { + codecReceivedEos = true; + codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + resetInputBuffer(); + } + codecDrainState = DRAIN_STATE_WAIT_END_OF_STREAM; + return false; + } + + if (codecNeedsAdaptationWorkaroundBuffer) { + codecNeedsAdaptationWorkaroundBuffer = false; + buffer.data.put(ADAPTATION_WORKAROUND_BUFFER); + codec.queueInputBuffer(inputIndex, 0, ADAPTATION_WORKAROUND_BUFFER.length, 0, 0); + resetInputBuffer(); + codecReceivedBuffers = true; + return true; + } + + int result; + FormatHolder formatHolder = getFormatHolder(); + int adaptiveReconfigurationBytes = 0; + if (waitingForKeys) { + // We've already read an encrypted sample into buffer, and are waiting for keys. + result = C.RESULT_BUFFER_READ; + } else { + // For adaptive reconfiguration OMX decoders expect all reconfiguration data to be supplied + // at the start of the buffer that also contains the first frame in the new format. + if (codecReconfigurationState == RECONFIGURATION_STATE_WRITE_PENDING) { + for (int i = 0; i < codecFormat.initializationData.size(); i++) { + byte[] data = codecFormat.initializationData.get(i); + buffer.data.put(data); + } + codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING; + } + adaptiveReconfigurationBytes = buffer.data.position(); + result = readSource(formatHolder, buffer, false); + } + + if (hasReadStreamToEnd()) { + // Notify output queue of the last buffer's timestamp. + lastBufferInStreamPresentationTimeUs = largestQueuedPresentationTimeUs; + } + + if (result == C.RESULT_NOTHING_READ) { + return false; + } + if (result == C.RESULT_FORMAT_READ) { + if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { + // We received two formats in a row. Clear the current buffer of any reconfiguration data + // associated with the first format. + buffer.clear(); + codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; + } + onInputFormatChanged(formatHolder); + return true; + } + + // We've read a buffer. + if (buffer.isEndOfStream()) { + if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { + // We received a new format immediately before the end of the stream. We need to clear + // the corresponding reconfiguration data from the current buffer, but re-write it into + // a subsequent buffer if there are any (e.g. if the user seeks backwards). + buffer.clear(); + codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; + } + inputStreamEnded = true; + if (!codecReceivedBuffers) { + processEndOfStream(); + return false; + } + try { + if (codecNeedsEosPropagation) { + // Do nothing. + } else { + codecReceivedEos = true; + codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + resetInputBuffer(); + } + } catch (CryptoException e) { + throw createRendererException(e, inputFormat); + } + return false; + } + if (waitingForFirstSyncSample && !buffer.isKeyFrame()) { + buffer.clear(); + if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { + // The buffer we just cleared contained reconfiguration data. We need to re-write this + // data into a subsequent buffer (if there is one). + codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; + } + return true; + } + waitingForFirstSyncSample = false; + boolean bufferEncrypted = buffer.isEncrypted(); + waitingForKeys = shouldWaitForKeys(bufferEncrypted); + if (waitingForKeys) { + return false; + } + if (codecNeedsDiscardToSpsWorkaround && !bufferEncrypted) { + NalUnitUtil.discardToSps(buffer.data); + if (buffer.data.position() == 0) { + return true; + } + codecNeedsDiscardToSpsWorkaround = false; + } + try { + long presentationTimeUs = buffer.timeUs; + if (buffer.isDecodeOnly()) { + decodeOnlyPresentationTimestamps.add(presentationTimeUs); + } + if (waitingForFirstSampleInFormat) { + formatQueue.add(presentationTimeUs, inputFormat); + waitingForFirstSampleInFormat = false; + } + largestQueuedPresentationTimeUs = + Math.max(largestQueuedPresentationTimeUs, presentationTimeUs); + + buffer.flip(); + if (buffer.hasSupplementalData()) { + handleInputBufferSupplementalData(buffer); + } + onQueueInputBuffer(buffer); + + if (bufferEncrypted) { + MediaCodec.CryptoInfo cryptoInfo = getFrameworkCryptoInfo(buffer, + adaptiveReconfigurationBytes); + codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0); + } else { + codec.queueInputBuffer(inputIndex, 0, buffer.data.limit(), presentationTimeUs, 0); + } + resetInputBuffer(); + codecReceivedBuffers = true; + codecReconfigurationState = RECONFIGURATION_STATE_NONE; + decoderCounters.inputBufferCount++; + } catch (CryptoException e) { + throw createRendererException(e, inputFormat); + } + return true; + } + + private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { + if (codecDrmSession == null + || (!bufferEncrypted + && (playClearSamplesWithoutKeys || codecDrmSession.playClearSamplesWithoutKeys()))) { + return false; + } + @DrmSession.State int drmSessionState = codecDrmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw createRendererException(codecDrmSession.getError(), inputFormat); + } + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; + } + + /** + * Called when a {@link MediaCodec} has been created and configured. + * <p> + * The default implementation is a no-op. + * + * @param name The name of the codec that was initialized. + * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization + * finished. + * @param initializationDurationMs The time taken to initialize the codec in milliseconds. + */ + protected void onCodecInitialized(String name, long initializedTimestampMs, + long initializationDurationMs) { + // Do nothing. + } + + /** + * Called when a new {@link Format} is read from the upstream {@link MediaPeriod}. + * + * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}. + * @throws ExoPlaybackException If an error occurs re-initializing the {@link MediaCodec}. + */ + @SuppressWarnings("unchecked") + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + waitingForFirstSampleInFormat = true; + Format newFormat = Assertions.checkNotNull(formatHolder.format); + if (formatHolder.includesDrmSession) { + setSourceDrmSession((DrmSession<FrameworkMediaCrypto>) formatHolder.drmSession); + } else { + sourceDrmSession = + getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession); + } + inputFormat = newFormat; + + if (codec == null) { + maybeInitCodec(); + return; + } + + // We have an existing codec that we may need to reconfigure or re-initialize. If the existing + // codec instance is being kept then its operating rate may need to be updated. + + if ((sourceDrmSession == null && codecDrmSession != null) + || (sourceDrmSession != null && codecDrmSession == null) + || (sourceDrmSession != codecDrmSession + && !codecInfo.secure + && maybeRequiresSecureDecoder(sourceDrmSession, newFormat)) + || (Util.SDK_INT < 23 && sourceDrmSession != codecDrmSession)) { + // We might need to switch between the clear and protected output paths, or we're using DRM + // prior to API level 23 where the codec needs to be re-initialized to switch to the new DRM + // session. + drainAndReinitializeCodec(); + return; + } + + switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) { + case KEEP_CODEC_RESULT_NO: + drainAndReinitializeCodec(); + break; + case KEEP_CODEC_RESULT_YES_WITH_FLUSH: + codecFormat = newFormat; + updateCodecOperatingRate(); + if (sourceDrmSession != codecDrmSession) { + drainAndUpdateCodecDrmSession(); + } else { + drainAndFlushCodec(); + } + break; + case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION: + if (codecNeedsReconfigureWorkaround) { + drainAndReinitializeCodec(); + } else { + codecReconfigured = true; + codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; + codecNeedsAdaptationWorkaroundBuffer = + codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS + || (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION + && newFormat.width == codecFormat.width + && newFormat.height == codecFormat.height); + codecFormat = newFormat; + updateCodecOperatingRate(); + if (sourceDrmSession != codecDrmSession) { + drainAndUpdateCodecDrmSession(); + } + } + break; + case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION: + codecFormat = newFormat; + updateCodecOperatingRate(); + if (sourceDrmSession != codecDrmSession) { + drainAndUpdateCodecDrmSession(); + } + break; + default: + throw new IllegalStateException(); // Never happens. + } + } + + /** + * Called when the output {@link MediaFormat} of the {@link MediaCodec} changes. + * + * <p>The default implementation is a no-op. + * + * @param codec The {@link MediaCodec} instance. + * @param outputMediaFormat The new output {@link MediaFormat}. + * @throws ExoPlaybackException Thrown if an error occurs handling the new output media format. + */ + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputMediaFormat) + throws ExoPlaybackException { + // Do nothing. + } + + /** + * Handles supplemental data associated with an input buffer. + * + * <p>The default implementation is a no-op. + * + * @param buffer The input buffer that is about to be queued. + * @throws ExoPlaybackException Thrown if an error occurs handling supplemental data. + */ + protected void handleInputBufferSupplementalData(DecoderInputBuffer buffer) + throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called immediately before an input buffer is queued into the codec. + * + * <p>The default implementation is a no-op. + * + * @param buffer The buffer to be queued. + */ + protected void onQueueInputBuffer(DecoderInputBuffer buffer) { + // Do nothing. + } + + /** + * Called when an output buffer is successfully processed. + * <p> + * The default implementation is a no-op. + * + * @param presentationTimeUs The timestamp associated with the output buffer. + */ + protected void onProcessedOutputBuffer(long presentationTimeUs) { + // Do nothing. + } + + /** + * Determines whether the existing {@link MediaCodec} can be kept for a new {@link Format}, and if + * it can whether it requires reconfiguration. + * + * <p>The default implementation returns {@link #KEEP_CODEC_RESULT_NO}. + * + * @param codec The existing {@link MediaCodec} instance. + * @param codecInfo A {@link MediaCodecInfo} describing the decoder. + * @param oldFormat The {@link Format} for which the existing instance is configured. + * @param newFormat The new {@link Format}. + * @return Whether the instance can be kept, and if it can whether it requires reconfiguration. + */ + protected @KeepCodecResult int canKeepCodec( + MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + return KEEP_CODEC_RESULT_NO; + } + + @Override + public boolean isEnded() { + return outputStreamEnded; + } + + @Override + public boolean isReady() { + return inputFormat != null + && !waitingForKeys + && (isSourceReady() + || hasOutputBuffer() + || (codecHotswapDeadlineMs != C.TIME_UNSET + && SystemClock.elapsedRealtime() < codecHotswapDeadlineMs)); + } + + /** + * Returns the maximum time to block whilst waiting for a decoded output buffer. + * + * @return The maximum time to block, in microseconds. + */ + protected long getDequeueOutputBufferTimeoutUs() { + return 0; + } + + /** + * Returns the {@link MediaFormat#KEY_OPERATING_RATE} value for a given renderer operating rate, + * current {@link Format} and set of possible stream formats. + * + * <p>The default implementation returns {@link #CODEC_OPERATING_RATE_UNSET}. + * + * @param operatingRate The renderer operating rate. + * @param format The {@link Format} for which the codec is being configured. + * @param streamFormats The possible stream formats. + * @return The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if no codec operating + * rate should be set. + */ + protected float getCodecOperatingRateV23( + float operatingRate, Format format, Format[] streamFormats) { + return CODEC_OPERATING_RATE_UNSET; + } + + /** + * Updates the codec operating rate. + * + * @throws ExoPlaybackException If an error occurs releasing or initializing a codec. + */ + private void updateCodecOperatingRate() throws ExoPlaybackException { + if (Util.SDK_INT < 23) { + return; + } + + float newCodecOperatingRate = + getCodecOperatingRateV23(rendererOperatingRate, codecFormat, getStreamFormats()); + if (codecOperatingRate == newCodecOperatingRate) { + // No change. + } else if (newCodecOperatingRate == CODEC_OPERATING_RATE_UNSET) { + // The only way to clear the operating rate is to instantiate a new codec instance. See + // [Internal ref: b/71987865]. + drainAndReinitializeCodec(); + } else if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET + || newCodecOperatingRate > assumedMinimumCodecOperatingRate) { + // We need to set the operating rate, either because we've set it previously or because it's + // above the assumed minimum rate. + Bundle codecParameters = new Bundle(); + codecParameters.putFloat(MediaFormat.KEY_OPERATING_RATE, newCodecOperatingRate); + codec.setParameters(codecParameters); + codecOperatingRate = newCodecOperatingRate; + } + } + + /** Starts draining the codec for flush. */ + private void drainAndFlushCodec() { + if (codecReceivedBuffers) { + codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM; + codecDrainAction = DRAIN_ACTION_FLUSH; + } + } + + /** + * Starts draining the codec to update its DRM session. The update may occur immediately if no + * buffers have been queued to the codec. + * + * @throws ExoPlaybackException If an error occurs updating the codec's DRM session. + */ + private void drainAndUpdateCodecDrmSession() throws ExoPlaybackException { + if (Util.SDK_INT < 23) { + // The codec needs to be re-initialized to switch to the source DRM session. + drainAndReinitializeCodec(); + return; + } + if (codecReceivedBuffers) { + codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM; + codecDrainAction = DRAIN_ACTION_UPDATE_DRM_SESSION; + } else { + // Nothing has been queued to the decoder, so we can do the update immediately. + updateDrmSessionOrReinitializeCodecV23(); + } + } + + /** + * Starts draining the codec for re-initialization. Re-initialization may occur immediately if no + * buffers have been queued to the codec. + * + * @throws ExoPlaybackException If an error occurs re-initializing a codec. + */ + private void drainAndReinitializeCodec() throws ExoPlaybackException { + if (codecReceivedBuffers) { + codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM; + codecDrainAction = DRAIN_ACTION_REINITIALIZE; + } else { + // Nothing has been queued to the decoder, so we can re-initialize immediately. + reinitializeCodec(); + } + } + + /** + * @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 { + if (!hasOutputBuffer()) { + int outputIndex; + if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) { + try { + outputIndex = + codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs()); + } catch (IllegalStateException e) { + processEndOfStream(); + if (outputStreamEnded) { + // Release the codec, as it's in an error state. + releaseCodec(); + } + return false; + } + } else { + outputIndex = + codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs()); + } + + if (outputIndex < 0) { + if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED /* (-2) */) { + processOutputFormat(); + return true; + } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED /* (-3) */) { + processOutputBuffersChanged(); + return true; + } + /* MediaCodec.INFO_TRY_AGAIN_LATER (-1) or unknown negative return value */ + if (codecNeedsEosPropagation + && (inputStreamEnded || codecDrainState == DRAIN_STATE_WAIT_END_OF_STREAM)) { + processEndOfStream(); + } + return false; + } + + // We've dequeued a buffer. + if (shouldSkipAdaptationWorkaroundOutputBuffer) { + shouldSkipAdaptationWorkaroundOutputBuffer = false; + codec.releaseOutputBuffer(outputIndex, false); + return true; + } else if (outputBufferInfo.size == 0 + && (outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + // The dequeued buffer indicates the end of the stream. Process it immediately. + processEndOfStream(); + return false; + } + + this.outputIndex = outputIndex; + outputBuffer = getOutputBuffer(outputIndex); + // The dequeued buffer is a media buffer. Do some initial setup. + // It will be processed by calling processOutputBuffer (possibly multiple times). + if (outputBuffer != null) { + outputBuffer.position(outputBufferInfo.offset); + outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size); + } + isDecodeOnlyOutputBuffer = isDecodeOnlyBuffer(outputBufferInfo.presentationTimeUs); + isLastOutputBuffer = + lastBufferInStreamPresentationTimeUs == outputBufferInfo.presentationTimeUs; + updateOutputFormatForTime(outputBufferInfo.presentationTimeUs); + } + + boolean processedOutputBuffer; + if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) { + try { + processedOutputBuffer = + processOutputBuffer( + positionUs, + elapsedRealtimeUs, + codec, + outputBuffer, + outputIndex, + outputBufferInfo.flags, + outputBufferInfo.presentationTimeUs, + isDecodeOnlyOutputBuffer, + isLastOutputBuffer, + outputFormat); + } catch (IllegalStateException e) { + processEndOfStream(); + if (outputStreamEnded) { + // Release the codec, as it's in an error state. + releaseCodec(); + } + return false; + } + } else { + processedOutputBuffer = + processOutputBuffer( + positionUs, + elapsedRealtimeUs, + codec, + outputBuffer, + outputIndex, + outputBufferInfo.flags, + outputBufferInfo.presentationTimeUs, + isDecodeOnlyOutputBuffer, + isLastOutputBuffer, + outputFormat); + } + + if (processedOutputBuffer) { + onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs); + boolean isEndOfStream = (outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; + resetOutputBuffer(); + if (!isEndOfStream) { + return true; + } + processEndOfStream(); + } + + return false; + } + + /** Processes a new output {@link MediaFormat}. */ + private void processOutputFormat() throws ExoPlaybackException { + codecHasOutputMediaFormat = true; + MediaFormat mediaFormat = codec.getOutputFormat(); + if (codecAdaptationWorkaroundMode != ADAPTATION_WORKAROUND_MODE_NEVER + && mediaFormat.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT + && mediaFormat.getInteger(MediaFormat.KEY_HEIGHT) + == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT) { + // We assume this format changed event was caused by the adaptation workaround. + shouldSkipAdaptationWorkaroundOutputBuffer = true; + return; + } + if (codecNeedsMonoChannelCountWorkaround) { + mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); + } + onOutputFormatChanged(codec, mediaFormat); + } + + /** + * Processes a change in the output buffers. + */ + private void processOutputBuffersChanged() { + if (Util.SDK_INT < 21) { + outputBuffers = codec.getOutputBuffers(); + } + } + + /** + * Processes an output media buffer. + * + * <p>When a new {@link ByteBuffer} is passed to this method its position and limit delineate the + * data to be processed. The return value indicates whether the buffer was processed in full. If + * true is returned then the next call to this method will receive a new buffer to be processed. + * If false is returned then the same buffer will be passed to the next call. An implementation of + * this method is free to modify the buffer and can assume that the buffer will not be externally + * modified between successive calls. Hence an implementation can, for example, modify the + * buffer's position to keep track of how much of the data it has processed. + * + * <p>Note that the first call to this method following a call to {@link #onPositionReset(long, + * boolean)} will always receive a new {@link ByteBuffer} to be processed. + * + * @param positionUs The current media time in microseconds, measured at the start of the current + * iteration of the rendering loop. + * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, measured at the + * start of the current iteration of the rendering loop. + * @param codec The {@link MediaCodec} instance. + * @param buffer The output buffer to process. + * @param bufferIndex The index of the output buffer. + * @param bufferFlags The flags attached to the output buffer. + * @param bufferPresentationTimeUs The presentation time of the output buffer in microseconds. + * @param isDecodeOnlyBuffer Whether the buffer was marked with {@link C#BUFFER_FLAG_DECODE_ONLY} + * by the source. + * @param isLastBuffer Whether the buffer is the last sample of the current stream. + * @param format The {@link Format} associated with the buffer. + * @return Whether the output buffer was fully processed (e.g. rendered or skipped). + * @throws ExoPlaybackException If an error occurs processing the output buffer. + */ + protected abstract boolean processOutputBuffer( + long positionUs, + long elapsedRealtimeUs, + MediaCodec codec, + ByteBuffer buffer, + int bufferIndex, + int bufferFlags, + long bufferPresentationTimeUs, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, + Format format) + throws ExoPlaybackException; + + /** + * Incrementally renders any remaining output. + * <p> + * The default implementation is a no-op. + * + * @throws ExoPlaybackException Thrown if an error occurs rendering remaining output. + */ + protected void renderToEndOfStream() throws ExoPlaybackException { + // Do nothing. + } + + /** + * Processes an end of stream signal. + * + * @throws ExoPlaybackException If an error occurs processing the signal. + */ + private void processEndOfStream() throws ExoPlaybackException { + switch (codecDrainAction) { + case DRAIN_ACTION_REINITIALIZE: + reinitializeCodec(); + break; + case DRAIN_ACTION_UPDATE_DRM_SESSION: + updateDrmSessionOrReinitializeCodecV23(); + break; + case DRAIN_ACTION_FLUSH: + flushOrReinitializeCodec(); + break; + case DRAIN_ACTION_NONE: + default: + outputStreamEnded = true; + renderToEndOfStream(); + break; + } + } + + /** + * Notifies the renderer that output end of stream is pending and should be handled on the next + * render. + */ + protected final void setPendingOutputEndOfStream() { + pendingOutputEndOfStream = true; + } + + private void reinitializeCodec() throws ExoPlaybackException { + releaseCodec(); + maybeInitCodec(); + } + + private boolean isDecodeOnlyBuffer(long presentationTimeUs) { + // We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would + // box presentationTimeUs, creating a Long object that would need to be garbage collected. + int size = decodeOnlyPresentationTimestamps.size(); + for (int i = 0; i < size; i++) { + if (decodeOnlyPresentationTimestamps.get(i) == presentationTimeUs) { + decodeOnlyPresentationTimestamps.remove(i); + return true; + } + } + return false; + } + + @TargetApi(23) + private void updateDrmSessionOrReinitializeCodecV23() throws ExoPlaybackException { + @Nullable FrameworkMediaCrypto sessionMediaCrypto = sourceDrmSession.getMediaCrypto(); + if (sessionMediaCrypto == null) { + // We'd only expect this to happen if the CDM from which the pending session is obtained needs + // provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme + // to another, where the new CDM hasn't been used before and needs provisioning). It would be + // possible to handle this case more efficiently (i.e. with a new renderer state that waits + // for provisioning to finish and then calls mediaCrypto.setMediaDrmSession), but the extra + // complexity is not warranted given how unlikely the case is to occur. + reinitializeCodec(); + return; + } + if (C.PLAYREADY_UUID.equals(sessionMediaCrypto.uuid)) { + // The PlayReady CDM does not implement setMediaDrmSession. + // TODO: Add API check once [Internal ref: b/128835874] is fixed. + reinitializeCodec(); + return; + } + + if (flushOrReinitializeCodec()) { + // The codec was reinitialized. The new codec will be using the new DRM session, so there's + // nothing more to do. + return; + } + + try { + mediaCrypto.setMediaDrmSession(sessionMediaCrypto.sessionId); + } catch (MediaCryptoException e) { + throw createRendererException(e, inputFormat); + } + setCodecDrmSession(sourceDrmSession); + codecDrainState = DRAIN_STATE_NONE; + codecDrainAction = DRAIN_ACTION_NONE; + } + + /** + * Returns whether a {@link DrmSession} may require a secure decoder for a given {@link Format}. + * + * @param drmSession The {@link DrmSession}. + * @param format The {@link Format}. + * @return Whether a secure decoder may be required. + */ + private static boolean maybeRequiresSecureDecoder( + DrmSession<FrameworkMediaCrypto> drmSession, Format format) { + @Nullable FrameworkMediaCrypto sessionMediaCrypto = drmSession.getMediaCrypto(); + if (sessionMediaCrypto == null) { + // We'd only expect this to happen if the CDM from which the pending session is obtained needs + // provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme + // to another, where the new CDM hasn't been used before and needs provisioning). Assume that + // a secure decoder may be required. + return true; + } + if (sessionMediaCrypto.forceAllowInsecureDecoderComponents) { + return false; + } + MediaCrypto mediaCrypto; + try { + mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId); + } catch (MediaCryptoException e) { + // This shouldn't happen, but if it does then assume that a secure decoder may be required. + return true; + } + try { + return mediaCrypto.requiresSecureDecoderComponent(format.sampleMimeType); + } finally { + mediaCrypto.release(); + } + } + + private static MediaCodec.CryptoInfo getFrameworkCryptoInfo( + DecoderInputBuffer buffer, int adaptiveReconfigurationBytes) { + MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfo(); + if (adaptiveReconfigurationBytes == 0) { + return cryptoInfo; + } + // There must be at least one sub-sample, although numBytesOfClearData is permitted to be + // null if it contains no clear data. Instantiate it if needed, and add the reconfiguration + // bytes to the clear byte count of the first sub-sample. + if (cryptoInfo.numBytesOfClearData == null) { + cryptoInfo.numBytesOfClearData = new int[1]; + } + cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes; + return cryptoInfo; + } + + private static boolean isMediaCodecException(IllegalStateException error) { + if (Util.SDK_INT >= 21 && isMediaCodecExceptionV21(error)) { + return true; + } + StackTraceElement[] stackTrace = error.getStackTrace(); + return stackTrace.length > 0 && stackTrace[0].getClassName().equals("android.media.MediaCodec"); + } + + @TargetApi(21) + private static boolean isMediaCodecExceptionV21(IllegalStateException error) { + return error instanceof MediaCodec.CodecException; + } + + /** + * Returns whether the decoder is known to fail when flushed. + * <p> + * If true is returned, the renderer will work around the issue by releasing the decoder and + * instantiating a new one rather than flushing the current instance. + * <p> + * See [Internal: b/8347958, b/8543366]. + * + * @param name The name of the decoder. + * @return True if the decoder is known to fail when flushed. + */ + private static boolean codecNeedsFlushWorkaround(String name) { + return Util.SDK_INT < 18 + || (Util.SDK_INT == 18 + && ("OMX.SEC.avc.dec".equals(name) || "OMX.SEC.avc.dec.secure".equals(name))) + || (Util.SDK_INT == 19 && Util.MODEL.startsWith("SM-G800") + && ("OMX.Exynos.avc.dec".equals(name) || "OMX.Exynos.avc.dec.secure".equals(name))); + } + + /** + * Returns a mode that specifies when the adaptation workaround should be enabled. + * + * <p>When enabled, the workaround queues and discards a blank frame with a resolution whose width + * and height both equal {@link #ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT}, to reset the decoder's + * internal state when a format change occurs. + * + * <p>See [Internal: b/27807182]. See <a + * href="https://github.com/google/ExoPlayer/issues/3257">GitHub issue #3257</a>. + * + * @param name The name of the decoder. + * @return The mode specifying when the adaptation workaround should be enabled. + */ + private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode(String name) { + if (Util.SDK_INT <= 25 && "OMX.Exynos.avc.dec.secure".equals(name) + && (Util.MODEL.startsWith("SM-T585") || Util.MODEL.startsWith("SM-A510") + || Util.MODEL.startsWith("SM-A520") || Util.MODEL.startsWith("SM-J700"))) { + return ADAPTATION_WORKAROUND_MODE_ALWAYS; + } else if (Util.SDK_INT < 24 + && ("OMX.Nvidia.h264.decode".equals(name) || "OMX.Nvidia.h264.decode.secure".equals(name)) + && ("flounder".equals(Util.DEVICE) || "flounder_lte".equals(Util.DEVICE) + || "grouper".equals(Util.DEVICE) || "tilapia".equals(Util.DEVICE))) { + return ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION; + } else { + return ADAPTATION_WORKAROUND_MODE_NEVER; + } + } + + /** + * Returns whether the decoder is known to fail when an attempt is made to reconfigure it with a + * new format's configuration data. + * + * <p>When enabled, the workaround will always release and recreate the decoder, rather than + * attempting to reconfigure the existing instance. + * + * @param name The name of the decoder. + * @return True if the decoder is known to fail when an attempt is made to reconfigure it with a + * new format's configuration data. + */ + private static boolean codecNeedsReconfigureWorkaround(String name) { + return Util.MODEL.startsWith("SM-T230") && "OMX.MARVELL.VIDEO.HW.CODA7542DECODER".equals(name); + } + + /** + * Returns whether the decoder is an H.264/AVC decoder known to fail if NAL units are queued + * before the codec specific data. + * + * <p>If true is returned, the renderer will work around the issue by discarding data up to the + * SPS. + * + * @param name The name of the decoder. + * @param format The {@link Format} used to configure the decoder. + * @return True if the decoder is known to fail if NAL units are queued before CSD. + */ + private static boolean codecNeedsDiscardToSpsWorkaround(String name, Format format) { + return Util.SDK_INT < 21 && format.initializationData.isEmpty() + && "OMX.MTK.VIDEO.DECODER.AVC".equals(name); + } + + /** + * Returns whether the decoder is known to handle the propagation of the {@link + * MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device. + * + * <p>If true is returned, the renderer will work around the issue by approximating end of stream + * behavior without relying on the flag being propagated through to an output buffer by the + * underlying decoder. + * + * @param codecInfo Information about the {@link MediaCodec}. + * @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} + * propagation incorrectly on the host device. False otherwise. + */ + private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) { + String name = codecInfo.name; + return (Util.SDK_INT <= 25 && "OMX.rk.video_decoder.avc".equals(name)) + || (Util.SDK_INT <= 17 && "OMX.allwinner.video.decoder.avc".equals(name)) + || ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure); + } + + /** + * Returns whether the decoder is known to behave incorrectly if flushed after receiving an input + * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set. + * <p> + * If true is returned, the renderer will work around the issue by instantiating a new decoder + * when this case occurs. + * <p> + * See [Internal: b/8578467, b/23361053]. + * + * @param name The name of the decoder. + * @return True if the decoder is known to behave incorrectly if flushed after receiving an input + * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set. False otherwise. + */ + private static boolean codecNeedsEosFlushWorkaround(String name) { + return (Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(name)) + || (Util.SDK_INT <= 19 + && ("hb2000".equals(Util.DEVICE) || "stvm8".equals(Util.DEVICE)) + && ("OMX.amlogic.avc.decoder.awesome".equals(name) + || "OMX.amlogic.avc.decoder.awesome.secure".equals(name))); + } + + /** + * Returns whether the decoder may throw an {@link IllegalStateException} from + * {@link MediaCodec#dequeueOutputBuffer(MediaCodec.BufferInfo, long)} or + * {@link MediaCodec#releaseOutputBuffer(int, boolean)} after receiving an input + * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set. + * <p> + * See [Internal: b/17933838]. + * + * @param name The name of the decoder. + * @return True if the decoder may throw an exception after receiving an end-of-stream buffer. + */ + private static boolean codecNeedsEosOutputExceptionWorkaround(String name) { + return Util.SDK_INT == 21 && "OMX.google.aac.decoder".equals(name); + } + + /** + * Returns whether the decoder is known to set the number of audio channels in the output {@link + * Format} to 2 for the given input {@link Format}, whilst only actually outputting a single + * channel. + * + * <p>If true is returned then we explicitly override the number of channels in the output {@link + * Format}, setting it to 1. + * + * @param name The decoder name. + * @param format The input {@link Format}. + * @return True if the decoder is known to set the number of audio channels in the output {@link + * Format} to 2 for the given input {@link Format}, whilst only actually outputting a single + * channel. False otherwise. + */ + private static boolean codecNeedsMonoChannelCountWorkaround(String name, Format format) { + return Util.SDK_INT <= 18 && format.channelCount == 1 + && "OMX.MTK.AUDIO.DECODER.MP3".equals(name); + } + + /** + * Returns whether the decoder is known to behave incorrectly if flushed prior to having output a + * {@link MediaFormat}. + * + * <p>If true is returned, the renderer will work around the issue by instantiating a new decoder + * when this case occurs. + * + * <p>See [Internal: b/141097367]. + * + * @param name The name of the decoder. + * @return True if the decoder is known to behave incorrectly if flushed prior to having output a + * {@link MediaFormat}. False otherwise. + */ + private static boolean codecNeedsSosFlushWorkaround(String name) { + return Util.SDK_INT == 29 && "c2.android.aac.decoder".equals(name); + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java new file mode 100644 index 0000000000..3f90c3a105 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java @@ -0,0 +1,71 @@ +/* + * 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.mediacodec; + +import android.media.MediaCodec; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import java.util.List; + +/** + * Selector of {@link MediaCodec} instances. + */ +public interface MediaCodecSelector { + + /** + * Default implementation of {@link MediaCodecSelector}, which returns the preferred decoder for + * the given format. + */ + MediaCodecSelector DEFAULT = + new MediaCodecSelector() { + @Override + public List<MediaCodecInfo> getDecoderInfos( + String mimeType, boolean requiresSecureDecoder, boolean requiresTunnelingDecoder) + throws DecoderQueryException { + return MediaCodecUtil.getDecoderInfos( + mimeType, requiresSecureDecoder, requiresTunnelingDecoder); + } + + @Override + @Nullable + public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { + return MediaCodecUtil.getPassthroughDecoderInfo(); + } + }; + + /** + * Returns a list of decoders that can decode media in the specified MIME type, in priority order. + * + * @param mimeType The MIME type for which a decoder is required. + * @param requiresSecureDecoder Whether a secure decoder is required. + * @param requiresTunnelingDecoder Whether a tunneling decoder is required. + * @return An unmodifiable list of {@link MediaCodecInfo}s corresponding to decoders. May be + * empty. + * @throws DecoderQueryException Thrown if there was an error querying decoders. + */ + List<MediaCodecInfo> getDecoderInfos( + String mimeType, boolean requiresSecureDecoder, boolean requiresTunnelingDecoder) + throws DecoderQueryException; + + /** + * Selects a decoder to instantiate for audio passthrough. + * + * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists. + * @throws DecoderQueryException Thrown if there was an error querying decoders. + */ + @Nullable + MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException; +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java new file mode 100644 index 0000000000..11fe931305 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -0,0 +1,1232 @@ +/* + * 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.mediacodec; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaCodecInfo.CodecProfileLevel; +import android.media.MediaCodecList; +import android.text.TextUtils; +import android.util.Pair; +import android.util.SparseIntArray; +import androidx.annotation.CheckResult; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +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.Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.ColorInfo; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; + +/** + * A utility class for querying the available codecs. + */ +@SuppressLint("InlinedApi") +public final class MediaCodecUtil { + + /** + * Thrown when an error occurs querying the device for its underlying media capabilities. + * <p> + * Such failures are not expected in normal operation and are normally temporary (e.g. if the + * mediaserver process has crashed and is yet to restart). + */ + public static class DecoderQueryException extends Exception { + + private DecoderQueryException(Throwable cause) { + super("Failed to query underlying media codecs", cause); + } + + } + + private static final String TAG = "MediaCodecUtil"; + private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$"); + + private static final HashMap<CodecKey, List<MediaCodecInfo>> decoderInfosCache = new HashMap<>(); + + // Codecs to constant mappings. + // AVC. + private static final SparseIntArray AVC_PROFILE_NUMBER_TO_CONST; + private static final SparseIntArray AVC_LEVEL_NUMBER_TO_CONST; + private static final String CODEC_ID_AVC1 = "avc1"; + private static final String CODEC_ID_AVC2 = "avc2"; + // VP9 + private static final SparseIntArray VP9_PROFILE_NUMBER_TO_CONST; + private static final SparseIntArray VP9_LEVEL_NUMBER_TO_CONST; + private static final String CODEC_ID_VP09 = "vp09"; + // HEVC. + private static final Map<String, Integer> HEVC_CODEC_STRING_TO_PROFILE_LEVEL; + private static final String CODEC_ID_HEV1 = "hev1"; + private static final String CODEC_ID_HVC1 = "hvc1"; + // Dolby Vision. + private static final Map<String, Integer> DOLBY_VISION_STRING_TO_PROFILE; + private static final Map<String, Integer> DOLBY_VISION_STRING_TO_LEVEL; + // AV1. + private static final SparseIntArray AV1_LEVEL_NUMBER_TO_CONST; + private static final String CODEC_ID_AV01 = "av01"; + // MP4A AAC. + private static final SparseIntArray MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE; + private static final String CODEC_ID_MP4A = "mp4a"; + + // Lazily initialized. + private static int maxH264DecodableFrameSize = -1; + + private MediaCodecUtil() {} + + /** + * Optional call to warm the codec cache for a given mime type. + * + * <p>Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean, + * boolean)} and {@link #getDecoderInfos(String, boolean, boolean)}. + * + * @param mimeType The mime type. + * @param secure Whether the decoder is required to support secure decryption. Always pass false + * unless secure decryption really is required. + * @param tunneling Whether the decoder is required to support tunneling. Always pass false unless + * tunneling really is required. + */ + public static void warmDecoderInfoCache(String mimeType, boolean secure, boolean tunneling) { + try { + getDecoderInfos(mimeType, secure, tunneling); + } catch (DecoderQueryException e) { + // Codec warming is best effort, so we can swallow the exception. + Log.e(TAG, "Codec warming failed", e); + } + } + + /** + * Returns information about a decoder suitable for audio passthrough. + * + * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists. + * @throws DecoderQueryException If there was an error querying the available decoders. + */ + @Nullable + public static MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { + @Nullable + MediaCodecInfo decoderInfo = + getDecoderInfo(MimeTypes.AUDIO_RAW, /* secure= */ false, /* tunneling= */ false); + return decoderInfo == null ? null : MediaCodecInfo.newPassthroughInstance(decoderInfo.name); + } + + /** + * Returns information about the preferred decoder for a given mime type. + * + * @param mimeType The MIME type. + * @param secure Whether the decoder is required to support secure decryption. Always pass false + * unless secure decryption really is required. + * @param tunneling Whether the decoder is required to support tunneling. Always pass false unless + * tunneling really is required. + * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists. + * @throws DecoderQueryException If there was an error querying the available decoders. + */ + @Nullable + public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure, boolean tunneling) + throws DecoderQueryException { + List<MediaCodecInfo> decoderInfos = getDecoderInfos(mimeType, secure, tunneling); + return decoderInfos.isEmpty() ? null : decoderInfos.get(0); + } + + /** + * Returns all {@link MediaCodecInfo}s for the given mime type, in the order given by {@link + * MediaCodecList}. + * + * @param mimeType The MIME type. + * @param secure Whether the decoder is required to support secure decryption. Always pass false + * unless secure decryption really is required. + * @param tunneling Whether the decoder is required to support tunneling. Always pass false unless + * tunneling really is required. + * @return An unmodifiable list of all {@link MediaCodecInfo}s for the given mime type, in the + * order given by {@link MediaCodecList}. + * @throws DecoderQueryException If there was an error querying the available decoders. + */ + public static synchronized List<MediaCodecInfo> getDecoderInfos( + String mimeType, boolean secure, boolean tunneling) throws DecoderQueryException { + CodecKey key = new CodecKey(mimeType, secure, tunneling); + @Nullable List<MediaCodecInfo> cachedDecoderInfos = decoderInfosCache.get(key); + if (cachedDecoderInfos != null) { + return cachedDecoderInfos; + } + MediaCodecListCompat mediaCodecList = + Util.SDK_INT >= 21 + ? new MediaCodecListCompatV21(secure, tunneling) + : new MediaCodecListCompatV16(); + ArrayList<MediaCodecInfo> decoderInfos = getDecoderInfosInternal(key, mediaCodecList); + if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) { + // Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the + // legacy path. We also try this path on API levels 22 and 23 as a defensive measure. + mediaCodecList = new MediaCodecListCompatV16(); + decoderInfos = getDecoderInfosInternal(key, mediaCodecList); + if (!decoderInfos.isEmpty()) { + Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType + + ". Assuming: " + decoderInfos.get(0).name); + } + } + applyWorkarounds(mimeType, decoderInfos); + List<MediaCodecInfo> unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos); + decoderInfosCache.put(key, unmodifiableDecoderInfos); + return unmodifiableDecoderInfos; + } + + /** + * Returns a copy of the provided decoder list sorted such that decoders with format support are + * listed first. The returned list is modifiable for convenience. + */ + @CheckResult + public static List<MediaCodecInfo> getDecoderInfosSortedByFormatSupport( + List<MediaCodecInfo> decoderInfos, Format format) { + decoderInfos = new ArrayList<>(decoderInfos); + sortByScore( + decoderInfos, + decoderInfo -> { + try { + return decoderInfo.isFormatSupported(format) ? 1 : 0; + } catch (DecoderQueryException e) { + return -1; + } + }); + return decoderInfos; + } + + /** + * Returns the maximum frame size supported by the default H264 decoder. + * + * @return The maximum frame size for an H264 stream that can be decoded on the device. + */ + public static int maxH264DecodableFrameSize() throws DecoderQueryException { + if (maxH264DecodableFrameSize == -1) { + int result = 0; + @Nullable + MediaCodecInfo decoderInfo = + getDecoderInfo(MimeTypes.VIDEO_H264, /* secure= */ false, /* tunneling= */ false); + if (decoderInfo != null) { + for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) { + result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result); + } + // We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are + // the levels mandated by the Android CDD. + result = Math.max(result, Util.SDK_INT >= 21 ? (720 * 480) : (480 * 360)); + } + maxH264DecodableFrameSize = result; + } + return maxH264DecodableFrameSize; + } + + /** + * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the codec + * description string (as defined by RFC 6381) of the given format. + * + * @param format Media format with a codec description string, as defined by RFC 6381. + * @return A pair (profile constant, level constant) if the codec of the {@code format} is + * well-formed and recognized, or null otherwise. + */ + @Nullable + public static Pair<Integer, Integer> getCodecProfileAndLevel(Format format) { + if (format.codecs == null) { + return null; + } + String[] parts = format.codecs.split("\\."); + // Dolby Vision can use DV, AVC or HEVC codec IDs, so check the MIME type first. + if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { + return getDolbyVisionProfileAndLevel(format.codecs, parts); + } + switch (parts[0]) { + case CODEC_ID_AVC1: + case CODEC_ID_AVC2: + return getAvcProfileAndLevel(format.codecs, parts); + case CODEC_ID_VP09: + return getVp9ProfileAndLevel(format.codecs, parts); + case CODEC_ID_HEV1: + case CODEC_ID_HVC1: + return getHevcProfileAndLevel(format.codecs, parts); + case CODEC_ID_AV01: + return getAv1ProfileAndLevel(format.codecs, parts, format.colorInfo); + case CODEC_ID_MP4A: + return getAacCodecProfileAndLevel(format.codecs, parts); + default: + return null; + } + } + + // Internal methods. + + /** + * Returns {@link MediaCodecInfo}s for the given codec {@link CodecKey} in the order given by + * {@code mediaCodecList}. + * + * @param key The codec key. + * @param mediaCodecList The codec list. + * @return The codec information for usable codecs matching the specified key. + * @throws DecoderQueryException If there was an error querying the available decoders. + */ + private static ArrayList<MediaCodecInfo> getDecoderInfosInternal( + CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException { + try { + ArrayList<MediaCodecInfo> decoderInfos = new ArrayList<>(); + String mimeType = key.mimeType; + int numberOfCodecs = mediaCodecList.getCodecCount(); + boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit(); + // Note: MediaCodecList is sorted by the framework such that the best decoders come first. + for (int i = 0; i < numberOfCodecs; i++) { + android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); + if (isAlias(codecInfo)) { + // Skip aliases of other codecs, since they will also be listed under their canonical + // names. + continue; + } + String name = codecInfo.getName(); + if (!isCodecUsableDecoder(codecInfo, name, secureDecodersExplicit, mimeType)) { + continue; + } + @Nullable String codecMimeType = getCodecMimeType(codecInfo, name, mimeType); + if (codecMimeType == null) { + continue; + } + try { + CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(codecMimeType); + boolean tunnelingSupported = + mediaCodecList.isFeatureSupported( + CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities); + boolean tunnelingRequired = + mediaCodecList.isFeatureRequired( + CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities); + if ((!key.tunneling && tunnelingRequired) || (key.tunneling && !tunnelingSupported)) { + continue; + } + boolean secureSupported = + mediaCodecList.isFeatureSupported( + CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities); + boolean secureRequired = + mediaCodecList.isFeatureRequired( + CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities); + if ((!key.secure && secureRequired) || (key.secure && !secureSupported)) { + continue; + } + boolean hardwareAccelerated = isHardwareAccelerated(codecInfo); + boolean softwareOnly = isSoftwareOnly(codecInfo); + boolean vendor = isVendor(codecInfo); + boolean forceDisableAdaptive = codecNeedsDisableAdaptationWorkaround(name); + if ((secureDecodersExplicit && key.secure == secureSupported) + || (!secureDecodersExplicit && !key.secure)) { + decoderInfos.add( + MediaCodecInfo.newInstance( + name, + mimeType, + codecMimeType, + capabilities, + hardwareAccelerated, + softwareOnly, + vendor, + forceDisableAdaptive, + /* forceSecure= */ false)); + } else if (!secureDecodersExplicit && secureSupported) { + decoderInfos.add( + MediaCodecInfo.newInstance( + name + ".secure", + mimeType, + codecMimeType, + capabilities, + hardwareAccelerated, + softwareOnly, + vendor, + forceDisableAdaptive, + /* forceSecure= */ true)); + // It only makes sense to have one synthesized secure decoder, return immediately. + return decoderInfos; + } + } catch (Exception e) { + if (Util.SDK_INT <= 23 && !decoderInfos.isEmpty()) { + // Suppress error querying secondary codec capabilities up to API level 23. + Log.e(TAG, "Skipping codec " + name + " (failed to query capabilities)"); + } else { + // Rethrow error querying primary codec capabilities, or secondary codec + // capabilities if API level is greater than 23. + Log.e(TAG, "Failed to query codec " + name + " (" + codecMimeType + ")"); + throw e; + } + } + } + return decoderInfos; + } catch (Exception e) { + // If the underlying mediaserver is in a bad state, we may catch an IllegalStateException + // or an IllegalArgumentException here. + throw new DecoderQueryException(e); + } + } + + /** + * Returns the codec's supported MIME type for media of type {@code mimeType}, or {@code null} if + * the codec can't be used. + * + * @param info The codec information. + * @param name The name of the codec + * @param mimeType The MIME type. + * @return The codec's supported MIME type for media of type {@code mimeType}, or {@code null} if + * the codec can't be used. If non-null, the returned type will be equal to {@code mimeType} + * except in cases where the codec is known to use a non-standard MIME type alias. + */ + @Nullable + private static String getCodecMimeType( + android.media.MediaCodecInfo info, + String name, + String mimeType) { + String[] supportedTypes = info.getSupportedTypes(); + for (String supportedType : supportedTypes) { + if (supportedType.equalsIgnoreCase(mimeType)) { + return supportedType; + } + } + + if (mimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) { + // Handle decoders that declare support for DV via MIME types that aren't + // video/dolby-vision. + if ("OMX.MS.HEVCDV.Decoder".equals(name)) { + return "video/hevcdv"; + } else if ("OMX.RTK.video.decoder".equals(name) + || "OMX.realtek.video.decoder.tunneled".equals(name)) { + return "video/dv_hevc"; + } + } else if (mimeType.equals(MimeTypes.AUDIO_ALAC) && "OMX.lge.alac.decoder".equals(name)) { + return "audio/x-lg-alac"; + } else if (mimeType.equals(MimeTypes.AUDIO_FLAC) && "OMX.lge.flac.decoder".equals(name)) { + return "audio/x-lg-flac"; + } + + return null; + } + + /** + * Returns whether the specified codec is usable for decoding on the current device. + * + * @param info The codec information. + * @param name The name of the codec + * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. + * @param mimeType The MIME type. + * @return Whether the specified codec is usable for decoding on the current device. + */ + private static boolean isCodecUsableDecoder( + android.media.MediaCodecInfo info, + String name, + boolean secureDecodersExplicit, + String mimeType) { + if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) { + return false; + } + + // Work around broken audio decoders. + if (Util.SDK_INT < 21 + && ("CIPAACDecoder".equals(name) + || "CIPMP3Decoder".equals(name) + || "CIPVorbisDecoder".equals(name) + || "CIPAMRNBDecoder".equals(name) + || "AACDecoder".equals(name) + || "MP3Decoder".equals(name))) { + return false; + } + + // Work around https://github.com/google/ExoPlayer/issues/1528 and + // https://github.com/google/ExoPlayer/issues/3171. + if (Util.SDK_INT < 18 + && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) + && ("a70".equals(Util.DEVICE) + || ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) { + return false; + } + + // Work around an issue where querying/creating a particular MP3 decoder on some devices on + // platform API version 16 fails. + if (Util.SDK_INT == 16 + && "OMX.qcom.audio.decoder.mp3".equals(name) + && ("dlxu".equals(Util.DEVICE) // HTC Butterfly + || "protou".equals(Util.DEVICE) // HTC Desire X + || "ville".equals(Util.DEVICE) // HTC One S + || "villeplus".equals(Util.DEVICE) + || "villec2".equals(Util.DEVICE) + || Util.DEVICE.startsWith("gee") // LGE Optimus G + || "C6602".equals(Util.DEVICE) // Sony Xperia Z + || "C6603".equals(Util.DEVICE) + || "C6606".equals(Util.DEVICE) + || "C6616".equals(Util.DEVICE) + || "L36h".equals(Util.DEVICE) + || "SO-02E".equals(Util.DEVICE))) { + return false; + } + + // Work around an issue where large timestamps are not propagated correctly. + if (Util.SDK_INT == 16 + && "OMX.qcom.audio.decoder.aac".equals(name) + && ("C1504".equals(Util.DEVICE) // Sony Xperia E + || "C1505".equals(Util.DEVICE) + || "C1604".equals(Util.DEVICE) // Sony Xperia E dual + || "C1605".equals(Util.DEVICE))) { + return false; + } + + // Work around https://github.com/google/ExoPlayer/issues/3249. + if (Util.SDK_INT < 24 + && ("OMX.SEC.aac.dec".equals(name) || "OMX.Exynos.AAC.Decoder".equals(name)) + && "samsung".equals(Util.MANUFACTURER) + && (Util.DEVICE.startsWith("zeroflte") // Galaxy S6 + || Util.DEVICE.startsWith("zerolte") // Galaxy S6 Edge + || Util.DEVICE.startsWith("zenlte") // Galaxy S6 Edge+ + || "SC-05G".equals(Util.DEVICE) // Galaxy S6 + || "marinelteatt".equals(Util.DEVICE) // Galaxy S6 Active + || "404SC".equals(Util.DEVICE) // Galaxy S6 Edge + || "SC-04G".equals(Util.DEVICE) + || "SCV31".equals(Util.DEVICE))) { + return false; + } + + // Work around https://github.com/google/ExoPlayer/issues/548. + // VP8 decoder on Samsung Galaxy S3/S4/S4 Mini/Tab 3/Note 2 does not render video. + if (Util.SDK_INT <= 19 + && "OMX.SEC.vp8.dec".equals(name) + && "samsung".equals(Util.MANUFACTURER) + && (Util.DEVICE.startsWith("d2") + || Util.DEVICE.startsWith("serrano") + || Util.DEVICE.startsWith("jflte") + || Util.DEVICE.startsWith("santos") + || Util.DEVICE.startsWith("t0"))) { + return false; + } + + // VP8 decoder on Samsung Galaxy S4 cannot be queried. + if (Util.SDK_INT <= 19 && Util.DEVICE.startsWith("jflte") + && "OMX.qcom.video.decoder.vp8".equals(name)) { + return false; + } + + // MTK E-AC3 decoder doesn't support decoding JOC streams in 2-D. See [Internal: b/69400041]. + if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType) && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) { + return false; + } + + return true; + } + + /** + * Modifies a list of {@link MediaCodecInfo}s to apply workarounds where we know better than the + * platform. + * + * @param mimeType The MIME type of input media. + * @param decoderInfos The list to modify. + */ + private static void applyWorkarounds(String mimeType, List<MediaCodecInfo> decoderInfos) { + if (MimeTypes.AUDIO_RAW.equals(mimeType)) { + if (Util.SDK_INT < 26 + && Util.DEVICE.equals("R9") + && decoderInfos.size() == 1 + && decoderInfos.get(0).name.equals("OMX.MTK.AUDIO.DECODER.RAW")) { + // This device does not list a generic raw audio decoder, yet it can be instantiated by + // name. See <a href="https://github.com/google/ExoPlayer/issues/5782">Issue #5782</a>. + decoderInfos.add( + MediaCodecInfo.newInstance( + /* name= */ "OMX.google.raw.decoder", + /* mimeType= */ MimeTypes.AUDIO_RAW, + /* codecMimeType= */ MimeTypes.AUDIO_RAW, + /* capabilities= */ null, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false)); + } + // Work around inconsistent raw audio decoding behavior across different devices. + sortByScore( + decoderInfos, + decoderInfo -> { + String name = decoderInfo.name; + if (name.startsWith("OMX.google") || name.startsWith("c2.android")) { + // Prefer generic decoders over ones provided by the device. + return 1; + } + if (Util.SDK_INT < 26 && name.equals("OMX.MTK.AUDIO.DECODER.RAW")) { + // This decoder may modify the audio, so any other compatible decoders take + // precedence. See [Internal: b/62337687]. + return -1; + } + return 0; + }); + } + + if (Util.SDK_INT < 21 && decoderInfos.size() > 1) { + String firstCodecName = decoderInfos.get(0).name; + if ("OMX.SEC.mp3.dec".equals(firstCodecName) + || "OMX.SEC.MP3.Decoder".equals(firstCodecName) + || "OMX.brcm.audio.mp3.decoder".equals(firstCodecName)) { + // Prefer OMX.google codecs over OMX.SEC.mp3.dec, OMX.SEC.MP3.Decoder and + // OMX.brcm.audio.mp3.decoder on older devices. See: + // https://github.com/google/ExoPlayer/issues/398 and + // https://github.com/google/ExoPlayer/issues/4519. + sortByScore(decoderInfos, decoderInfo -> decoderInfo.name.startsWith("OMX.google") ? 1 : 0); + } + } + + if (Util.SDK_INT < 30 && decoderInfos.size() > 1) { + String firstCodecName = decoderInfos.get(0).name; + // Prefer anything other than OMX.qti.audio.decoder.flac on older devices. See [Internal + // ref: b/147278539] and [Internal ref: b/147354613]. + if ("OMX.qti.audio.decoder.flac".equals(firstCodecName)) { + decoderInfos.add(decoderInfos.remove(0)); + } + } + } + + private static boolean isAlias(android.media.MediaCodecInfo info) { + return Util.SDK_INT >= 29 && isAliasV29(info); + } + + @RequiresApi(29) + private static boolean isAliasV29(android.media.MediaCodecInfo info) { + return info.isAlias(); + } + + /** + * The result of {@link android.media.MediaCodecInfo#isHardwareAccelerated()} for API levels 29+, + * or a best-effort approximation for lower levels. + */ + private static boolean isHardwareAccelerated(android.media.MediaCodecInfo codecInfo) { + if (Util.SDK_INT >= 29) { + return isHardwareAcceleratedV29(codecInfo); + } + // codecInfo.isHardwareAccelerated() != codecInfo.isSoftwareOnly() is not necessarily true. + // However, we assume this to be true as an approximation. + return !isSoftwareOnly(codecInfo); + } + + @TargetApi(29) + private static boolean isHardwareAcceleratedV29(android.media.MediaCodecInfo codecInfo) { + return codecInfo.isHardwareAccelerated(); + } + + /** + * The result of {@link android.media.MediaCodecInfo#isSoftwareOnly()} for API levels 29+, or a + * best-effort approximation for lower levels. + */ + private static boolean isSoftwareOnly(android.media.MediaCodecInfo codecInfo) { + if (Util.SDK_INT >= 29) { + return isSoftwareOnlyV29(codecInfo); + } + String codecName = Util.toLowerInvariant(codecInfo.getName()); + if (codecName.startsWith("arc.")) { // App Runtime for Chrome (ARC) codecs + return false; + } + return codecName.startsWith("omx.google.") + || codecName.startsWith("omx.ffmpeg.") + || (codecName.startsWith("omx.sec.") && codecName.contains(".sw.")) + || codecName.equals("omx.qcom.video.decoder.hevcswvdec") + || codecName.startsWith("c2.android.") + || codecName.startsWith("c2.google.") + || (!codecName.startsWith("omx.") && !codecName.startsWith("c2.")); + } + + @TargetApi(29) + private static boolean isSoftwareOnlyV29(android.media.MediaCodecInfo codecInfo) { + return codecInfo.isSoftwareOnly(); + } + + /** + * The result of {@link android.media.MediaCodecInfo#isVendor()} for API levels 29+, or a + * best-effort approximation for lower levels. + */ + private static boolean isVendor(android.media.MediaCodecInfo codecInfo) { + if (Util.SDK_INT >= 29) { + return isVendorV29(codecInfo); + } + String codecName = Util.toLowerInvariant(codecInfo.getName()); + return !codecName.startsWith("omx.google.") + && !codecName.startsWith("c2.android.") + && !codecName.startsWith("c2.google."); + } + + @TargetApi(29) + private static boolean isVendorV29(android.media.MediaCodecInfo codecInfo) { + return codecInfo.isVendor(); + } + + /** + * Returns whether the decoder is known to fail when adapting, despite advertising itself as an + * adaptive decoder. + * + * @param name The decoder name. + * @return True if the decoder is known to fail when adapting. + */ + private static boolean codecNeedsDisableAdaptationWorkaround(String name) { + return Util.SDK_INT <= 22 + && ("ODROID-XU3".equals(Util.MODEL) || "Nexus 10".equals(Util.MODEL)) + && ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name)); + } + + @Nullable + private static Pair<Integer, Integer> getDolbyVisionProfileAndLevel( + String codec, String[] parts) { + if (parts.length < 3) { + // The codec has fewer parts than required by the Dolby Vision codec string format. + Log.w(TAG, "Ignoring malformed Dolby Vision codec string: " + codec); + return null; + } + // The profile_space gets ignored. + Matcher matcher = PROFILE_PATTERN.matcher(parts[1]); + if (!matcher.matches()) { + Log.w(TAG, "Ignoring malformed Dolby Vision codec string: " + codec); + return null; + } + @Nullable String profileString = matcher.group(1); + @Nullable Integer profile = DOLBY_VISION_STRING_TO_PROFILE.get(profileString); + if (profile == null) { + Log.w(TAG, "Unknown Dolby Vision profile string: " + profileString); + return null; + } + String levelString = parts[2]; + @Nullable Integer level = DOLBY_VISION_STRING_TO_LEVEL.get(levelString); + if (level == null) { + Log.w(TAG, "Unknown Dolby Vision level string: " + levelString); + return null; + } + return new Pair<>(profile, level); + } + + @Nullable + private static Pair<Integer, Integer> getHevcProfileAndLevel(String codec, String[] parts) { + if (parts.length < 4) { + // The codec has fewer parts than required by the HEVC codec string format. + Log.w(TAG, "Ignoring malformed HEVC codec string: " + codec); + return null; + } + // The profile_space gets ignored. + Matcher matcher = PROFILE_PATTERN.matcher(parts[1]); + if (!matcher.matches()) { + Log.w(TAG, "Ignoring malformed HEVC codec string: " + codec); + return null; + } + @Nullable String profileString = matcher.group(1); + int profile; + if ("1".equals(profileString)) { + profile = CodecProfileLevel.HEVCProfileMain; + } else if ("2".equals(profileString)) { + profile = CodecProfileLevel.HEVCProfileMain10; + } else { + Log.w(TAG, "Unknown HEVC profile string: " + profileString); + return null; + } + @Nullable String levelString = parts[3]; + @Nullable Integer level = HEVC_CODEC_STRING_TO_PROFILE_LEVEL.get(levelString); + if (level == null) { + Log.w(TAG, "Unknown HEVC level string: " + levelString); + return null; + } + return new Pair<>(profile, level); + } + + @Nullable + private static Pair<Integer, Integer> getAvcProfileAndLevel(String codec, String[] parts) { + if (parts.length < 2) { + // The codec has fewer parts than required by the AVC codec string format. + Log.w(TAG, "Ignoring malformed AVC codec string: " + codec); + return null; + } + int profileInteger; + int levelInteger; + try { + if (parts[1].length() == 6) { + // Format: avc1.xxccyy, where xx is profile and yy level, both hexadecimal. + profileInteger = Integer.parseInt(parts[1].substring(0, 2), 16); + levelInteger = Integer.parseInt(parts[1].substring(4), 16); + } else if (parts.length >= 3) { + // Format: avc1.xx.[y]yy where xx is profile and [y]yy level, both decimal. + profileInteger = Integer.parseInt(parts[1]); + levelInteger = Integer.parseInt(parts[2]); + } else { + // We don't recognize the format. + Log.w(TAG, "Ignoring malformed AVC codec string: " + codec); + return null; + } + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed AVC codec string: " + codec); + return null; + } + + int profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger, -1); + if (profile == -1) { + Log.w(TAG, "Unknown AVC profile: " + profileInteger); + return null; + } + int level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1); + if (level == -1) { + Log.w(TAG, "Unknown AVC level: " + levelInteger); + return null; + } + return new Pair<>(profile, level); + } + + @Nullable + private static Pair<Integer, Integer> getVp9ProfileAndLevel(String codec, String[] parts) { + if (parts.length < 3) { + Log.w(TAG, "Ignoring malformed VP9 codec string: " + codec); + return null; + } + int profileInteger; + int levelInteger; + try { + profileInteger = Integer.parseInt(parts[1]); + levelInteger = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed VP9 codec string: " + codec); + return null; + } + + int profile = VP9_PROFILE_NUMBER_TO_CONST.get(profileInteger, -1); + if (profile == -1) { + Log.w(TAG, "Unknown VP9 profile: " + profileInteger); + return null; + } + int level = VP9_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1); + if (level == -1) { + Log.w(TAG, "Unknown VP9 level: " + levelInteger); + return null; + } + return new Pair<>(profile, level); + } + + @Nullable + private static Pair<Integer, Integer> getAv1ProfileAndLevel( + String codec, String[] parts, @Nullable ColorInfo colorInfo) { + if (parts.length < 4) { + Log.w(TAG, "Ignoring malformed AV1 codec string: " + codec); + return null; + } + int profileInteger; + int levelInteger; + int bitDepthInteger; + try { + profileInteger = Integer.parseInt(parts[1]); + levelInteger = Integer.parseInt(parts[2].substring(0, 2)); + bitDepthInteger = Integer.parseInt(parts[3]); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed AV1 codec string: " + codec); + return null; + } + + if (profileInteger != 0) { + Log.w(TAG, "Unknown AV1 profile: " + profileInteger); + return null; + } + if (bitDepthInteger != 8 && bitDepthInteger != 10) { + Log.w(TAG, "Unknown AV1 bit depth: " + bitDepthInteger); + return null; + } + int profile; + if (bitDepthInteger == 8) { + profile = CodecProfileLevel.AV1ProfileMain8; + } else if (colorInfo != null + && (colorInfo.hdrStaticInfo != null + || colorInfo.colorTransfer == C.COLOR_TRANSFER_HLG + || colorInfo.colorTransfer == C.COLOR_TRANSFER_ST2084)) { + profile = CodecProfileLevel.AV1ProfileMain10HDR10; + } else { + profile = CodecProfileLevel.AV1ProfileMain10; + } + + int level = AV1_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1); + if (level == -1) { + Log.w(TAG, "Unknown AV1 level: " + levelInteger); + return null; + } + return new Pair<>(profile, level); + } + + /** + * Conversion values taken from ISO 14496-10 Table A-1. + * + * @param avcLevel one of CodecProfileLevel.AVCLevel* constants. + * @return maximum frame size that can be decoded by a decoder with the specified avc level + * (or {@code -1} if the level is not recognized) + */ + private static int avcLevelToMaxFrameSize(int avcLevel) { + switch (avcLevel) { + case CodecProfileLevel.AVCLevel1: + case CodecProfileLevel.AVCLevel1b: + return 99 * 16 * 16; + case CodecProfileLevel.AVCLevel12: + case CodecProfileLevel.AVCLevel13: + case CodecProfileLevel.AVCLevel2: + return 396 * 16 * 16; + case CodecProfileLevel.AVCLevel21: + return 792 * 16 * 16; + case CodecProfileLevel.AVCLevel22: + case CodecProfileLevel.AVCLevel3: + return 1620 * 16 * 16; + case CodecProfileLevel.AVCLevel31: + return 3600 * 16 * 16; + case CodecProfileLevel.AVCLevel32: + return 5120 * 16 * 16; + case CodecProfileLevel.AVCLevel4: + case CodecProfileLevel.AVCLevel41: + return 8192 * 16 * 16; + case CodecProfileLevel.AVCLevel42: + return 8704 * 16 * 16; + case CodecProfileLevel.AVCLevel5: + return 22080 * 16 * 16; + case CodecProfileLevel.AVCLevel51: + case CodecProfileLevel.AVCLevel52: + return 36864 * 16 * 16; + default: + return -1; + } + } + + @Nullable + private static Pair<Integer, Integer> getAacCodecProfileAndLevel(String codec, String[] parts) { + if (parts.length != 3) { + Log.w(TAG, "Ignoring malformed MP4A codec string: " + codec); + return null; + } + try { + // Get the object type indication, which is a hexadecimal value (see RFC 6381/ISO 14496-1). + int objectTypeIndication = Integer.parseInt(parts[1], 16); + @Nullable String mimeType = MimeTypes.getMimeTypeFromMp4ObjectType(objectTypeIndication); + if (MimeTypes.AUDIO_AAC.equals(mimeType)) { + // For MPEG-4 audio this is followed by an audio object type indication as a decimal number. + int audioObjectTypeIndication = Integer.parseInt(parts[2]); + int profile = MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.get(audioObjectTypeIndication, -1); + if (profile != -1) { + // Level is set to zero in AAC decoder CodecProfileLevels. + return new Pair<>(profile, 0); + } + } + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed MP4A codec string: " + codec); + } + return null; + } + + /** Stably sorts the provided {@code list} in-place, in order of decreasing score. */ + private static <T> void sortByScore(List<T> list, ScoreProvider<T> scoreProvider) { + Collections.sort(list, (a, b) -> scoreProvider.getScore(b) - scoreProvider.getScore(a)); + } + + /** Interface for providers of item scores. */ + private interface ScoreProvider<T> { + /** Returns the score of the provided item. */ + int getScore(T t); + } + + private interface MediaCodecListCompat { + + /** + * The number of codecs in the list. + */ + int getCodecCount(); + + /** + * The info at the specified index in the list. + * + * @param index The index. + */ + android.media.MediaCodecInfo getCodecInfoAt(int index); + + /** + * Returns whether secure decoders are explicitly listed, if present. + */ + boolean secureDecodersExplicit(); + + /** Whether the specified {@link CodecCapabilities} {@code feature} is supported. */ + boolean isFeatureSupported(String feature, String mimeType, CodecCapabilities capabilities); + + /** Whether the specified {@link CodecCapabilities} {@code feature} is required. */ + boolean isFeatureRequired(String feature, String mimeType, CodecCapabilities capabilities); + } + + @TargetApi(21) + private static final class MediaCodecListCompatV21 implements MediaCodecListCompat { + + private final int codecKind; + + @Nullable private android.media.MediaCodecInfo[] mediaCodecInfos; + + // the constructor does not initialize fields: mediaCodecInfos + @SuppressWarnings("nullness:initialization.fields.uninitialized") + public MediaCodecListCompatV21(boolean includeSecure, boolean includeTunneling) { + codecKind = + includeSecure || includeTunneling + ? MediaCodecList.ALL_CODECS + : MediaCodecList.REGULAR_CODECS; + } + + @Override + public int getCodecCount() { + ensureMediaCodecInfosInitialized(); + return mediaCodecInfos.length; + } + + // incompatible types in return. + @SuppressWarnings("nullness:return.type.incompatible") + @Override + public android.media.MediaCodecInfo getCodecInfoAt(int index) { + ensureMediaCodecInfosInitialized(); + return mediaCodecInfos[index]; + } + + @Override + public boolean secureDecodersExplicit() { + return true; + } + + @Override + public boolean isFeatureSupported( + String feature, String mimeType, CodecCapabilities capabilities) { + return capabilities.isFeatureSupported(feature); + } + + @Override + public boolean isFeatureRequired( + String feature, String mimeType, CodecCapabilities capabilities) { + return capabilities.isFeatureRequired(feature); + } + + @EnsuresNonNull({"mediaCodecInfos"}) + private void ensureMediaCodecInfosInitialized() { + if (mediaCodecInfos == null) { + mediaCodecInfos = new MediaCodecList(codecKind).getCodecInfos(); + } + } + + } + + private static final class MediaCodecListCompatV16 implements MediaCodecListCompat { + + @Override + public int getCodecCount() { + return MediaCodecList.getCodecCount(); + } + + @Override + public android.media.MediaCodecInfo getCodecInfoAt(int index) { + return MediaCodecList.getCodecInfoAt(index); + } + + @Override + public boolean secureDecodersExplicit() { + return false; + } + + @Override + public boolean isFeatureSupported( + String feature, String mimeType, CodecCapabilities capabilities) { + // Secure decoders weren't explicitly listed prior to API level 21. We assume that a secure + // H264 decoder exists. + return CodecCapabilities.FEATURE_SecurePlayback.equals(feature) + && MimeTypes.VIDEO_H264.equals(mimeType); + } + + @Override + public boolean isFeatureRequired( + String feature, String mimeType, CodecCapabilities capabilities) { + return false; + } + + } + + private static final class CodecKey { + + public final String mimeType; + public final boolean secure; + public final boolean tunneling; + + public CodecKey(String mimeType, boolean secure, boolean tunneling) { + this.mimeType = mimeType; + this.secure = secure; + this.tunneling = tunneling; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mimeType.hashCode(); + result = prime * result + (secure ? 1231 : 1237); + result = prime * result + (tunneling ? 1231 : 1237); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || obj.getClass() != CodecKey.class) { + return false; + } + CodecKey other = (CodecKey) obj; + return TextUtils.equals(mimeType, other.mimeType) + && secure == other.secure + && tunneling == other.tunneling; + } + + } + + static { + AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray(); + AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline); + AVC_PROFILE_NUMBER_TO_CONST.put(77, CodecProfileLevel.AVCProfileMain); + AVC_PROFILE_NUMBER_TO_CONST.put(88, CodecProfileLevel.AVCProfileExtended); + AVC_PROFILE_NUMBER_TO_CONST.put(100, CodecProfileLevel.AVCProfileHigh); + AVC_PROFILE_NUMBER_TO_CONST.put(110, CodecProfileLevel.AVCProfileHigh10); + AVC_PROFILE_NUMBER_TO_CONST.put(122, CodecProfileLevel.AVCProfileHigh422); + AVC_PROFILE_NUMBER_TO_CONST.put(244, CodecProfileLevel.AVCProfileHigh444); + + AVC_LEVEL_NUMBER_TO_CONST = new SparseIntArray(); + AVC_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.AVCLevel1); + // TODO: Find int for CodecProfileLevel.AVCLevel1b. + AVC_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.AVCLevel11); + AVC_LEVEL_NUMBER_TO_CONST.put(12, CodecProfileLevel.AVCLevel12); + AVC_LEVEL_NUMBER_TO_CONST.put(13, CodecProfileLevel.AVCLevel13); + AVC_LEVEL_NUMBER_TO_CONST.put(20, CodecProfileLevel.AVCLevel2); + AVC_LEVEL_NUMBER_TO_CONST.put(21, CodecProfileLevel.AVCLevel21); + AVC_LEVEL_NUMBER_TO_CONST.put(22, CodecProfileLevel.AVCLevel22); + AVC_LEVEL_NUMBER_TO_CONST.put(30, CodecProfileLevel.AVCLevel3); + AVC_LEVEL_NUMBER_TO_CONST.put(31, CodecProfileLevel.AVCLevel31); + AVC_LEVEL_NUMBER_TO_CONST.put(32, CodecProfileLevel.AVCLevel32); + AVC_LEVEL_NUMBER_TO_CONST.put(40, CodecProfileLevel.AVCLevel4); + AVC_LEVEL_NUMBER_TO_CONST.put(41, CodecProfileLevel.AVCLevel41); + AVC_LEVEL_NUMBER_TO_CONST.put(42, CodecProfileLevel.AVCLevel42); + AVC_LEVEL_NUMBER_TO_CONST.put(50, CodecProfileLevel.AVCLevel5); + AVC_LEVEL_NUMBER_TO_CONST.put(51, CodecProfileLevel.AVCLevel51); + AVC_LEVEL_NUMBER_TO_CONST.put(52, CodecProfileLevel.AVCLevel52); + + VP9_PROFILE_NUMBER_TO_CONST = new SparseIntArray(); + VP9_PROFILE_NUMBER_TO_CONST.put(0, CodecProfileLevel.VP9Profile0); + VP9_PROFILE_NUMBER_TO_CONST.put(1, CodecProfileLevel.VP9Profile1); + VP9_PROFILE_NUMBER_TO_CONST.put(2, CodecProfileLevel.VP9Profile2); + VP9_PROFILE_NUMBER_TO_CONST.put(3, CodecProfileLevel.VP9Profile3); + VP9_LEVEL_NUMBER_TO_CONST = new SparseIntArray(); + VP9_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.VP9Level1); + VP9_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.VP9Level11); + VP9_LEVEL_NUMBER_TO_CONST.put(20, CodecProfileLevel.VP9Level2); + VP9_LEVEL_NUMBER_TO_CONST.put(21, CodecProfileLevel.VP9Level21); + VP9_LEVEL_NUMBER_TO_CONST.put(30, CodecProfileLevel.VP9Level3); + VP9_LEVEL_NUMBER_TO_CONST.put(31, CodecProfileLevel.VP9Level31); + VP9_LEVEL_NUMBER_TO_CONST.put(40, CodecProfileLevel.VP9Level4); + VP9_LEVEL_NUMBER_TO_CONST.put(41, CodecProfileLevel.VP9Level41); + VP9_LEVEL_NUMBER_TO_CONST.put(50, CodecProfileLevel.VP9Level5); + VP9_LEVEL_NUMBER_TO_CONST.put(51, CodecProfileLevel.VP9Level51); + VP9_LEVEL_NUMBER_TO_CONST.put(60, CodecProfileLevel.VP9Level6); + VP9_LEVEL_NUMBER_TO_CONST.put(61, CodecProfileLevel.VP9Level61); + VP9_LEVEL_NUMBER_TO_CONST.put(62, CodecProfileLevel.VP9Level62); + + HEVC_CODEC_STRING_TO_PROFILE_LEVEL = new HashMap<>(); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L30", CodecProfileLevel.HEVCMainTierLevel1); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L60", CodecProfileLevel.HEVCMainTierLevel2); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L63", CodecProfileLevel.HEVCMainTierLevel21); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L90", CodecProfileLevel.HEVCMainTierLevel3); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L93", CodecProfileLevel.HEVCMainTierLevel31); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L120", CodecProfileLevel.HEVCMainTierLevel4); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L123", CodecProfileLevel.HEVCMainTierLevel41); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L150", CodecProfileLevel.HEVCMainTierLevel5); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L153", CodecProfileLevel.HEVCMainTierLevel51); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L156", CodecProfileLevel.HEVCMainTierLevel52); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L180", CodecProfileLevel.HEVCMainTierLevel6); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L183", CodecProfileLevel.HEVCMainTierLevel61); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L186", CodecProfileLevel.HEVCMainTierLevel62); + + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H30", CodecProfileLevel.HEVCHighTierLevel1); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H60", CodecProfileLevel.HEVCHighTierLevel2); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H63", CodecProfileLevel.HEVCHighTierLevel21); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H90", CodecProfileLevel.HEVCHighTierLevel3); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H93", CodecProfileLevel.HEVCHighTierLevel31); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H120", CodecProfileLevel.HEVCHighTierLevel4); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H123", CodecProfileLevel.HEVCHighTierLevel41); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H150", CodecProfileLevel.HEVCHighTierLevel5); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H153", CodecProfileLevel.HEVCHighTierLevel51); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H156", CodecProfileLevel.HEVCHighTierLevel52); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H180", CodecProfileLevel.HEVCHighTierLevel6); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H183", CodecProfileLevel.HEVCHighTierLevel61); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H186", CodecProfileLevel.HEVCHighTierLevel62); + + DOLBY_VISION_STRING_TO_PROFILE = new HashMap<>(); + DOLBY_VISION_STRING_TO_PROFILE.put("00", CodecProfileLevel.DolbyVisionProfileDvavPer); + DOLBY_VISION_STRING_TO_PROFILE.put("01", CodecProfileLevel.DolbyVisionProfileDvavPen); + DOLBY_VISION_STRING_TO_PROFILE.put("02", CodecProfileLevel.DolbyVisionProfileDvheDer); + DOLBY_VISION_STRING_TO_PROFILE.put("03", CodecProfileLevel.DolbyVisionProfileDvheDen); + DOLBY_VISION_STRING_TO_PROFILE.put("04", CodecProfileLevel.DolbyVisionProfileDvheDtr); + DOLBY_VISION_STRING_TO_PROFILE.put("05", CodecProfileLevel.DolbyVisionProfileDvheStn); + DOLBY_VISION_STRING_TO_PROFILE.put("06", CodecProfileLevel.DolbyVisionProfileDvheDth); + DOLBY_VISION_STRING_TO_PROFILE.put("07", CodecProfileLevel.DolbyVisionProfileDvheDtb); + DOLBY_VISION_STRING_TO_PROFILE.put("08", CodecProfileLevel.DolbyVisionProfileDvheSt); + DOLBY_VISION_STRING_TO_PROFILE.put("09", CodecProfileLevel.DolbyVisionProfileDvavSe); + + DOLBY_VISION_STRING_TO_LEVEL = new HashMap<>(); + DOLBY_VISION_STRING_TO_LEVEL.put("01", CodecProfileLevel.DolbyVisionLevelHd24); + DOLBY_VISION_STRING_TO_LEVEL.put("02", CodecProfileLevel.DolbyVisionLevelHd30); + DOLBY_VISION_STRING_TO_LEVEL.put("03", CodecProfileLevel.DolbyVisionLevelFhd24); + DOLBY_VISION_STRING_TO_LEVEL.put("04", CodecProfileLevel.DolbyVisionLevelFhd30); + DOLBY_VISION_STRING_TO_LEVEL.put("05", CodecProfileLevel.DolbyVisionLevelFhd60); + DOLBY_VISION_STRING_TO_LEVEL.put("06", CodecProfileLevel.DolbyVisionLevelUhd24); + DOLBY_VISION_STRING_TO_LEVEL.put("07", CodecProfileLevel.DolbyVisionLevelUhd30); + DOLBY_VISION_STRING_TO_LEVEL.put("08", CodecProfileLevel.DolbyVisionLevelUhd48); + DOLBY_VISION_STRING_TO_LEVEL.put("09", CodecProfileLevel.DolbyVisionLevelUhd60); + + // See https://aomediacodec.github.io/av1-spec/av1-spec.pdf Annex A: Profiles and levels for + // more information on mapping AV1 codec strings to levels. + AV1_LEVEL_NUMBER_TO_CONST = new SparseIntArray(); + AV1_LEVEL_NUMBER_TO_CONST.put(0, CodecProfileLevel.AV1Level2); + AV1_LEVEL_NUMBER_TO_CONST.put(1, CodecProfileLevel.AV1Level21); + AV1_LEVEL_NUMBER_TO_CONST.put(2, CodecProfileLevel.AV1Level22); + AV1_LEVEL_NUMBER_TO_CONST.put(3, CodecProfileLevel.AV1Level23); + AV1_LEVEL_NUMBER_TO_CONST.put(4, CodecProfileLevel.AV1Level3); + AV1_LEVEL_NUMBER_TO_CONST.put(5, CodecProfileLevel.AV1Level31); + AV1_LEVEL_NUMBER_TO_CONST.put(6, CodecProfileLevel.AV1Level32); + AV1_LEVEL_NUMBER_TO_CONST.put(7, CodecProfileLevel.AV1Level33); + AV1_LEVEL_NUMBER_TO_CONST.put(8, CodecProfileLevel.AV1Level4); + AV1_LEVEL_NUMBER_TO_CONST.put(9, CodecProfileLevel.AV1Level41); + AV1_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.AV1Level42); + AV1_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.AV1Level43); + AV1_LEVEL_NUMBER_TO_CONST.put(12, CodecProfileLevel.AV1Level5); + AV1_LEVEL_NUMBER_TO_CONST.put(13, CodecProfileLevel.AV1Level51); + AV1_LEVEL_NUMBER_TO_CONST.put(14, CodecProfileLevel.AV1Level52); + AV1_LEVEL_NUMBER_TO_CONST.put(15, CodecProfileLevel.AV1Level53); + AV1_LEVEL_NUMBER_TO_CONST.put(16, CodecProfileLevel.AV1Level6); + AV1_LEVEL_NUMBER_TO_CONST.put(17, CodecProfileLevel.AV1Level61); + AV1_LEVEL_NUMBER_TO_CONST.put(18, CodecProfileLevel.AV1Level62); + AV1_LEVEL_NUMBER_TO_CONST.put(19, CodecProfileLevel.AV1Level63); + AV1_LEVEL_NUMBER_TO_CONST.put(20, CodecProfileLevel.AV1Level7); + AV1_LEVEL_NUMBER_TO_CONST.put(21, CodecProfileLevel.AV1Level71); + AV1_LEVEL_NUMBER_TO_CONST.put(22, CodecProfileLevel.AV1Level72); + AV1_LEVEL_NUMBER_TO_CONST.put(23, CodecProfileLevel.AV1Level73); + + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE = new SparseIntArray(); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(1, CodecProfileLevel.AACObjectMain); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(2, CodecProfileLevel.AACObjectLC); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(3, CodecProfileLevel.AACObjectSSR); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(4, CodecProfileLevel.AACObjectLTP); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(5, CodecProfileLevel.AACObjectHE); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(6, CodecProfileLevel.AACObjectScalable); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(17, CodecProfileLevel.AACObjectERLC); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(20, CodecProfileLevel.AACObjectERScalable); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(23, CodecProfileLevel.AACObjectLD); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(29, CodecProfileLevel.AACObjectHE_PS); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(39, CodecProfileLevel.AACObjectELD); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(42, CodecProfileLevel.AACObjectXHE); + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java new file mode 100644 index 0000000000..cafaaa7c83 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2018 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.mediacodec; + +import android.media.MediaFormat; +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.ColorInfo; +import java.nio.ByteBuffer; +import java.util.List; + +/** Helper class for configuring {@link MediaFormat} instances. */ +public final class MediaFormatUtil { + + private MediaFormatUtil() {} + + /** + * Sets a {@link MediaFormat} {@link String} value. + * + * @param format The {@link MediaFormat} being configured. + * @param key The key to set. + * @param value The value to set. + */ + public static void setString(MediaFormat format, String key, String value) { + format.setString(key, value); + } + + /** + * Sets a {@link MediaFormat}'s codec specific data buffers. + * + * @param format The {@link MediaFormat} being configured. + * @param csdBuffers The csd buffers to set. + */ + public static void setCsdBuffers(MediaFormat format, List<byte[]> csdBuffers) { + for (int i = 0; i < csdBuffers.size(); i++) { + format.setByteBuffer("csd-" + i, ByteBuffer.wrap(csdBuffers.get(i))); + } + } + + /** + * Sets a {@link MediaFormat} integer value. Does nothing if {@code value} is {@link + * Format#NO_VALUE}. + * + * @param format The {@link MediaFormat} being configured. + * @param key The key to set. + * @param value The value to set. + */ + public static void maybeSetInteger(MediaFormat format, String key, int value) { + if (value != Format.NO_VALUE) { + format.setInteger(key, value); + } + } + + /** + * Sets a {@link MediaFormat} float value. Does nothing if {@code value} is {@link + * Format#NO_VALUE}. + * + * @param format The {@link MediaFormat} being configured. + * @param key The key to set. + * @param value The value to set. + */ + public static void maybeSetFloat(MediaFormat format, String key, float value) { + if (value != Format.NO_VALUE) { + format.setFloat(key, value); + } + } + + /** + * Sets a {@link MediaFormat} {@link ByteBuffer} value. Does nothing if {@code value} is null. + * + * @param format The {@link MediaFormat} being configured. + * @param key The key to set. + * @param value The {@link byte[]} that will be wrapped to obtain the value. + */ + public static void maybeSetByteBuffer(MediaFormat format, String key, @Nullable byte[] value) { + if (value != null) { + format.setByteBuffer(key, ByteBuffer.wrap(value)); + } + } + + /** + * Sets a {@link MediaFormat}'s color information. Does nothing if {@code colorInfo} is null. + * + * @param format The {@link MediaFormat} being configured. + * @param colorInfo The color info to set. + */ + @SuppressWarnings("InlinedApi") + public static void maybeSetColorInfo(MediaFormat format, @Nullable ColorInfo colorInfo) { + if (colorInfo != null) { + maybeSetInteger(format, MediaFormat.KEY_COLOR_TRANSFER, colorInfo.colorTransfer); + maybeSetInteger(format, MediaFormat.KEY_COLOR_STANDARD, colorInfo.colorSpace); + maybeSetInteger(format, MediaFormat.KEY_COLOR_RANGE, colorInfo.colorRange); + maybeSetByteBuffer(format, MediaFormat.KEY_HDR_STATIC_INFO, colorInfo.hdrStaticInfo); + } + } +} diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/package-info.java new file mode 100644 index 0000000000..c8dd17d0df --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec; + +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi; |