1 /* 2 * Copyright 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.android.exoplayer2.transformer; 18 19 import static com.google.android.exoplayer2.util.Assertions.checkNotNull; 20 import static java.lang.Math.max; 21 import static java.lang.Math.round; 22 23 import android.media.MediaCodec; 24 import android.media.MediaCodecInfo; 25 import android.media.MediaCodecList; 26 import android.media.MediaFormat; 27 import android.util.Pair; 28 import android.util.Range; 29 import android.util.Size; 30 import androidx.annotation.DoNotInline; 31 import androidx.annotation.Nullable; 32 import androidx.annotation.RequiresApi; 33 import com.google.android.exoplayer2.Format; 34 import com.google.android.exoplayer2.util.MediaFormatUtil; 35 import com.google.android.exoplayer2.util.MimeTypes; 36 import com.google.android.exoplayer2.util.Util; 37 import com.google.common.base.Ascii; 38 import com.google.common.base.Supplier; 39 import com.google.common.base.Suppliers; 40 import com.google.common.collect.ImmutableList; 41 import com.google.common.collect.ImmutableListMultimap; 42 import com.google.common.collect.ImmutableSet; 43 import com.google.common.primitives.Ints; 44 45 /** Utility methods for {@link MediaCodec} encoders. */ 46 public final class EncoderUtil { 47 48 /** A value to indicate the encoding level is not set. */ 49 public static final int LEVEL_UNSET = Format.NO_VALUE; 50 51 private static final Supplier<ImmutableListMultimap<String, MediaCodecInfo>> 52 MIME_TYPE_TO_ENCODERS = Suppliers.memoize(EncoderUtil::populateEncoderInfos); 53 54 /** 55 * Returns a list of {@linkplain MediaCodecInfo encoders} that support the given {@code mimeType}, 56 * or an empty list if there is none. 57 */ getSupportedEncoders(String mimeType)58 public static ImmutableList<MediaCodecInfo> getSupportedEncoders(String mimeType) { 59 return checkNotNull(MIME_TYPE_TO_ENCODERS.get()).get(Ascii.toLowerCase(mimeType)); 60 } 61 62 /** Returns a list of video {@linkplain MimeTypes MIME types} that can be encoded. */ getSupportedVideoMimeTypes()63 public static ImmutableSet<String> getSupportedVideoMimeTypes() { 64 return checkNotNull(MIME_TYPE_TO_ENCODERS.get()).keySet(); 65 } 66 67 /** 68 * Returns a {@link Range} of supported heights for the given {@link MediaCodecInfo encoder}, 69 * {@linkplain MimeTypes MIME type} and {@code width}. 70 * 71 * @throws IllegalArgumentException When the width is not in the range of {@linkplain 72 * #getSupportedResolutionRanges supported widths}. 73 */ getSupportedHeights( MediaCodecInfo encoderInfo, String mimeType, int width)74 public static Range<Integer> getSupportedHeights( 75 MediaCodecInfo encoderInfo, String mimeType, int width) { 76 return encoderInfo 77 .getCapabilitiesForType(mimeType) 78 .getVideoCapabilities() 79 .getSupportedHeightsFor(width); 80 } 81 82 /** 83 * Returns a {@link Pair} of supported width and height {@link Range ranges} for the given {@link 84 * MediaCodecInfo encoder} and {@linkplain MimeTypes MIME type}. 85 */ getSupportedResolutionRanges( MediaCodecInfo encoderInfo, String mimeType)86 public static Pair<Range<Integer>, Range<Integer>> getSupportedResolutionRanges( 87 MediaCodecInfo encoderInfo, String mimeType) { 88 MediaCodecInfo.VideoCapabilities videoCapabilities = 89 encoderInfo.getCapabilitiesForType(mimeType).getVideoCapabilities(); 90 return Pair.create( 91 videoCapabilities.getSupportedWidths(), videoCapabilities.getSupportedHeights()); 92 } 93 94 /** 95 * Finds an {@linkplain MediaCodecInfo encoder}'s supported resolution from a given resolution. 96 * 97 * <p>The input resolution is returned, if it (after aligning to the encoder's requirement) is 98 * supported by the {@linkplain MediaCodecInfo encoder}. 99 * 100 * <p>The resolution will be adjusted to be within the {@linkplain MediaCodecInfo encoder}'s range 101 * of supported resolutions, and will be aligned to the {@linkplain MediaCodecInfo encoder}'s 102 * alignment requirement. The adjustment process takes into account the original aspect ratio. But 103 * the fixed resolution may not preserve the original aspect ratio, depending on the encoder's 104 * required size alignment. 105 * 106 * @param encoderInfo The {@link MediaCodecInfo} of the encoder. 107 * @param mimeType The output MIME type. 108 * @param width The original width. 109 * @param height The original height. 110 * @return A {@linkplain Size supported resolution}, or {@code null} if unable to find a fallback. 111 */ 112 @Nullable getSupportedResolution( MediaCodecInfo encoderInfo, String mimeType, int width, int height)113 public static Size getSupportedResolution( 114 MediaCodecInfo encoderInfo, String mimeType, int width, int height) { 115 MediaCodecInfo.VideoCapabilities videoEncoderCapabilities = 116 encoderInfo.getCapabilitiesForType(mimeType).getVideoCapabilities(); 117 int widthAlignment = videoEncoderCapabilities.getWidthAlignment(); 118 int heightAlignment = videoEncoderCapabilities.getHeightAlignment(); 119 120 // Fix size alignment. 121 width = alignResolution(width, widthAlignment); 122 height = alignResolution(height, heightAlignment); 123 if (videoEncoderCapabilities.isSizeSupported(width, height)) { 124 return new Size(width, height); 125 } 126 127 // Try three-fourths (e.g. 1440 -> 1080). 128 int newWidth = alignResolution(width * 3 / 4, widthAlignment); 129 int newHeight = alignResolution(height * 3 / 4, heightAlignment); 130 if (videoEncoderCapabilities.isSizeSupported(newWidth, newHeight)) { 131 return new Size(newWidth, newHeight); 132 } 133 134 // Try two-thirds (e.g. 4k -> 1440). 135 newWidth = alignResolution(width * 2 / 3, widthAlignment); 136 newHeight = alignResolution(height * 2 / 3, heightAlignment); 137 if (videoEncoderCapabilities.isSizeSupported(newWidth, newHeight)) { 138 return new Size(newWidth, newHeight); 139 } 140 141 // Try half (e.g. 4k -> 1080). 142 newWidth = alignResolution(width / 2, widthAlignment); 143 newHeight = alignResolution(height / 2, heightAlignment); 144 if (videoEncoderCapabilities.isSizeSupported(newWidth, newHeight)) { 145 return new Size(newWidth, newHeight); 146 } 147 148 // Try one-third (e.g. 4k -> 720). 149 newWidth = alignResolution(width / 3, widthAlignment); 150 newHeight = alignResolution(height / 3, heightAlignment); 151 if (videoEncoderCapabilities.isSizeSupported(newWidth, newHeight)) { 152 return new Size(newWidth, newHeight); 153 } 154 155 // Fix frame being too wide or too tall. 156 width = videoEncoderCapabilities.getSupportedWidths().clamp(width); 157 int adjustedHeight = videoEncoderCapabilities.getSupportedHeightsFor(width).clamp(height); 158 if (adjustedHeight != height) { 159 width = 160 alignResolution((int) round((double) width * adjustedHeight / height), widthAlignment); 161 height = alignResolution(adjustedHeight, heightAlignment); 162 } 163 164 return videoEncoderCapabilities.isSizeSupported(width, height) ? new Size(width, height) : null; 165 } 166 167 /** 168 * Returns a {@link ImmutableSet set} of supported {@linkplain MediaCodecInfo.CodecProfileLevel 169 * encoding profiles} for the given {@linkplain MediaCodecInfo encoder} and {@linkplain MimeTypes 170 * MIME type}. 171 */ findSupportedEncodingProfiles( MediaCodecInfo encoderInfo, String mimeType)172 public static ImmutableSet<Integer> findSupportedEncodingProfiles( 173 MediaCodecInfo encoderInfo, String mimeType) { 174 MediaCodecInfo.CodecProfileLevel[] profileLevels = 175 encoderInfo.getCapabilitiesForType(mimeType).profileLevels; 176 ImmutableSet.Builder<Integer> supportedProfilesBuilder = new ImmutableSet.Builder<>(); 177 for (MediaCodecInfo.CodecProfileLevel profileLevel : profileLevels) { 178 supportedProfilesBuilder.add(profileLevel.profile); 179 } 180 return supportedProfilesBuilder.build(); 181 } 182 183 /** 184 * Finds the highest supported encoding level given a profile. 185 * 186 * @param encoderInfo The {@link MediaCodecInfo encoderInfo}. 187 * @param mimeType The {@linkplain MimeTypes MIME type}. 188 * @param profile The encoding profile. 189 * @return The highest supported encoding level, as documented in {@link 190 * MediaCodecInfo.CodecProfileLevel}, or {@link #LEVEL_UNSET} if the profile is not supported. 191 */ findHighestSupportedEncodingLevel( MediaCodecInfo encoderInfo, String mimeType, int profile)192 public static int findHighestSupportedEncodingLevel( 193 MediaCodecInfo encoderInfo, String mimeType, int profile) { 194 // TODO(b/214964116): Merge into MediaCodecUtil. 195 MediaCodecInfo.CodecProfileLevel[] profileLevels = 196 encoderInfo.getCapabilitiesForType(mimeType).profileLevels; 197 198 int maxSupportedLevel = LEVEL_UNSET; 199 for (MediaCodecInfo.CodecProfileLevel profileLevel : profileLevels) { 200 if (profileLevel.profile == profile) { 201 maxSupportedLevel = max(maxSupportedLevel, profileLevel.level); 202 } 203 } 204 return maxSupportedLevel; 205 } 206 207 /** 208 * Finds a {@link MediaCodec} that supports the {@link MediaFormat}, or {@code null} if none is 209 * found. 210 */ 211 @Nullable findCodecForFormat(MediaFormat format, boolean isDecoder)212 public static String findCodecForFormat(MediaFormat format, boolean isDecoder) { 213 MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS); 214 // Format must not include KEY_FRAME_RATE on API21. 215 // https://developer.android.com/reference/android/media/MediaCodecList#findDecoderForFormat(android.media.MediaFormat) 216 @Nullable String frameRate = null; 217 if (Util.SDK_INT == 21 && format.containsKey(MediaFormat.KEY_FRAME_RATE)) { 218 frameRate = format.getString(MediaFormat.KEY_FRAME_RATE); 219 format.setString(MediaFormat.KEY_FRAME_RATE, null); 220 } 221 222 String mediaCodecName = 223 isDecoder 224 ? mediaCodecList.findDecoderForFormat(format) 225 : mediaCodecList.findEncoderForFormat(format); 226 227 if (Util.SDK_INT == 21) { 228 MediaFormatUtil.maybeSetString(format, MediaFormat.KEY_FRAME_RATE, frameRate); 229 } 230 return mediaCodecName; 231 } 232 233 /** Returns the range of supported bitrates for the given {@linkplain MimeTypes MIME type}. */ getSupportedBitrateRange( MediaCodecInfo encoderInfo, String mimeType)234 public static Range<Integer> getSupportedBitrateRange( 235 MediaCodecInfo encoderInfo, String mimeType) { 236 return encoderInfo.getCapabilitiesForType(mimeType).getVideoCapabilities().getBitrateRange(); 237 } 238 239 /** Returns whether the bitrate mode is supported by the encoder. */ isBitrateModeSupported( MediaCodecInfo encoderInfo, String mimeType, int bitrateMode)240 public static boolean isBitrateModeSupported( 241 MediaCodecInfo encoderInfo, String mimeType, int bitrateMode) { 242 return encoderInfo 243 .getCapabilitiesForType(mimeType) 244 .getEncoderCapabilities() 245 .isBitrateModeSupported(bitrateMode); 246 } 247 248 /** 249 * Returns a {@link ImmutableList list} of supported {@linkplain 250 * MediaCodecInfo.CodecCapabilities#colorFormats color formats} for the given {@linkplain 251 * MediaCodecInfo encoder} and {@linkplain MimeTypes MIME type}. 252 */ getSupportedColorFormats( MediaCodecInfo encoderInfo, String mimeType)253 public static ImmutableList<Integer> getSupportedColorFormats( 254 MediaCodecInfo encoderInfo, String mimeType) { 255 return ImmutableList.copyOf( 256 Ints.asList(encoderInfo.getCapabilitiesForType(mimeType).colorFormats)); 257 } 258 259 /** Checks if a {@linkplain MediaCodecInfo codec} is hardware-accelerated. */ isHardwareAccelerated(MediaCodecInfo encoderInfo, String mimeType)260 public static boolean isHardwareAccelerated(MediaCodecInfo encoderInfo, String mimeType) { 261 // TODO(b/214964116): Merge into MediaCodecUtil. 262 if (Util.SDK_INT >= 29) { 263 return Api29.isHardwareAccelerated(encoderInfo); 264 } 265 // codecInfo.isHardwareAccelerated() == !codecInfo.isSoftwareOnly() is not necessarily true. 266 // However, we assume this to be true as an approximation. 267 return !isSoftwareOnly(encoderInfo, mimeType); 268 } 269 270 /** Returns whether a given feature is supported. */ isFeatureSupported( MediaCodecInfo encoderInfo, String mimeType, String featureName)271 public static boolean isFeatureSupported( 272 MediaCodecInfo encoderInfo, String mimeType, String featureName) { 273 return encoderInfo.getCapabilitiesForType(mimeType).isFeatureSupported(featureName); 274 } 275 276 /** Returns the number of max number of the supported concurrent codec instances. */ 277 @RequiresApi(23) getMaxSupportedInstances(MediaCodecInfo encoderInfo, String mimeType)278 public static int getMaxSupportedInstances(MediaCodecInfo encoderInfo, String mimeType) { 279 return encoderInfo.getCapabilitiesForType(mimeType).getMaxSupportedInstances(); 280 } 281 isSoftwareOnly(MediaCodecInfo encoderInfo, String mimeType)282 private static boolean isSoftwareOnly(MediaCodecInfo encoderInfo, String mimeType) { 283 if (Util.SDK_INT >= 29) { 284 return Api29.isSoftwareOnly(encoderInfo); 285 } 286 287 if (MimeTypes.isAudio(mimeType)) { 288 // Assume audio decoders are software only. 289 return true; 290 } 291 String codecName = Ascii.toLowerCase(encoderInfo.getName()); 292 if (codecName.startsWith("arc.")) { 293 // App Runtime for Chrome (ARC) codecs 294 return false; 295 } 296 297 // Estimate whether a codec is software-only, to emulate isSoftwareOnly on API < 29. 298 return codecName.startsWith("omx.google.") 299 || codecName.startsWith("omx.ffmpeg.") 300 || (codecName.startsWith("omx.sec.") && codecName.contains(".sw.")) 301 || codecName.equals("omx.qcom.video.decoder.hevcswvdec") 302 || codecName.startsWith("c2.android.") 303 || codecName.startsWith("c2.google.") 304 || (!codecName.startsWith("omx.") && !codecName.startsWith("c2.")); 305 } 306 307 /** 308 * Align to the closest resolution that respects the encoder's supported alignment. 309 * 310 * <p>For example, size 35 will be aligned to 32 if the alignment is 16, and size 45 will be 311 * aligned to 48. 312 */ alignResolution(int size, int alignment)313 private static int alignResolution(int size, int alignment) { 314 // Aligning to resolutions that are multiples of 10, like from 1081 to 1080, assuming alignment 315 // is 2 in most encoders. 316 boolean shouldRoundDown = false; 317 if (size % 10 == 1) { 318 shouldRoundDown = true; 319 } 320 return shouldRoundDown 321 ? (int) (alignment * Math.floor((float) size / alignment)) 322 : alignment * Math.round((float) size / alignment); 323 } 324 populateEncoderInfos()325 private static ImmutableListMultimap<String, MediaCodecInfo> populateEncoderInfos() { 326 ImmutableListMultimap.Builder<String, MediaCodecInfo> encoderInfosBuilder = 327 new ImmutableListMultimap.Builder<>(); 328 329 MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS); 330 MediaCodecInfo[] allCodecInfos = mediaCodecList.getCodecInfos(); 331 332 for (MediaCodecInfo mediaCodecInfo : allCodecInfos) { 333 if (!mediaCodecInfo.isEncoder()) { 334 continue; 335 } 336 String[] supportedMimeTypes = mediaCodecInfo.getSupportedTypes(); 337 for (String mimeType : supportedMimeTypes) { 338 if (MimeTypes.isVideo(mimeType)) { 339 encoderInfosBuilder.put(Ascii.toLowerCase(mimeType), mediaCodecInfo); 340 } 341 } 342 } 343 return encoderInfosBuilder.build(); 344 } 345 346 @RequiresApi(29) 347 private static final class Api29 { 348 @DoNotInline isHardwareAccelerated(MediaCodecInfo encoderInfo)349 public static boolean isHardwareAccelerated(MediaCodecInfo encoderInfo) { 350 return encoderInfo.isHardwareAccelerated(); 351 } 352 353 @DoNotInline isSoftwareOnly(MediaCodecInfo encoderInfo)354 public static boolean isSoftwareOnly(MediaCodecInfo encoderInfo) { 355 return encoderInfo.isSoftwareOnly(); 356 } 357 } 358 EncoderUtil()359 private EncoderUtil() {} 360 } 361