summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java617
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java2014
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java71
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java1232
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java109
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/package-info.java19
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;