EncoderUtil.java
/*
* Copyright 2022 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 androidx.media3.transformer;
import static java.lang.Math.round;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
/** Utility methods for {@link MediaCodec} encoders. */
@UnstableApi
public final class EncoderUtil {
private static final List<MediaCodecInfo> encoders = new ArrayList<>();
/**
* Returns a list of {@link MediaCodecInfo encoders} that support the given {@code mimeType}, or
* an empty list if there is none.
*/
public static ImmutableList<MediaCodecInfo> getSupportedEncoders(String mimeType) {
maybePopulateEncoderInfos();
ImmutableList.Builder<MediaCodecInfo> availableEncoders = new ImmutableList.Builder<>();
for (int i = 0; i < encoders.size(); i++) {
MediaCodecInfo encoderInfo = encoders.get(i);
String[] supportedMimeTypes = encoderInfo.getSupportedTypes();
for (String supportedMimeType : supportedMimeTypes) {
if (Ascii.equalsIgnoreCase(supportedMimeType, mimeType)) {
availableEncoders.add(encoderInfo);
}
}
}
return availableEncoders.build();
}
/**
* Finds the {@link MediaCodecInfo encoder}'s closest supported resolution from the given
* resolution.
*
* <p>The input resolution is returned, if it is supported by the {@link MediaCodecInfo encoder}.
*
* <p>The resolution will be clamped to the {@link MediaCodecInfo encoder}'s range of supported
* resolutions, and adjusted to the {@link MediaCodecInfo encoder}'s size alignment. The
* adjustment process takes into account the original aspect ratio. But the fixed resolution may
* not preserve the original aspect ratio, depending on the encoder's required size alignment.
*
* @param encoderInfo The {@link MediaCodecInfo} of the encoder.
* @param mimeType The output MIME type.
* @param width The original width.
* @param height The original height.
* @return A {@link Pair} of width and height, or {@code null} if unable to find a fix.
*/
@Nullable
public static Pair<Integer, Integer> getClosestSupportedResolution(
MediaCodecInfo encoderInfo, String mimeType, int width, int height) {
MediaCodecInfo.VideoCapabilities videoEncoderCapabilities =
encoderInfo.getCapabilitiesForType(mimeType).getVideoCapabilities();
if (videoEncoderCapabilities.isSizeSupported(width, height)) {
return Pair.create(width, height);
}
// Fix frame being too wide or too tall.
int adjustedHeight = videoEncoderCapabilities.getSupportedHeights().clamp(height);
if (adjustedHeight != height) {
width = (int) round((double) width * adjustedHeight / height);
height = adjustedHeight;
}
int adjustedWidth = videoEncoderCapabilities.getSupportedWidths().clamp(width);
if (adjustedWidth != width) {
height = (int) round((double) height * adjustedWidth / width);
width = adjustedWidth;
}
// Fix pixel alignment.
width = alignResolution(width, videoEncoderCapabilities.getWidthAlignment());
height = alignResolution(height, videoEncoderCapabilities.getHeightAlignment());
return videoEncoderCapabilities.isSizeSupported(width, height)
? Pair.create(width, height)
: null;
}
/** Returns whether the {@link MediaCodecInfo encoder} supports the given profile and level. */
public static boolean isProfileLevelSupported(
MediaCodecInfo encoderInfo, String mimeType, int profile, int level) {
MediaCodecInfo.CodecProfileLevel[] profileLevels =
encoderInfo.getCapabilitiesForType(mimeType).profileLevels;
for (MediaCodecInfo.CodecProfileLevel profileLevel : profileLevels) {
if (profileLevel.profile == profile && profileLevel.level == level) {
return true;
}
}
return false;
}
/**
* Finds the {@link MediaCodecInfo encoder}'s closest supported bitrate from the given bitrate.
*/
public static int getClosestSupportedBitrate(
MediaCodecInfo encoderInfo, String mimeType, int bitrate) {
return encoderInfo
.getCapabilitiesForType(mimeType)
.getVideoCapabilities()
.getBitrateRange()
.clamp(bitrate);
}
/**
* Align to the closest resolution that respects the encoder's supported alignment.
*
* <p>For example, size 35 will be aligned to 32 if the alignment is 16, and size 45 will be
* aligned to 48.
*/
private static int alignResolution(int size, int alignment) {
return alignment * Math.round((float) size / alignment);
}
private static synchronized void maybePopulateEncoderInfos() {
if (encoders.isEmpty()) {
MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
MediaCodecInfo[] allCodecInfos = mediaCodecList.getCodecInfos();
for (MediaCodecInfo mediaCodecInfo : allCodecInfos) {
if (!mediaCodecInfo.isEncoder()) {
continue;
}
encoders.add(mediaCodecInfo);
}
}
}
private EncoderUtil() {}
}