diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java')
-rw-r--r-- | mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java | 617 |
1 files changed, 617 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; + } +} |