• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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