/* * Copyright (C) 2021 pedroSG94. * * 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 com.pedro.encoder.utils; import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.os.Build; import androidx.annotation.RequiresApi; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Created by pedro on 14/02/18. */ public class CodecUtil { private static final String TAG = "CodecUtil"; public static final String H264_MIME = "video/avc"; public static final String H265_MIME = "video/hevc"; public static final String AAC_MIME = "audio/mp4a-latm"; public static final String VORBIS_MIME = "audio/ogg"; public static final String OPUS_MIME = "audio/opus"; public enum Force { FIRST_COMPATIBLE_FOUND, SOFTWARE, HARDWARE } public static List showAllCodecsInfo() { List mediaCodecInfoList = getAllCodecs(false); List infos = new ArrayList<>(); for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) { StringBuilder info = new StringBuilder("----------------\n"); info.append("Name: ") .append(mediaCodecInfo.getName()) .append("\n"); for (String type : mediaCodecInfo.getSupportedTypes()) { info.append("Type: ") .append(type) .append("\n"); MediaCodecInfo.CodecCapabilities codecCapabilities = mediaCodecInfo.getCapabilitiesForType(type); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { info.append("Max instances: ") .append(codecCapabilities.getMaxSupportedInstances()) .append("\n"); } if (mediaCodecInfo.isEncoder()) { info.append("----- Encoder info -----\n"); MediaCodecInfo.EncoderCapabilities encoderCapabilities = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { encoderCapabilities = codecCapabilities.getEncoderCapabilities(); info.append("Complexity range: ") .append(encoderCapabilities.getComplexityRange().getLower()) .append(" - ") .append(encoderCapabilities.getComplexityRange().getUpper()) .append("\n"); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { info.append("Quality range: ") .append(encoderCapabilities.getQualityRange().getLower()) .append(" - ") .append(encoderCapabilities.getQualityRange().getUpper()) .append("\n"); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { info.append("CBR supported: ") .append(encoderCapabilities.isBitrateModeSupported(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR)) .append("\n") .append("VBR supported: ") .append(encoderCapabilities.isBitrateModeSupported(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR)) .append("\n") .append("CQ supported: ") .append(encoderCapabilities.isBitrateModeSupported(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) .append("\n"); } info.append("----- -----\n"); } else { info.append("----- Decoder info -----\n") .append("----- -----\n"); } if (codecCapabilities.colorFormats != null && codecCapabilities.colorFormats.length > 0) { info.append("----- Video info -----\n") .append("Supported colors: \n"); for (int color : codecCapabilities.colorFormats) info.append(color) .append("\n"); for (MediaCodecInfo.CodecProfileLevel profile : codecCapabilities.profileLevels) info.append("Profile: ") .append(profile.profile) .append(", level: ") .append(profile.level) .append("\n"); MediaCodecInfo.VideoCapabilities videoCapabilities = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { videoCapabilities = codecCapabilities.getVideoCapabilities(); info.append("Bitrate range: ") .append(videoCapabilities.getBitrateRange().getLower()) .append(" - ") .append(videoCapabilities.getBitrateRange().getUpper()) .append("\n") .append("Frame rate range: ") .append(videoCapabilities.getSupportedFrameRates().getLower()) .append(" - ") .append(videoCapabilities.getSupportedFrameRates().getUpper()) .append("\n") .append("Width range: ") .append(videoCapabilities.getSupportedWidths().getLower()) .append(" - ") .append(videoCapabilities.getSupportedWidths().getUpper()) .append("\n") .append("Height range: ") .append(videoCapabilities.getSupportedHeights().getLower()) .append(" - ") .append(videoCapabilities.getSupportedHeights().getUpper()) .append("\n"); } info.append("----- -----\n"); } else { info.append("----- Audio info -----\n"); for (MediaCodecInfo.CodecProfileLevel profile : codecCapabilities.profileLevels) info.append("Profile: ") .append(profile.profile) .append(", level: ") .append(profile.level) .append("\n"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { MediaCodecInfo.AudioCapabilities audioCapabilities = codecCapabilities.getAudioCapabilities(); info.append("Bitrate range: ") .append(audioCapabilities.getBitrateRange().getLower()) .append(" - ") .append(audioCapabilities.getBitrateRange().getUpper()) .append("\n") .append("Channels supported: ") .append(audioCapabilities.getMaxInputChannelCount()) .append("\n"); try { if (audioCapabilities.getSupportedSampleRates() != null && audioCapabilities.getSupportedSampleRates().length > 0) { info.append("Supported sample rate: \n"); for (int sr : audioCapabilities.getSupportedSampleRates()) info.append(sr) .append("\n"); } } catch (Exception ignored) { } } info.append("----- -----\n"); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { info.append("Max instances: ") .append(codecCapabilities.getMaxSupportedInstances()) .append("\n"); } } info.append("----------------\n"); infos.add(info.toString()); } return infos; } public static List getAllCodecs(boolean filterBroken) { List mediaCodecInfoList = new ArrayList<>(); if (Build.VERSION.SDK_INT >= 21) { MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS); MediaCodecInfo[] mediaCodecInfos = mediaCodecList.getCodecInfos(); mediaCodecInfoList.addAll(Arrays.asList(mediaCodecInfos)); } else { int count = MediaCodecList.getCodecCount(); for (int i = 0; i < count; i++) { MediaCodecInfo mci = MediaCodecList.getCodecInfoAt(i); mediaCodecInfoList.add(mci); } } return filterBroken ? filterBrokenCodecs(mediaCodecInfoList) : mediaCodecInfoList; } public static List getAllHardwareEncoders(String mime, boolean cbrPriority) { List mediaCodecInfoList = getAllEncoders(mime); List mediaCodecInfoHardware = new ArrayList<>(); List mediaCodecInfoHardwareCBR = new ArrayList<>(); for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) { if (isHardwareAccelerated(mediaCodecInfo)) { mediaCodecInfoHardware.add(mediaCodecInfo); if (cbrPriority &&Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isCBRModeSupported(mediaCodecInfo, mime)) { mediaCodecInfoHardwareCBR.add(mediaCodecInfo); } } } mediaCodecInfoHardware.removeAll(mediaCodecInfoHardwareCBR); mediaCodecInfoHardware.addAll(0, mediaCodecInfoHardwareCBR); return mediaCodecInfoHardware; } public static List getAllHardwareEncoders(String mime) { return getAllHardwareEncoders(mime, false); } public static List getAllHardwareDecoders(String mime) { List mediaCodecInfoList = getAllDecoders(mime); List mediaCodecInfoHardware = new ArrayList<>(); for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) { if (isHardwareAccelerated(mediaCodecInfo)) { mediaCodecInfoHardware.add(mediaCodecInfo); } } return mediaCodecInfoHardware; } public static List getAllSoftwareEncoders(String mime, boolean cbrPriority) { List mediaCodecInfoList = getAllEncoders(mime); List mediaCodecInfoSoftware = new ArrayList<>(); List mediaCodecInfoSoftwareCBR = new ArrayList<>(); for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) { if (isSoftwareOnly(mediaCodecInfo)) { mediaCodecInfoSoftware.add(mediaCodecInfo); if (cbrPriority &&Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isCBRModeSupported(mediaCodecInfo, mime)) { mediaCodecInfoSoftwareCBR.add(mediaCodecInfo); } } } mediaCodecInfoSoftware.removeAll(mediaCodecInfoSoftwareCBR); mediaCodecInfoSoftware.addAll(0, mediaCodecInfoSoftwareCBR); return mediaCodecInfoSoftware; } public static List getAllSoftwareEncoders(String mime) { return getAllSoftwareEncoders(mime, false); } public static List getAllSoftwareDecoders(String mime) { List mediaCodecInfoList = getAllDecoders(mime); List mediaCodecInfoSoftware = new ArrayList<>(); for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) { if (isSoftwareOnly(mediaCodecInfo)) { mediaCodecInfoSoftware.add(mediaCodecInfo); } } return mediaCodecInfoSoftware; } /** * choose encoder by mime. */ public static List getAllEncoders(String mime) { List mediaCodecInfoList = new ArrayList<>(); List mediaCodecInfos = getAllCodecs(true); for (MediaCodecInfo mci : mediaCodecInfos) { if (!mci.isEncoder()) { continue; } String[] types = mci.getSupportedTypes(); for (String type : types) { if (type.equalsIgnoreCase(mime)) { mediaCodecInfoList.add(mci); } } } return mediaCodecInfoList; } public static List getAllEncoders(String mime, boolean hardwarePriority, boolean cbrPriority) { List mediaCodecInfoList = new ArrayList<>(); if (hardwarePriority) { mediaCodecInfoList.addAll(getAllHardwareEncoders(mime, cbrPriority)); mediaCodecInfoList.addAll(getAllSoftwareEncoders(mime, cbrPriority)); } else { mediaCodecInfoList.addAll(getAllEncoders(mime)); } return mediaCodecInfoList; } public static List getAllEncoders(String mime, boolean hardwarePriority) { return getAllEncoders(mime, hardwarePriority, false); } /** * choose decoder by mime. */ public static List getAllDecoders(String mime) { List mediaCodecInfoList = new ArrayList<>(); List mediaCodecInfos = getAllCodecs(true); for (MediaCodecInfo mci : mediaCodecInfos) { if (mci.isEncoder()) { continue; } String[] types = mci.getSupportedTypes(); for (String type : types) { if (type.equalsIgnoreCase(mime)) { mediaCodecInfoList.add(mci); } } } return mediaCodecInfoList; } public static List getAllDecoders(String mime, boolean hardwarePriority) { List mediaCodecInfoList = new ArrayList<>(); if (hardwarePriority) { mediaCodecInfoList.addAll(getAllHardwareDecoders(mime)); mediaCodecInfoList.addAll(getAllSoftwareDecoders(mime)); } else { mediaCodecInfoList.addAll(getAllDecoders(mime)); } return mediaCodecInfoList; } /* Adapted from google/ExoPlayer * https://github.com/google/ExoPlayer/commit/48555550d7fcf6953f2382466818c74092b26355 */ private static boolean isHardwareAccelerated(MediaCodecInfo codecInfo) { if (Build.VERSION.SDK_INT >= 29) { return codecInfo.isHardwareAccelerated(); } // codecInfo.isHardwareAccelerated() != codecInfo.isSoftwareOnly() is not necessarily true. // However, we assume this to be true as an approximation. return !isSoftwareOnly(codecInfo); } /* Adapted from google/ExoPlayer * https://github.com/google/ExoPlayer/commit/48555550d7fcf6953f2382466818c74092b26355 */ private static boolean isSoftwareOnly(MediaCodecInfo mediaCodecInfo) { if (Build.VERSION.SDK_INT >= 29) { //mediaCodecInfo.isSoftwareOnly() is not working on emulators. //Use !mediaCodecInfo.isHardwareAccelerated() to make sure that all codecs are classified as software or hardware return !mediaCodecInfo.isHardwareAccelerated(); } String name = mediaCodecInfo.getName().toLowerCase(); if (name.startsWith("arc.")) { // App Runtime for Chrome (ARC) codecs return false; } return name.startsWith("omx.google.") || name.startsWith("omx.ffmpeg.") || (name.startsWith("omx.sec.") && name.contains(".sw.")) || name.equals("omx.qcom.video.decoder.hevcswvdec") || name.startsWith("c2.android.") || name.startsWith("c2.google.") || (!name.startsWith("omx.") && !name.startsWith("c2.")); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public static boolean isCBRModeSupported(MediaCodecInfo mediaCodecInfo, String mime) { MediaCodecInfo.CodecCapabilities codecCapabilities = mediaCodecInfo.getCapabilitiesForType(mime); MediaCodecInfo.EncoderCapabilities encoderCapabilities = codecCapabilities.getEncoderCapabilities(); return encoderCapabilities.isBitrateModeSupported( MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR); } /** * Filter broken codecs by name and device model. * * Note: * There is no way to know broken encoders so we will check by name and device. * Please add your encoder to this method if you detect one. * * @param codecs All device codecs * @return a list without broken codecs */ private static List filterBrokenCodecs(List codecs) { List listFilter = new ArrayList<>(); List listLowPriority = new ArrayList<>(); List listUltraLowPriority = new ArrayList<>(); for (MediaCodecInfo mediaCodecInfo : codecs) { if (isValid(mediaCodecInfo.getName())) { CodecPriority priority = checkCodecPriority(mediaCodecInfo.getName()); switch (priority) { case ULTRA_LOW: listUltraLowPriority.add(mediaCodecInfo); break; case LOW: listLowPriority.add(mediaCodecInfo); break; case NORMAL: default: listFilter.add(mediaCodecInfo); break; } } } listFilter.addAll(listLowPriority); listFilter.addAll(listUltraLowPriority); return listFilter; } /** * For now, none broken codec reported. */ private static boolean isValid(String name) { //This encoder is invalid and produce errors (Only found in AVD API 16) if (name.equalsIgnoreCase("aacencoder")) return false; return true; } private enum CodecPriority { NORMAL, LOW, ULTRA_LOW } /** * Few devices have codecs that is not working properly in few cases like using AWS MediaLive or YouTube * but it is still usable in most of cases. * @return priority level. */ private static CodecPriority checkCodecPriority(String name) { //maybe only broke on samsung with Android 12+ using YouTube and AWS MediaLive // but set as ultra low priority in all cases. if (name.equalsIgnoreCase("c2.sec.aac.encoder")) return CodecPriority.ULTRA_LOW; //broke on few devices using YouTube and AWS MediaLive else if (name.equalsIgnoreCase("omx.google.aac.encoder")) return CodecPriority.LOW; else return CodecPriority.NORMAL; } }