chromium/media/base/android/java/src/org/chromium/media/VideoAcceleratorUtil.java

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.media;

import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecProfileLevel;
import android.media.MediaCodecList;
import android.os.Build;
import android.util.Range;

import androidx.annotation.RequiresApi;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;

import org.chromium.base.Log;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/**
 * A collection of SDK based helper functions for retrieving supported profiles
 * for accelerated encoders and decoders from MediaCodecInfo. Only called from
 * the GPU process, so doesn't need to be tagged with MainDex.
 */
@JNINamespace("media")
class VideoAcceleratorUtil {
    private static final String TAG = "VAUtil";

    private static final String[] SUPPORTED_ENCODER_TYPES = {
        MediaCodecUtil.MimeTypes.VIDEO_VP8,
        MediaCodecUtil.MimeTypes.VIDEO_VP9,
        MediaCodecUtil.MimeTypes.VIDEO_AV1,
        MediaCodecUtil.MimeTypes.VIDEO_H264,
        MediaCodecUtil.MimeTypes.VIDEO_HEVC,
    };

    private static final String[] SUPPORTED_DECODER_TYPES = {
        MediaCodecUtil.MimeTypes.VIDEO_VP8,
        MediaCodecUtil.MimeTypes.VIDEO_VP9,
        MediaCodecUtil.MimeTypes.VIDEO_AV1,
        MediaCodecUtil.MimeTypes.VIDEO_H264,
        MediaCodecUtil.MimeTypes.VIDEO_HEVC,
        MediaCodecUtil.MimeTypes.VIDEO_DV,
    };

    // Encoders known to support temporal layers.
    private static final Set<String> TEMPORAL_SVC_SUPPORTING_ENCODERS =
            Set.of("c2.qti.avc.encoder", "c2.exynos.h264.encoder");

    private static class SupportedProfileAdapter {
        public int profile;
        public int level;
        public int maxWidth;
        public int maxHeight;
        public int minWidth;
        public int minHeight;
        public int maxFramerateNumerator;
        public int maxFramerateDenominator;
        public boolean supportsCbr;
        public boolean supportsVbr;
        public String name;
        public boolean isSoftwareCodec;
        public boolean supportsSecurePlayback;
        public boolean requiresSecurePlayback;
        public int maxNumberOfTemporalLayers;

        @CalledByNative("SupportedProfileAdapter")
        public int getProfile() {
            return this.profile;
        }

        @CalledByNative("SupportedProfileAdapter")
        public int getLevel() {
            return this.level;
        }

        @CalledByNative("SupportedProfileAdapter")
        public int getMaxWidth() {
            return this.maxWidth;
        }

        @CalledByNative("SupportedProfileAdapter")
        public int getMaxHeight() {
            return this.maxHeight;
        }

        @CalledByNative("SupportedProfileAdapter")
        public int getMinWidth() {
            return this.minWidth;
        }

        @CalledByNative("SupportedProfileAdapter")
        public int getMinHeight() {
            return this.minHeight;
        }

        @CalledByNative("SupportedProfileAdapter")
        public int getMaxFramerateNumerator() {
            return this.maxFramerateNumerator;
        }

        @CalledByNative("SupportedProfileAdapter")
        public int getMaxFramerateDenominator() {
            return this.maxFramerateDenominator;
        }

        @CalledByNative("SupportedProfileAdapter")
        public boolean supportsCbr() {
            return this.supportsCbr;
        }

        @CalledByNative("SupportedProfileAdapter")
        public boolean supportsVbr() {
            return this.supportsVbr;
        }

        @CalledByNative("SupportedProfileAdapter")
        public String getName() {
            return this.name;
        }

        @CalledByNative("SupportedProfileAdapter")
        public boolean isSoftwareCodec() {
            return this.isSoftwareCodec;
        }

        @CalledByNative("SupportedProfileAdapter")
        public boolean supportsSecurePlayback() {
            return this.supportsSecurePlayback;
        }

        @CalledByNative("SupportedProfileAdapter")
        public boolean requiresSecurePlayback() {
            return this.requiresSecurePlayback;
        }

        @CalledByNative("SupportedProfileAdapter")
        public int getMaxNumberOfTemporalLayers() {
            return this.maxNumberOfTemporalLayers;
        }
    }

    // Currently our encoder only supports NV12.
    private static boolean hasSupportedColorFormat(int[] colorFormats) {
        for (int format : colorFormats) {
            if (format == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar) {
                return true;
            }
        }
        return false;
    }

    // Chromium doesn't bundle a software encoder or decoder for H.264 or H.265 so allow
    // usage of software codecs through MediaCodec for those codecs.
    private static boolean requiresHardware(String type) {
        return !type.equalsIgnoreCase(MediaCodecUtil.MimeTypes.VIDEO_H264)
                && !type.equalsIgnoreCase(MediaCodecUtil.MimeTypes.VIDEO_HEVC);
    }

    // H.264 high profile isn't required by Android platform, so we can only add support if
    // we know its supported by the underlying codec.
    private static boolean hasHighProfileSupport(String name) {
        var lowerName = name.toLowerCase(Locale.ROOT);

        // Some platforms seem to have a trailing `.` in the name...
        return lowerName.startsWith("omx.google.h264.decoder")
                || lowerName.startsWith("c2.android.avc.decoder");
    }

    // Return true if and only if this is a low latency decoder.
    private static boolean isLowLatency(String name) {
        var lowerName = name.toLowerCase(Locale.ROOT);
        // This is usually a hw decoder provided by the OEM vendors.
        return lowerName.endsWith(".low_latency");
    }

    private static int getNumberOfTemporalLayers(String name) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
            return 1;
        }

        if (TEMPORAL_SVC_SUPPORTING_ENCODERS.contains(name)) {
            return 3;
        }
        return 1;
    }

    /**
     * Returns an array of SupportedProfileAdapter entries since the NDK
     * doesn't provide this functionality :/
     */
    @CalledByNative
    @RequiresApi(Build.VERSION_CODES.Q)
    private static SupportedProfileAdapter[] getSupportedEncoderProfiles() {
        MediaCodecInfo[] codecList;
        try {
            codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS).getCodecInfos();
        } catch (Throwable e) {
            // Swallow the exception due to bad Android implementation and pretend
            // MediaCodecList is not supported.
            Log.e(TAG, "Unable to retrieve MediaCodecInfo: ", e);
            return null;
        }

        ArrayList<SupportedProfileAdapter> hardwareProfiles =
                new ArrayList<SupportedProfileAdapter>();
        ArrayList<SupportedProfileAdapter> softwareProfiles =
                new ArrayList<SupportedProfileAdapter>();

        for (String type : SUPPORTED_ENCODER_TYPES) {
            for (MediaCodecInfo info : codecList) {
                if (info.isAlias()) continue; // Skip duplicates.
                if (!info.isEncoder()) continue;
                if (!info.isHardwareAccelerated() && requiresHardware(type)) continue;

                MediaCodecInfo.CodecCapabilities capabilities = null;
                try {
                    capabilities = info.getCapabilitiesForType(type);
                } catch (IllegalArgumentException e) {
                    // Type is not supported.
                    continue;
                }

                if (!hasSupportedColorFormat(capabilities.colorFormats)) continue;

                MediaCodecInfo.EncoderCapabilities encoderCapabilities =
                        capabilities.getEncoderCapabilities();
                boolean supportsCbr =
                        encoderCapabilities.isBitrateModeSupported(
                                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
                boolean supportsVbr =
                        encoderCapabilities.isBitrateModeSupported(
                                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);

                MediaCodecInfo.VideoCapabilities videoCapabilities =
                        capabilities.getVideoCapabilities();

                // In landscape mode, width is always larger than height, so first get the
                // maximum width and then the height range supported for that width.
                Range<Integer> supportedWidths = videoCapabilities.getSupportedWidths();
                Range<Integer> supportedHeights =
                        videoCapabilities.getSupportedHeightsFor(supportedWidths.getUpper());

                // Some devices don't have their max supported level configured correctly, so they
                // can return max resolutions like 7680x1714 which prevents both 4K and 8K content
                // from being hardware decoded.
                //
                // In cases where supported area is > 4k, but width, height are less than standard
                // and the standard resolution is supported, use the standard one instead so that at
                // least 4k support works. See https://crbug.com/41481822.
                if ((supportedWidths.getUpper() < 3840 || supportedHeights.getUpper() < 2160)
                        && supportedWidths.getUpper() * supportedHeights.getUpper() >= 3840 * 2160
                        && videoCapabilities.isSizeSupported(3840, 2160)) {
                    supportedWidths = new Range<Integer>(supportedWidths.getLower(), 3840);
                    supportedHeights = new Range<Integer>(supportedHeights.getLower(), 2160);
                }

                boolean needsPortraitEntry =
                        !supportedHeights.getUpper().equals(supportedWidths.getUpper())
                                && videoCapabilities.isSizeSupported(
                                        supportedHeights.getUpper(), supportedWidths.getUpper());

                // The frame rate entry in the supported profile is independent of the resolution
                // range, so we don't query based on the maximum resolution.
                Range<Integer> supportedFrameRates = videoCapabilities.getSupportedFrameRates();

                // Since the supported profiles interface doesn't support levels, we just attach
                // the same min/max to every profile.
                HashSet<Integer> supportedProfiles = new HashSet<Integer>();
                int codec = CodecProfileLevelList.getCodecFromMime(type);
                for (CodecProfileLevel cpl : capabilities.profileLevels) {
                    try {
                        supportedProfiles.add(
                                CodecProfileLevelList.mediaCodecProfileToChromiumMediaProfile(
                                        codec, cpl.profile));
                    } catch (RuntimeException e) {
                        // This means mediaCodecProfileToChromiumMediaProfile() needs updating.
                        Log.w(TAG, "Unknown profile: " + cpl.profile + " for codec " + type);
                        continue;
                    }
                }

                int maxNumberOfTemporalLayers =
                        getNumberOfTemporalLayers(info.getName().toLowerCase(Locale.getDefault()));
                ArrayList<SupportedProfileAdapter> profiles =
                        info.isHardwareAccelerated() ? hardwareProfiles : softwareProfiles;
                for (int mediaProfile : supportedProfiles) {
                    SupportedProfileAdapter profile = new SupportedProfileAdapter();

                    profile.profile = mediaProfile;
                    profile.minWidth = supportedWidths.getLower();
                    profile.minHeight = supportedHeights.getLower();
                    profile.maxWidth = supportedWidths.getUpper();
                    profile.maxHeight = supportedHeights.getUpper();
                    profile.maxFramerateNumerator = supportedFrameRates.getUpper();
                    profile.maxFramerateDenominator = 1;
                    profile.supportsCbr = supportsCbr;
                    profile.supportsVbr = supportsVbr;
                    profile.name = info.getName();
                    profile.isSoftwareCodec = info.isSoftwareOnly();
                    profile.maxNumberOfTemporalLayers = maxNumberOfTemporalLayers;
                    profiles.add(profile);

                    // Invert min/max height/width for a portrait mode entry if needed.
                    if (needsPortraitEntry) {
                        profile = new SupportedProfileAdapter();

                        profile.profile = mediaProfile;
                        profile.minWidth = supportedHeights.getLower();
                        profile.minHeight = supportedWidths.getLower();
                        profile.maxWidth = supportedHeights.getUpper();
                        profile.maxHeight = supportedWidths.getUpper();
                        profile.maxFramerateNumerator = supportedFrameRates.getUpper();
                        profile.maxFramerateDenominator = 1;
                        profile.supportsCbr = supportsCbr;
                        profile.supportsVbr = supportsVbr;
                        profile.name = info.getName();
                        profile.isSoftwareCodec = info.isSoftwareOnly();
                        profiles.add(profile);
                    }
                }
            }
        }

        // Insert all software codecs after the hardware support.
        ArrayList<SupportedProfileAdapter> profiles = hardwareProfiles;
        profiles.addAll(softwareProfiles);

        SupportedProfileAdapter[] profileArray = new SupportedProfileAdapter[profiles.size()];
        profiles.toArray(profileArray);
        return profileArray;
    }

    /**
     * Returns an array of SupportedProfileAdapter entries since the NDK
     * doesn't provide this functionality :/
     */
    @CalledByNative
    private static SupportedProfileAdapter[] getSupportedDecoderProfiles() {
        MediaCodecInfo[] codecList;
        try {
            codecList = new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos();
        } catch (Throwable e) {
            // Swallow the exception due to bad Android implementation and pretend
            // MediaCodecList is not supported.
            Log.e(TAG, "Unable to retrieve MediaCodecInfo: ", e);
            return null;
        }

        ArrayList<SupportedProfileAdapter> profiles = new ArrayList<SupportedProfileAdapter>();

        boolean isAtLeastQ = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
        for (String type : SUPPORTED_DECODER_TYPES) {
            for (MediaCodecInfo info : codecList) {
                // Skip duplicates. Harmless, but pollutes chrome://gpu
                if (isAtLeastQ && info.isAlias()) continue;
                if (info.isEncoder()) continue;
                // Skip low latency codec in case duplication.
                if (isLowLatency(info.getName())) continue;

                MediaCodecInfo.CodecCapabilities capabilities = null;
                try {
                    capabilities = info.getCapabilitiesForType(type);
                } catch (IllegalArgumentException e) {
                    // Type is not supported.
                    continue;
                }

                // Skip tunnel decoders because it's not supported by the media pipeline.
                if (capabilities.isFeatureRequired(
                        MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback)) {
                    continue;
                }

                MediaCodecInfo.VideoCapabilities videoCapabilities =
                        capabilities.getVideoCapabilities();

                // In landscape mode, width is always larger than height, so first get the
                // maximum width and then the height range supported for that width.
                Range<Integer> supportedWidths = videoCapabilities.getSupportedWidths();
                Range<Integer> supportedHeights =
                        videoCapabilities.getSupportedHeightsFor(supportedWidths.getUpper());

                // Some devices don't have their max supported level configured correctly, so they
                // can return max resolutions like 7680x1714 which prevents both 4K and 8K content
                // from being hardware decoded.
                //
                // In cases where supported area is > 4k, but width, height are less than standard
                // and the standard resolution is supported, use the standard one instead so that at
                // least 4k support works. See https://crbug.com/41481822.
                if ((supportedWidths.getUpper() < 3840 || supportedHeights.getUpper() < 2160)
                        && supportedWidths.getUpper() * supportedHeights.getUpper() >= 3840 * 2160
                        && videoCapabilities.isSizeSupported(3840, 2160)) {
                    supportedWidths = new Range<Integer>(supportedWidths.getLower(), 3840);
                    supportedHeights = new Range<Integer>(supportedHeights.getLower(), 2160);
                }

                boolean needsPortraitEntry =
                        !supportedHeights.getUpper().equals(supportedWidths.getUpper())
                                && videoCapabilities.isSizeSupported(
                                        supportedHeights.getUpper(), supportedWidths.getUpper());

                // See video_codecs.h
                final int kNoVideoCodecLevel = 0;

                // The map from supported profile to the highest supported level. We just attach
                // the same min/max to every profile, level pair.
                HashMap<Integer, Integer> supportedProfileLevels = new HashMap<>();
                int codec = CodecProfileLevelList.getCodecFromMime(type);
                for (CodecProfileLevel cpl : capabilities.profileLevels) {
                    try {
                        int profile =
                                CodecProfileLevelList.mediaCodecProfileToChromiumMediaProfile(
                                        codec, cpl.profile);

                        // Some devices don't provide valid level information, zero means
                        // no level.
                        int level = kNoVideoCodecLevel;
                        try {
                            level =
                                    CodecProfileLevelList.mediaCodecLevelToChromiumMediaLevel(
                                            codec, cpl.level);
                        } catch (RuntimeException e) {
                            // This may mean mediaCodecLevelToChromiumMediaLevel() needs updating,
                            // but may also just mean the device has invalid levels.
                            Log.w(
                                    TAG,
                                    "Unknown level: "
                                            + cpl.level
                                            + " for profile "
                                            + cpl.profile
                                            + " of codec "
                                            + type);
                        }

                        // We use kNoVideoCodecLevel -1 here so level == kNoVideoCodecLevel adds a
                        // supportedProfileLevels entry.
                        int supportedLevel =
                                supportedProfileLevels.getOrDefault(
                                        profile, kNoVideoCodecLevel - 1);
                        if (level > supportedLevel) {
                            supportedProfileLevels.put(profile, level);
                        }
                    } catch (RuntimeException e) {
                        // This means mediaCodecProfileToChromiumMediaProfile() needs updating.
                        Log.w(TAG, "Unknown profile: " + cpl.profile + " for codec " + type);
                        continue;
                    }
                }

                // Not all platforms seem to have a populated `profileLevels`, e.g., the
                // x86 emulator. In these cases, populate what's required by Android:
                // https://developer.android.com/guide/topics/media/media-formats
                //
                // The decoder selection will choose the decoder if the supported level is
                // larger than or equal to the requested level. So here we the 'no level'
                // sentinel value, if Android doesn't list required levels.
                if (supportedProfileLevels.isEmpty()) {
                    Log.d(
                            TAG,
                            "CodecCapabilities.profileLevels is missing for codec "
                                    + type
                                    + ". Assuming default support.");
                    switch (codec) {
                        case VideoCodec.VP8:
                            supportedProfileLevels.put(
                                    VideoCodecProfile.VP8PROFILE_ANY, kNoVideoCodecLevel);
                            break;
                        case VideoCodec.VP9:
                            supportedProfileLevels.put(
                                    VideoCodecProfile.VP9PROFILE_PROFILE0, kNoVideoCodecLevel);
                            break;
                        case VideoCodec.HEVC:
                            supportedProfileLevels.put(
                                    VideoCodecProfile.HEVCPROFILE_MAIN, kNoVideoCodecLevel);
                            break;
                        case VideoCodec.H264:
                            supportedProfileLevels.put(
                                    VideoCodecProfile.H264PROFILE_BASELINE, kNoVideoCodecLevel);
                            supportedProfileLevels.put(
                                    VideoCodecProfile.H264PROFILE_MAIN, kNoVideoCodecLevel);
                            break;
                        case VideoCodec.AV1:
                            supportedProfileLevels.put(
                                    VideoCodecProfile.AV1PROFILE_PROFILE_MAIN, kNoVideoCodecLevel);
                            break;
                    }
                }

                // Prior to Oreo, high profile support wasn't advertised properly.
                if (codec == VideoCodec.H264
                        && Build.VERSION.SDK_INT < Build.VERSION_CODES.O
                        && hasHighProfileSupport(info.getName())) {
                    supportedProfileLevels.put(
                            VideoCodecProfile.H264PROFILE_HIGH, kNoVideoCodecLevel);
                }

                boolean isSoftwareCodec = MediaCodecUtil.isSoftwareCodec(info);
                boolean supportsSecurePlayback =
                        capabilities.isFeatureSupported(
                                MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback);
                boolean requiresSecurePlayback =
                        capabilities.isFeatureRequired(
                                MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback);
                for (Map.Entry<Integer, Integer> profileLevel : supportedProfileLevels.entrySet()) {
                    SupportedProfileAdapter profile = new SupportedProfileAdapter();
                    profile.profile = profileLevel.getKey();
                    profile.level = profileLevel.getValue();
                    profile.minWidth = supportedWidths.getLower();
                    profile.minHeight = supportedHeights.getLower();
                    profile.maxWidth = supportedWidths.getUpper();
                    profile.maxHeight = supportedHeights.getUpper();
                    profile.name = info.getName();
                    profile.isSoftwareCodec = isSoftwareCodec;
                    profile.supportsSecurePlayback = supportsSecurePlayback;
                    profile.requiresSecurePlayback = requiresSecurePlayback;
                    profiles.add(profile);

                    Log.d(
                            TAG,
                            "Support: name="
                                    + info.getName()
                                    + ", profile="
                                    + profile.profile
                                    + ", level="
                                    + profile.level
                                    + ", min="
                                    + profile.minWidth
                                    + "x"
                                    + profile.minHeight
                                    + ", max="
                                    + profile.maxWidth
                                    + "x"
                                    + profile.maxHeight
                                    + ", is_sw="
                                    + profile.isSoftwareCodec
                                    + ", supports_secure="
                                    + profile.supportsSecurePlayback
                                    + ", requires_secure="
                                    + profile.requiresSecurePlayback);

                    // Invert min/max height/width for a portrait mode entry if needed.
                    if (needsPortraitEntry) {
                        profile = new SupportedProfileAdapter();

                        profile.profile = profileLevel.getKey();
                        profile.level = profileLevel.getValue();
                        profile.minWidth = supportedHeights.getLower();
                        profile.minHeight = supportedWidths.getLower();
                        profile.maxWidth = supportedHeights.getUpper();
                        profile.maxHeight = supportedWidths.getUpper();
                        profile.name = info.getName();
                        profile.isSoftwareCodec = isSoftwareCodec;
                        profile.supportsSecurePlayback = supportsSecurePlayback;
                        profile.requiresSecurePlayback = requiresSecurePlayback;
                        profiles.add(profile);
                    }
                }
            }
        }

        SupportedProfileAdapter[] profileArray = new SupportedProfileAdapter[profiles.size()];
        profiles.toArray(profileArray);
        return profileArray;
    }
}