/*
* Copyright (C) 2014 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 com.google.android.exoplayer;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi;
import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.CodecProfileLevel;
import android.media.MediaCodecList;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import java.util.HashMap;
/**
* A utility class for querying the available codecs.
*/
@TargetApi(16)
public class MediaCodecUtil {
/**
* Thrown when an error occurs querying the device for its underlying media capabilities.
* <p>
* Such failures are not expected in normal operation and are normally temporary (e.g. if the
* mediaserver process has crashed and is yet to restart).
*/
public static class DecoderQueryException extends Exception {
private DecoderQueryException(Throwable cause) {
super("Failed to query underlying media codecs", cause);
}
}
private static final String TAG = "MediaCodecUtil";
private static final HashMap<CodecKey, Pair<String, CodecCapabilities>> codecs =
new HashMap<CodecKey, Pair<String, CodecCapabilities>>();
/**
* Get information about the decoder that will be used for a given mime type.
*
* @param mimeType The mime type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false
* unless secure decryption really is required.
* @return Information about the decoder that will be used, or null if no decoder exists.
*/
public static DecoderInfo getDecoderInfo(String mimeType, boolean secure)
throws DecoderQueryException {
Pair<String, CodecCapabilities> info = getMediaCodecInfo(mimeType, secure);
if (info == null) {
return null;
}
return new DecoderInfo(info.first, isAdaptive(info.second));
}
/**
* Optional call to warm the codec cache for a given mime type.
* <p>
* Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean)}.
*
* @param mimeType The mime type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false
* unless secure decryption really is required.
*/
public static synchronized void warmCodec(String mimeType, boolean secure) {
try {
getMediaCodecInfo(mimeType, secure);
} catch (DecoderQueryException e) {
// Codec warming is best effort, so we can swallow the exception.
Log.e(TAG, "Codec warming failed", e);
}
}
/**
* Returns the name of the best decoder and its capabilities for the given mimeType.
*/
private static synchronized Pair<String, CodecCapabilities> getMediaCodecInfo(
String mimeType, boolean secure) throws DecoderQueryException {
CodecKey key = new CodecKey(mimeType, secure);
if (codecs.containsKey(key)) {
return codecs.get(key);
}
MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21
? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16();
Pair<String, CodecCapabilities> codecInfo = getMediaCodecInfo(key, mediaCodecList);
// TODO: Verify this cannot occur on v22, and change >= to == [Internal: b/18678462].
if (secure && codecInfo == null && Util.SDK_INT >= 21) {
// Some devices don't list secure decoders on API level 21. Try the legacy path.
mediaCodecList = new MediaCodecListCompatV16();
codecInfo = getMediaCodecInfo(key, mediaCodecList);
if (codecInfo != null) {
Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType
+ ". Assuming: " + codecInfo.first);
}
}
return codecInfo;
}
private static Pair<String, CodecCapabilities> getMediaCodecInfo(CodecKey key,
MediaCodecListCompat mediaCodecList) throws DecoderQueryException {
try {
return getMediaCodecInfoInternal(key, mediaCodecList);
} catch (Exception e) {
// If the underlying mediaserver is in a bad state, we may catch an IllegalStateException
// or an IllegalArgumentException here.
throw new DecoderQueryException(e);
}
}
private static Pair<String, CodecCapabilities> getMediaCodecInfoInternal(CodecKey key,
MediaCodecListCompat mediaCodecList) {
String mimeType = key.mimeType;
int numberOfCodecs = mediaCodecList.getCodecCount();
boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit();
// Note: MediaCodecList is sorted by the framework such that the best decoders come first.
for (int i = 0; i < numberOfCodecs; i++) {
MediaCodecInfo info = mediaCodecList.getCodecInfoAt(i);
String codecName = info.getName();
if (!info.isEncoder() && codecName.startsWith("OMX.")
&& (secureDecodersExplicit || !codecName.endsWith(".secure"))) {
String[] supportedTypes = info.getSupportedTypes();
for (int j = 0; j < supportedTypes.length; j++) {
String supportedType = supportedTypes[j];
if (supportedType.equalsIgnoreCase(mimeType)) {
CodecCapabilities capabilities = info.getCapabilitiesForType(supportedType);
boolean secure = mediaCodecList.isSecurePlaybackSupported(key.mimeType, capabilities);
if (!secureDecodersExplicit) {
// Cache variants for both insecure and (if we think it's supported) secure playback.
codecs.put(key.secure ? new CodecKey(mimeType, false) : key,
Pair.create(codecName, capabilities));
if (secure) {
codecs.put(key.secure ? key : new CodecKey(mimeType, true),
Pair.create(codecName + ".secure", capabilities));
}
} else {
// Only cache this variant. If both insecure and secure decoders are available, they
// should both be listed separately.
codecs.put(key.secure == secure ? key : new CodecKey(mimeType, secure),
Pair.create(codecName, capabilities));
}
if (codecs.containsKey(key)) {
return codecs.get(key);
}
}
}
}
}
return null;
}
private static boolean isAdaptive(CodecCapabilities capabilities) {
if (Util.SDK_INT >= 19) {
return isAdaptiveV19(capabilities);
} else {
return false;
}
}
@TargetApi(19)
private static boolean isAdaptiveV19(CodecCapabilities capabilities) {
return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback);
}
/**
* @param profile An AVC profile constant from {@link CodecProfileLevel}.
* @param level An AVC profile level from {@link CodecProfileLevel}.
* @return Whether the specified profile is supported at the specified level.
*/
public static boolean isH264ProfileSupported(int profile, int level)
throws DecoderQueryException {
Pair<String, CodecCapabilities> info = getMediaCodecInfo(MimeTypes.VIDEO_H264, false);
if (info == null) {
return false;
}
CodecCapabilities capabilities = info.second;
for (int i = 0; i < capabilities.profileLevels.length; i++) {
CodecProfileLevel profileLevel = capabilities.profileLevels[i];
if (profileLevel.profile == profile && profileLevel.level >= level) {
return true;
}
}
return false;
}
/**
* @return the maximum frame size for an H264 stream that can be decoded on the device.
*/
public static int maxH264DecodableFrameSize() throws DecoderQueryException {
Pair<String, CodecCapabilities> info = getMediaCodecInfo(MimeTypes.VIDEO_H264, false);
if (info == null) {
return 0;
}
int maxH264DecodableFrameSize = 0;
CodecCapabilities capabilities = info.second;
for (int i = 0; i < capabilities.profileLevels.length; i++) {
CodecProfileLevel profileLevel = capabilities.profileLevels[i];
maxH264DecodableFrameSize = Math.max(
avcLevelToMaxFrameSize(profileLevel.level), maxH264DecodableFrameSize);
}
return maxH264DecodableFrameSize;
}
/**
* Conversion values taken from: https://en.wikipedia.org/wiki/H.264/MPEG-4_AVC.
*
* @param avcLevel one of CodecProfileLevel.AVCLevel* constants.
* @return maximum frame size that can be decoded by a decoder with the specified avc level
* (or {@code -1} if the level is not recognized)
*/
private static int avcLevelToMaxFrameSize(int avcLevel) {
switch (avcLevel) {
case CodecProfileLevel.AVCLevel1: return 25344;
case CodecProfileLevel.AVCLevel1b: return 25344;
case CodecProfileLevel.AVCLevel12: return 101376;
case CodecProfileLevel.AVCLevel13: return 101376;
case CodecProfileLevel.AVCLevel2: return 101376;
case CodecProfileLevel.AVCLevel21: return 202752;
case CodecProfileLevel.AVCLevel22: return 414720;
case CodecProfileLevel.AVCLevel3: return 414720;
case CodecProfileLevel.AVCLevel31: return 921600;
case CodecProfileLevel.AVCLevel32: return 1310720;
case CodecProfileLevel.AVCLevel4: return 2097152;
case CodecProfileLevel.AVCLevel41: return 2097152;
case CodecProfileLevel.AVCLevel42: return 2228224;
case CodecProfileLevel.AVCLevel5: return 5652480;
case CodecProfileLevel.AVCLevel51: return 9437184;
default: return -1;
}
}
private interface MediaCodecListCompat {
/**
* The number of codecs in the list.
*/
public int getCodecCount();
/**
* The info at the specified index in the list.
*
* @param index The index.
*/
public MediaCodecInfo getCodecInfoAt(int index);
/**
* @return Returns whether secure decoders are explicitly listed, if present.
*/
public boolean secureDecodersExplicit();
/**
* Whether secure playback is supported for the given {@link CodecCapabilities}, which should
* have been obtained from a {@link MediaCodecInfo} obtained from this list.
*/
public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities);
}
@TargetApi(21)
private static final class MediaCodecListCompatV21 implements MediaCodecListCompat {
private final int codecKind;
private MediaCodecInfo[] mediaCodecInfos;
public MediaCodecListCompatV21(boolean includeSecure) {
codecKind = MediaCodecList.getCodecCount();//includeSecure ? MediaCodecList.ALL_CODECS : MediaCodecList.REGULAR_CODECS;
}
@Override
public int getCodecCount() {
ensureMediaCodecInfosInitialized();
return mediaCodecInfos.length;
}
@Override
public MediaCodecInfo getCodecInfoAt(int index) {
ensureMediaCodecInfosInitialized();
return mediaCodecInfos[index];
}
@Override
public boolean secureDecodersExplicit() {
return true;
}
@Override
public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) {
return false;
}
private void ensureMediaCodecInfosInitialized() {
}
}
@SuppressWarnings("deprecation")
private static final class MediaCodecListCompatV16 implements MediaCodecListCompat {
@Override
public int getCodecCount() {
return MediaCodecList.getCodecCount();
}
@Override
public MediaCodecInfo getCodecInfoAt(int index) {
return MediaCodecList.getCodecInfoAt(index);
}
@Override
public boolean secureDecodersExplicit() {
return false;
}
@Override
public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) {
// Secure decoders weren't explicitly listed prior to API level 21. We assume that a secure
// H264 decoder exists.
return MimeTypes.VIDEO_H264.equals(mimeType);
}
}
private static final class CodecKey {
public final String mimeType;
public final boolean secure;
public CodecKey(String mimeType, boolean secure) {
this.mimeType = mimeType;
this.secure = secure;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((mimeType == null) ? 0 : mimeType.hashCode());
result = prime * result + (secure ? 1231 : 1237);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || obj.getClass() != CodecKey.class) {
return false;
}
CodecKey other = (CodecKey) obj;
return TextUtils.equals(mimeType, other.mimeType) && secure == other.secure;
}
}
}