diff options
author | 2024-12-18 18:02:51 -0800 | |
---|---|---|
committer | 2024-12-18 18:02:51 -0800 | |
commit | d63c32b2acdc00c2392ad61fa0231845e3abc0c3 (patch) | |
tree | 10b8d636a8a73f56a9e7764e6650bc29e377c3c2 | |
parent | e85b1bb69579002980c079a3b886c83f095a6ceb (diff) | |
parent | 7142fd7704294fe95f1d5146a5b8e668796183c4 (diff) |
Merge changes I34c10bf7,I39ba5949,I40b8ac00,Ie0864e21,Ie96de170 into main am: cec4bfe6d7 am: 7142fd7704
Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/3093180
Change-Id: I0b9211deb7778b76eccd6fee7a76fcb051ad7f0e
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r-- | media/java/android/media/MediaCodecInfo.java | 5716 | ||||
-rw-r--r-- | media/jni/Android.bp | 2 | ||||
-rw-r--r-- | media/jni/android_media_CodecCapabilities.cpp | 1015 | ||||
-rw-r--r-- | media/jni/android_media_CodecCapabilities.h | 47 | ||||
-rw-r--r-- | media/jni/android_media_MediaCodec.cpp | 39 | ||||
-rw-r--r-- | media/jni/android_media_MediaCodecList.cpp | 154 | ||||
-rw-r--r-- | media/jni/android_media_MediaPlayer.cpp | 6 |
7 files changed, 4462 insertions, 2517 deletions
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java index 302969f58ba8..19d39234d1c6 100644 --- a/media/java/android/media/MediaCodecInfo.java +++ b/media/java/android/media/MediaCodecInfo.java @@ -245,12 +245,7 @@ public final class MediaCodecInfo { * {@link MediaCodecInfo#getCapabilitiesForType getCapabilitiesForType()}, passing a MIME type. */ public static final class CodecCapabilities { - public CodecCapabilities() { - } - - // CLASSIFICATION - private String mMime; - private int mMaxSupportedInstances; + private static final String TAG = "CodecCapabilities"; // LEGACY FIELDS @@ -628,12 +623,6 @@ public final class MediaCodecInfo { */ public int[] colorFormats; // NOTE this array is modifiable by user - // FEATURES - - private int mFlagsSupported; - private int mFlagsRequired; - private int mFlagsVerified; - /** * <b>video decoder only</b>: codec supports seamless resolution changes. */ @@ -823,122 +812,680 @@ public final class MediaCodecInfo { @FlaggedApi(FLAG_NULL_OUTPUT_SURFACE) public static final String FEATURE_DetachedSurface = "detached-surface"; - /** - * Query codec feature capabilities. - * <p> - * These features are supported to be used by the codec. These - * include optional features that can be turned on, as well as - * features that are always on. - */ - public final boolean isFeatureSupported(String name) { - return checkFeature(name, mFlagsSupported); - } + /** package private */ interface CodecCapsIntf { + public CodecCapsIntf dup(); - /** - * Query codec feature requirements. - * <p> - * These features are required to be used by the codec, and as such, - * they are always turned on. - */ - public final boolean isFeatureRequired(String name) { - return checkFeature(name, mFlagsRequired); + public boolean isFeatureSupported(String name); + + public boolean isFeatureRequired(String name); + + public boolean isFormatSupported(MediaFormat format); + + public MediaFormat getDefaultFormat(); + + public String getMimeType(); + + public int getMaxSupportedInstances(); + + public AudioCapabilities getAudioCapabilities(); + + public VideoCapabilities getVideoCapabilities(); + + public EncoderCapabilities getEncoderCapabilities(); + + public boolean isRegular(); + + public CodecProfileLevel[] getProfileLevels(); + + public int[] getColorFormats(); } - // Flags are used for feature list creation so separate this into a private - // static class to delay reading the flags only when constructing the list. - private static class FeatureList { - private static Feature[] getDecoderFeatures() { - ArrayList<Feature> features = new ArrayList(); - features.add(new Feature(FEATURE_AdaptivePlayback, (1 << 0), true)); - features.add(new Feature(FEATURE_SecurePlayback, (1 << 1), false)); - features.add(new Feature(FEATURE_TunneledPlayback, (1 << 2), false)); - features.add(new Feature(FEATURE_PartialFrame, (1 << 3), false)); - features.add(new Feature(FEATURE_FrameParsing, (1 << 4), false)); - features.add(new Feature(FEATURE_MultipleFrames, (1 << 5), false)); - features.add(new Feature(FEATURE_DynamicTimestamp, (1 << 6), false)); - features.add(new Feature(FEATURE_LowLatency, (1 << 7), true)); - if (GetFlag(() -> android.media.codec.Flags.dynamicColorAspects())) { - features.add(new Feature(FEATURE_DynamicColorAspects, (1 << 8), true)); + /* package private */ static final class CodecCapsLegacyImpl implements CodecCapsIntf { + // errors while reading profile levels - accessed from sister capabilities + int mError; + + private CodecProfileLevel[] mProfileLevels; + private int[] mColorFormats; + + // CLASSIFICATION + private String mMime; + private int mMaxSupportedInstances; + + // FEATURES + private int mFlagsSupported; + private int mFlagsRequired; + private int mFlagsVerified; + + // NEW-STYLE CAPABILITIES + private AudioCapabilities mAudioCaps; + private VideoCapabilities mVideoCaps; + private EncoderCapabilities mEncoderCaps; + private MediaFormat mDefaultFormat; + + private MediaFormat mCapabilitiesInfo; + + public CodecProfileLevel[] getProfileLevels() { + return mProfileLevels; + } + + public int[] getColorFormats() { + return mColorFormats; + } + + public CodecCapsLegacyImpl() {} + + public CodecCapsLegacyImpl dup() { + CodecCapsLegacyImpl caps = new CodecCapsLegacyImpl(); + + caps.mProfileLevels = Arrays.copyOf(mProfileLevels, mProfileLevels.length); + caps.mColorFormats = Arrays.copyOf(mColorFormats, mColorFormats.length); + + caps.mMime = mMime; + caps.mMaxSupportedInstances = mMaxSupportedInstances; + caps.mFlagsRequired = mFlagsRequired; + caps.mFlagsSupported = mFlagsSupported; + caps.mFlagsVerified = mFlagsVerified; + caps.mAudioCaps = mAudioCaps; + caps.mVideoCaps = mVideoCaps; + caps.mEncoderCaps = mEncoderCaps; + caps.mDefaultFormat = mDefaultFormat; + caps.mCapabilitiesInfo = mCapabilitiesInfo; + + return caps; + } + + public final boolean isFeatureSupported(String name) { + return checkFeature(name, mFlagsSupported); + } + + public final boolean isFeatureRequired(String name) { + return checkFeature(name, mFlagsRequired); + } + + // Flags are used for feature list creation so separate this into a private + // static class to delay reading the flags only when constructing the list. + private static class FeatureList { + private static Feature[] getDecoderFeatures() { + ArrayList<Feature> features = new ArrayList(); + features.add(new Feature(FEATURE_AdaptivePlayback, (1 << 0), true)); + features.add(new Feature(FEATURE_SecurePlayback, (1 << 1), false)); + features.add(new Feature(FEATURE_TunneledPlayback, (1 << 2), false)); + features.add(new Feature(FEATURE_PartialFrame, (1 << 3), false)); + features.add(new Feature(FEATURE_FrameParsing, (1 << 4), false)); + features.add(new Feature(FEATURE_MultipleFrames, (1 << 5), false)); + features.add(new Feature(FEATURE_DynamicTimestamp, (1 << 6), false)); + features.add(new Feature(FEATURE_LowLatency, (1 << 7), true)); + if (GetFlag(() -> android.media.codec.Flags.dynamicColorAspects())) { + features.add(new Feature(FEATURE_DynamicColorAspects, (1 << 8), true)); + } + if (GetFlag(() -> android.media.codec.Flags.nullOutputSurface())) { + features.add(new Feature(FEATURE_DetachedSurface, (1 << 9), true)); + } + + // feature to exclude codec from REGULAR codec list + features.add(new Feature(FEATURE_SpecialCodec, (1 << 30), false, true)); + + return features.toArray(new Feature[0]); + }; + + private static Feature[] decoderFeatures = getDecoderFeatures(); + + private static Feature[] getEncoderFeatures() { + ArrayList<Feature> features = new ArrayList(); + + features.add(new Feature(FEATURE_IntraRefresh, (1 << 0), false)); + features.add(new Feature(FEATURE_MultipleFrames, (1 << 1), false)); + features.add(new Feature(FEATURE_DynamicTimestamp, (1 << 2), false)); + features.add(new Feature(FEATURE_QpBounds, (1 << 3), false)); + features.add(new Feature(FEATURE_EncodingStatistics, (1 << 4), false)); + features.add(new Feature(FEATURE_HdrEditing, (1 << 5), false)); + if (GetFlag(() -> android.media.codec.Flags.hlgEditing())) { + features.add(new Feature(FEATURE_HlgEditing, (1 << 6), true)); + } + if (GetFlag(() -> android.media.codec.Flags.regionOfInterest())) { + features.add(new Feature(FEATURE_Roi, (1 << 7), true)); + } + + // feature to exclude codec from REGULAR codec list + features.add(new Feature(FEATURE_SpecialCodec, (1 << 30), false, true)); + + return features.toArray(new Feature[0]); + }; + + private static Feature[] encoderFeatures = getEncoderFeatures(); + + public static Feature[] getFeatures(boolean isEncoder) { + if (isEncoder) { + return encoderFeatures; + } else { + return decoderFeatures; + } } - if (GetFlag(() -> android.media.codec.Flags.nullOutputSurface())) { - features.add(new Feature(FEATURE_DetachedSurface, (1 << 9), true)); + } + + /** @hide */ + public String[] validFeatures() { + Feature[] features = getValidFeatures(); + String[] res = new String[features.length]; + for (int i = 0; i < res.length; i++) { + if (!features[i].mInternal) { + res[i] = features[i].mName; + } } + return res; + } - // feature to exclude codec from REGULAR codec list - features.add(new Feature(FEATURE_SpecialCodec, (1 << 30), false, true)); + private Feature[] getValidFeatures() { + return FeatureList.getFeatures(isEncoder()); + } - return features.toArray(new Feature[0]); - }; + private boolean checkFeature(String name, int flags) { + for (Feature feat: getValidFeatures()) { + if (feat.mName.equals(name)) { + return (flags & feat.mValue) != 0; + } + } + return false; + } - private static Feature[] decoderFeatures = getDecoderFeatures(); + public boolean isRegular() { + // regular codecs only require default features + for (Feature feat: getValidFeatures()) { + if (!feat.mDefault && isFeatureRequired(feat.mName)) { + return false; + } + } + return true; + } - private static Feature[] getEncoderFeatures() { - ArrayList<Feature> features = new ArrayList(); + public final boolean isFormatSupported(MediaFormat format) { + final Map<String, Object> map = format.getMap(); + final String mime = (String) map.get(MediaFormat.KEY_MIME); - features.add(new Feature(FEATURE_IntraRefresh, (1 << 0), false)); - features.add(new Feature(FEATURE_MultipleFrames, (1 << 1), false)); - features.add(new Feature(FEATURE_DynamicTimestamp, (1 << 2), false)); - features.add(new Feature(FEATURE_QpBounds, (1 << 3), false)); - features.add(new Feature(FEATURE_EncodingStatistics, (1 << 4), false)); - features.add(new Feature(FEATURE_HdrEditing, (1 << 5), false)); - if (GetFlag(() -> android.media.codec.Flags.hlgEditing())) { - features.add(new Feature(FEATURE_HlgEditing, (1 << 6), true)); + // mime must match if present + if (mime != null && !mMime.equalsIgnoreCase(mime)) { + return false; } - if (GetFlag(() -> android.media.codec.Flags.regionOfInterest())) { - features.add(new Feature(FEATURE_Roi, (1 << 7), true)); + + // check feature support + for (Feature feat: getValidFeatures()) { + if (feat.mInternal) { + continue; + } + + Integer yesNo = (Integer) map.get(MediaFormat.KEY_FEATURE_ + feat.mName); + if (yesNo == null) { + continue; + } + if ((yesNo == 1 && !isFeatureSupported(feat.mName)) + || (yesNo == 0 && isFeatureRequired(feat.mName))) { + return false; + } } - // feature to exclude codec from REGULAR codec list - features.add(new Feature(FEATURE_SpecialCodec, (1 << 30), false, true)); + Integer profile = (Integer) map.get(MediaFormat.KEY_PROFILE); + Integer level = (Integer) map.get(MediaFormat.KEY_LEVEL); - return features.toArray(new Feature[0]); - }; + if (profile != null) { + if (!supportsProfileLevel(profile, level)) { + return false; + } - private static Feature[] encoderFeatures = getEncoderFeatures(); + // If we recognize this profile, check that this format is supported by the + // highest level supported by the codec for that profile. (Ignore specified + // level beyond the above profile/level check as level is only used as a + // guidance. E.g. AVC Level 1 CIF format is supported if codec supports + // level 1.1 even though max size for Level 1 is QCIF. However, MPEG2 Simple + // Profile 1080p format is not supported even if codec supports Main Profile + // Level High, as Simple Profile does not support 1080p. + CodecCapsLegacyImpl levelCaps = null; + int maxLevel = 0; + for (CodecProfileLevel pl : mProfileLevels) { + if (pl.profile == profile && pl.level > maxLevel) { + // H.263 levels are not completely ordered: + // Level45 support only implies Level10 support + if (!mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263) + || pl.level != CodecProfileLevel.H263Level45 + || maxLevel == CodecProfileLevel.H263Level10) { + maxLevel = pl.level; + } + } + } + levelCaps = createFromProfileLevel(mMime, profile, maxLevel); + // We must remove the profile from this format otherwise + // levelCaps.isFormatSupported will get into this same condition and loop + // forever. Furthermore, since levelCaps does not contain features and bitrate + // specific keys, keep only keys relevant for a level check. + Map<String, Object> levelCriticalFormatMap = new HashMap<>(map); + final Set<String> criticalKeys = isVideo() + ? VideoCapabilities.VideoCapsLegacyImpl.VIDEO_LEVEL_CRITICAL_FORMAT_KEYS + : isAudio() + ? AudioCapabilities.AudioCapsLegacyImpl.AUDIO_LEVEL_CRITICAL_FORMAT_KEYS + : null; + + // critical keys will always contain KEY_MIME, but should also contain others + // to be meaningful + if (criticalKeys != null && criticalKeys.size() > 1 && levelCaps != null) { + levelCriticalFormatMap.keySet().retainAll(criticalKeys); + + MediaFormat levelCriticalFormat = new MediaFormat(levelCriticalFormatMap); + if (!levelCaps.isFormatSupported(levelCriticalFormat)) { + return false; + } + } + } + if (mAudioCaps != null && !mAudioCaps.supportsFormat(format)) { + return false; + } + if (mVideoCaps != null && !mVideoCaps.supportsFormat(format)) { + return false; + } + if (mEncoderCaps != null && !mEncoderCaps.supportsFormat(format)) { + return false; + } + return true; + } - public static Feature[] getFeatures(boolean isEncoder) { - if (isEncoder) { - return encoderFeatures; - } else { - return decoderFeatures; + private static boolean supportsBitrate( + Range<Integer> bitrateRange, MediaFormat format) { + Map<String, Object> map = format.getMap(); + + // consider max bitrate over average bitrate for support + Integer maxBitrate = (Integer)map.get(MediaFormat.KEY_MAX_BIT_RATE); + Integer bitrate = (Integer)map.get(MediaFormat.KEY_BIT_RATE); + if (bitrate == null) { + bitrate = maxBitrate; + } else if (maxBitrate != null) { + bitrate = Math.max(bitrate, maxBitrate); + } + + if (bitrate != null && bitrate > 0) { + return bitrateRange.contains(bitrate); } + + return true; } - } - /** @hide */ - public String[] validFeatures() { - Feature[] features = getValidFeatures(); - String[] res = new String[features.length]; - for (int i = 0; i < res.length; i++) { - if (!features[i].mInternal) { - res[i] = features[i].mName; + private boolean supportsProfileLevel(int profile, Integer level) { + for (CodecProfileLevel pl: mProfileLevels) { + if (pl.profile != profile) { + continue; + } + + // No specific level requested + if (level == null) { + return true; + } + + // AAC doesn't use levels + if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AAC)) { + return true; + } + + // DTS doesn't use levels + if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS) + || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_HD) + || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_UHD)) { + return true; + } + + // H.263 levels are not completely ordered: + // Level45 support only implies Level10 support + if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263)) { + if (pl.level != level && pl.level == CodecProfileLevel.H263Level45 + && level > CodecProfileLevel.H263Level10) { + continue; + } + } + + // MPEG4 levels are not completely ordered: + // Level1 support only implies Level0 (and not Level0b) support + if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG4)) { + if (pl.level != level && pl.level == CodecProfileLevel.MPEG4Level1 + && level > CodecProfileLevel.MPEG4Level0) { + continue; + } + } + + // HEVC levels incorporate both tiers and levels. Verify tier support. + if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) { + boolean supportsHighTier = + (pl.level & CodecProfileLevel.HEVCHighTierLevels) != 0; + boolean checkingHighTier + = (level & CodecProfileLevel.HEVCHighTierLevels) != 0; + // high tier levels are only supported by other high tier levels + if (checkingHighTier && !supportsHighTier) { + continue; + } + } + + if (pl.level >= level) { + // if we recognize the listed profile/level, we must also recognize the + // profile/level arguments. + if (createFromProfileLevel(mMime, profile, pl.level) != null) { + return createFromProfileLevel(mMime, profile, level) != null; + } + return true; + } } + return false; + } + + public MediaFormat getDefaultFormat() { + return mDefaultFormat; + } + + public String getMimeType() { + return mMime; + } + + public int getMaxSupportedInstances() { + return mMaxSupportedInstances; } - return res; - } - private Feature[] getValidFeatures() { - return FeatureList.getFeatures(isEncoder()); + private boolean isAudio() { + return mAudioCaps != null; + } + + public AudioCapabilities getAudioCapabilities() { + return mAudioCaps; + } + + private boolean isEncoder() { + return mEncoderCaps != null; + } + + public EncoderCapabilities getEncoderCapabilities() { + return mEncoderCaps; + } + + private boolean isVideo() { + return mVideoCaps != null; + } + + public VideoCapabilities getVideoCapabilities() { + return mVideoCaps; + } + + public static CodecCapsLegacyImpl createFromProfileLevel( + String mime, int profile, int level) { + CodecProfileLevel pl = new CodecProfileLevel(); + pl.profile = profile; + pl.level = level; + MediaFormat defaultFormat = new MediaFormat(); + defaultFormat.setString(MediaFormat.KEY_MIME, mime); + + CodecCapsLegacyImpl ret = new CodecCapsLegacyImpl( + new CodecProfileLevel[] { pl }, new int[0], true /* encoder */, + defaultFormat, new MediaFormat() /* info */); + if (ret.mError != 0) { + return null; + } + return ret; + } + + /* package private */ CodecCapsLegacyImpl( + CodecProfileLevel[] profLevs, int[] colFmts, + boolean encoder, + Map<String, Object>defaultFormatMap, + Map<String, Object>capabilitiesMap) { + this(profLevs, colFmts, encoder, + new MediaFormat(defaultFormatMap), + new MediaFormat(capabilitiesMap)); + } + + /* package private */ CodecCapsLegacyImpl( + CodecProfileLevel[] profLevs, int[] colFmts, boolean encoder, + MediaFormat defaultFormat, MediaFormat info) { + final Map<String, Object> map = info.getMap(); + mColorFormats = colFmts; + mFlagsVerified = 0; // TODO: remove as it is unused + mDefaultFormat = defaultFormat; + mCapabilitiesInfo = info; + mMime = mDefaultFormat.getString(MediaFormat.KEY_MIME); + + /* VP9 introduced profiles around 2016, so some VP9 codecs may not advertise any + supported profiles. Determine the level for them using the info they provide. */ + if (profLevs.length == 0 + && mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP9)) { + CodecProfileLevel profLev = new CodecProfileLevel(); + profLev.profile = CodecProfileLevel.VP9Profile0; + profLev.level = VideoCapabilities.VideoCapsLegacyImpl.equivalentVP9Level(info); + profLevs = new CodecProfileLevel[] { profLev }; + } + mProfileLevels = profLevs; + + if (mMime.toLowerCase().startsWith("audio/")) { + mAudioCaps = AudioCapabilities.create(info, this); + mAudioCaps.getDefaultFormat(mDefaultFormat); + } else if (mMime.toLowerCase().startsWith("video/") + || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC)) { + mVideoCaps = VideoCapabilities.create(info, this); + } + if (encoder) { + mEncoderCaps = EncoderCapabilities.create(info, this); + mEncoderCaps.getDefaultFormat(mDefaultFormat); + } + + final Map<String, Object> global = MediaCodecList.getGlobalSettings(); + mMaxSupportedInstances = Utils.parseIntSafely( + global.get("max-concurrent-instances"), DEFAULT_MAX_SUPPORTED_INSTANCES); + + int maxInstances = Utils.parseIntSafely( + map.get("max-concurrent-instances"), mMaxSupportedInstances); + mMaxSupportedInstances = + Range.create(1, MAX_SUPPORTED_INSTANCES_LIMIT).clamp(maxInstances); + + for (Feature feat: getValidFeatures()) { + String key = MediaFormat.KEY_FEATURE_ + feat.mName; + Integer yesNo = (Integer)map.get(key); + if (yesNo == null) { + continue; + } + if (yesNo > 0) { + mFlagsRequired |= feat.mValue; + } + mFlagsSupported |= feat.mValue; + if (!feat.mInternal) { + mDefaultFormat.setInteger(key, 1); + } + // TODO restrict features by mFlagsVerified once all codecs reliably verify them + } + } } - private boolean checkFeature(String name, int flags) { - for (Feature feat: getValidFeatures()) { - if (feat.mName.equals(name)) { - return (flags & feat.mValue) != 0; + /* package private */ static final class CodecCapsNativeImpl implements CodecCapsIntf { + private long mNativeContext; // accessed by native methods + + private CodecProfileLevel[] mProfileLevels; + private int[] mColorFormats; + + private MediaFormat mDefaultFormat; + private AudioCapabilities mAudioCaps; + private VideoCapabilities mVideoCaps; + private EncoderCapabilities mEncoderCaps; + + public static CodecCapsNativeImpl createFromProfileLevel( + String mime, int profile, int level) { + return native_createFromProfileLevel(mime, profile, level); + } + + /** + * Constructor used by JNI. + * + * The Java CodecCapabilities object keeps these subobjects to avoid recontructing. + */ + /* package private */ CodecCapsNativeImpl(CodecProfileLevel[] profLevs, int[] colFmts, + MediaFormat defaultFormat, AudioCapabilities audioCaps, + VideoCapabilities videoCaps, EncoderCapabilities encoderCaps) { + mProfileLevels = profLevs; + mColorFormats = colFmts; + mDefaultFormat = defaultFormat; + mAudioCaps = audioCaps; + mVideoCaps = videoCaps; + mEncoderCaps = encoderCaps; + } + + public CodecCapsNativeImpl dup() { + CodecCapsNativeImpl impl = native_dup(); + return impl; + } + + @Override + protected void finalize() { + native_finalize(); + } + + public CodecProfileLevel[] getProfileLevels() { + return mProfileLevels; + } + + public int[] getColorFormats() { + return mColorFormats; + } + + public final boolean isFeatureSupported(String name) { + return native_isFeatureSupported(name); + } + + public final boolean isFeatureRequired(String name) { + return native_isFeatureRequired(name); + } + + public boolean isRegular() { + return native_isRegular(); + } + + public final boolean isFormatSupported(MediaFormat format) { + if (format == null) { + throw new NullPointerException(); + } + + Map<String, Object> formatMap = format.getMap(); + String[] keys = new String[formatMap.size()]; + Object[] values = new Object[formatMap.size()]; + + int i = 0; + for (Map.Entry<String, Object> entry: formatMap.entrySet()) { + keys[i] = entry.getKey(); + values[i] = entry.getValue(); + ++i; } + + return native_isFormatSupported(keys, values); } - return false; + + public MediaFormat getDefaultFormat() { + return mDefaultFormat; + } + + public String getMimeType() { + return native_getMimeType(); + } + + public int getMaxSupportedInstances() { + return native_getMaxSupportedInstances(); + } + + public AudioCapabilities getAudioCapabilities() { + return mAudioCaps; + } + + public EncoderCapabilities getEncoderCapabilities() { + return mEncoderCaps; + } + + public VideoCapabilities getVideoCapabilities() { + return mVideoCaps; + } + + private static native void native_init(); + private static native CodecCapsNativeImpl native_createFromProfileLevel( + String mime, int profile, int level); + private native CodecCapsNativeImpl native_dup(); + private native void native_finalize(); + private native int native_getMaxSupportedInstances(); + private native String native_getMimeType(); + private native final boolean native_isFeatureRequired(String name); + private native final boolean native_isFeatureSupported(String name); + private native final boolean native_isFormatSupported(@Nullable String[] keys, + @Nullable Object[] values); + private native boolean native_isRegular(); + + static { + System.loadLibrary("media_jni"); + native_init(); + } + } + + private CodecCapsIntf mImpl; + + /** + * Retrieve the codec capabilities for a certain {@code mime type}, {@code + * profile} and {@code level}. If the type, or profile-level combination + * is not understood by the framework, it returns null. + * <p class=note> In {@link android.os.Build.VERSION_CODES#M}, calling this + * method without calling any method of the {@link MediaCodecList} class beforehand + * results in a {@link NullPointerException}.</p> + */ + public static CodecCapabilities createFromProfileLevel( + String mime, int profile, int level) { + CodecCapsIntf impl; + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + impl = CodecCapsNativeImpl.createFromProfileLevel(mime, profile, level); + } else { + impl = CodecCapsLegacyImpl.createFromProfileLevel(mime, profile, level); + } + return new CodecCapabilities(impl); + } + + public CodecCapabilities() { + mImpl = new CodecCapsLegacyImpl(); + } + + /** package private */ CodecCapabilities(CodecCapsIntf impl) { + mImpl = impl; + profileLevels = mImpl.getProfileLevels(); + colorFormats = mImpl.getColorFormats(); + } + + /** @hide */ + public CodecCapabilities dup() { + CodecCapabilities caps = new CodecCapabilities(); + + // profileLevels and colorFormats may be modified by client. + caps.profileLevels = Arrays.copyOf(profileLevels, profileLevels.length); + caps.colorFormats = Arrays.copyOf(colorFormats, colorFormats.length); + + caps.mImpl = mImpl.dup(); + + return caps; + } + + /** + * Query codec feature capabilities. + * <p> + * These features are supported to be used by the codec. These + * include optional features that can be turned on, as well as + * features that are always on. + */ + public final boolean isFeatureSupported(String name) { + return mImpl.isFeatureSupported(name); + } + + /** + * Query codec feature requirements. + * <p> + * These features are required to be used by the codec, and as such, + * they are always turned on. + */ + public final boolean isFeatureRequired(String name) { + return mImpl.isFeatureRequired(name); } /** @hide */ public boolean isRegular() { - // regular codecs only require default features - for (Feature feat: getValidFeatures()) { - if (!feat.mDefault && isFeatureRequired(feat.mName)) { - return false; - } - } - return true; + return mImpl.isRegular(); } /** @@ -1047,384 +1594,573 @@ public final class MediaCodecInfo { * and feature requests. */ public final boolean isFormatSupported(MediaFormat format) { - final Map<String, Object> map = format.getMap(); - final String mime = (String)map.get(MediaFormat.KEY_MIME); + return mImpl.isFormatSupported(format); + } - // mime must match if present - if (mime != null && !mMime.equalsIgnoreCase(mime)) { - return false; + /** + * Returns a MediaFormat object with default values for configurations that have + * defaults. + */ + public MediaFormat getDefaultFormat() { + return mImpl.getDefaultFormat(); + } + + /** + * Returns the mime type for which this codec-capability object was created. + */ + public String getMimeType() { + return mImpl.getMimeType(); + } + + /** + * Returns the max number of the supported concurrent codec instances. + * <p> + * This is a hint for an upper bound. Applications should not expect to successfully + * operate more instances than the returned value, but the actual number of + * concurrently operable instances may be less as it depends on the available + * resources at time of use. + */ + public int getMaxSupportedInstances() { + return mImpl.getMaxSupportedInstances(); + } + + /** + * Returns the audio capabilities or {@code null} if this is not an audio codec. + */ + public AudioCapabilities getAudioCapabilities() { + return mImpl.getAudioCapabilities(); + } + + /** + * Returns the encoding capabilities or {@code null} if this is not an encoder. + */ + public EncoderCapabilities getEncoderCapabilities() { + return mImpl.getEncoderCapabilities(); + } + + /** + * Returns the video capabilities or {@code null} if this is not a video codec. + */ + public VideoCapabilities getVideoCapabilities() { + return mImpl.getVideoCapabilities(); + } + } + + /** + * A class that supports querying the audio capabilities of a codec. + */ + public static final class AudioCapabilities { + private static final String TAG = "AudioCapabilities"; + + /* package private */ interface AudioCapsIntf { + public Range<Integer> getBitrateRange(); + + public int[] getSupportedSampleRates(); + + public Range<Integer>[] getSupportedSampleRateRanges(); + + public int getMaxInputChannelCount(); + + public int getMinInputChannelCount(); + + public Range<Integer>[] getInputChannelCountRanges(); + + public boolean isSampleRateSupported(int sampleRate); + + public void getDefaultFormat(MediaFormat format); + + public boolean supportsFormat(MediaFormat format); + } + + /* package private */ static final class AudioCapsLegacyImpl implements AudioCapsIntf { + private CodecCapabilities.CodecCapsLegacyImpl mParent; + private Range<Integer> mBitrateRange; + + private int[] mSampleRates; + private Range<Integer>[] mSampleRateRanges; + private Range<Integer>[] mInputChannelRanges; + + private static final int MAX_INPUT_CHANNEL_COUNT = 30; + + public Range<Integer> getBitrateRange() { + return mBitrateRange; } - // check feature support - for (Feature feat: getValidFeatures()) { - if (feat.mInternal) { - continue; - } + public int[] getSupportedSampleRates() { + return mSampleRates != null ? Arrays.copyOf(mSampleRates, mSampleRates.length) + : null; + } + + public Range<Integer>[] getSupportedSampleRateRanges() { + return Arrays.copyOf(mSampleRateRanges, mSampleRateRanges.length); + } - Integer yesNo = (Integer)map.get(MediaFormat.KEY_FEATURE_ + feat.mName); - if (yesNo == null) { - continue; + public int getMaxInputChannelCount() { + int overall_max = 0; + for (int i = mInputChannelRanges.length - 1; i >= 0; i--) { + int lmax = mInputChannelRanges[i].getUpper(); + if (lmax > overall_max) { + overall_max = lmax; + } } - if ((yesNo == 1 && !isFeatureSupported(feat.mName)) || - (yesNo == 0 && isFeatureRequired(feat.mName))) { - return false; + return overall_max; + } + + public int getMinInputChannelCount() { + int overall_min = MAX_INPUT_CHANNEL_COUNT; + for (int i = mInputChannelRanges.length - 1; i >= 0; i--) { + int lmin = mInputChannelRanges[i].getLower(); + if (lmin < overall_min) { + overall_min = lmin; + } } + return overall_min; } - Integer profile = (Integer)map.get(MediaFormat.KEY_PROFILE); - Integer level = (Integer)map.get(MediaFormat.KEY_LEVEL); + public Range<Integer>[] getInputChannelCountRanges() { + return Arrays.copyOf(mInputChannelRanges, mInputChannelRanges.length); + } - if (profile != null) { - if (!supportsProfileLevel(profile, level)) { - return false; + /* no public constructor */ + private AudioCapsLegacyImpl() { } + + public static AudioCapsLegacyImpl create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + Log.d(TAG, "Legacy implementation is called while native flag is on."); } - // If we recognize this profile, check that this format is supported by the - // highest level supported by the codec for that profile. (Ignore specified - // level beyond the above profile/level check as level is only used as a - // guidance. E.g. AVC Level 1 CIF format is supported if codec supports level 1.1 - // even though max size for Level 1 is QCIF. However, MPEG2 Simple Profile - // 1080p format is not supported even if codec supports Main Profile Level High, - // as Simple Profile does not support 1080p. - CodecCapabilities levelCaps = null; - int maxLevel = 0; - for (CodecProfileLevel pl : profileLevels) { - if (pl.profile == profile && pl.level > maxLevel) { - // H.263 levels are not completely ordered: - // Level45 support only implies Level10 support - if (!mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263) - || pl.level != CodecProfileLevel.H263Level45 - || maxLevel == CodecProfileLevel.H263Level10) { - maxLevel = pl.level; - } + AudioCapsLegacyImpl caps = new AudioCapsLegacyImpl(); + caps.init(info, parent); + return caps; + } + + private void init(MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + mParent = parent; + initWithPlatformLimits(); + applyLevelLimits(); + parseFromInfo(info); + } + + private void initWithPlatformLimits() { + mBitrateRange = Range.create(0, Integer.MAX_VALUE); + mInputChannelRanges = new Range[] {Range.create(1, MAX_INPUT_CHANNEL_COUNT)}; + // mBitrateRange = Range.create(1, 320000); + final int minSampleRate = SystemProperties. + getInt("ro.mediacodec.min_sample_rate", 7350); + final int maxSampleRate = SystemProperties. + getInt("ro.mediacodec.max_sample_rate", 192000); + mSampleRateRanges = new Range[] { Range.create(minSampleRate, maxSampleRate) }; + mSampleRates = null; + } + + private boolean supports(Integer sampleRate, Integer inputChannels) { + // channels and sample rates are checked orthogonally + if (inputChannels != null) { + int ix = Utils.binarySearchDistinctRanges( + mInputChannelRanges, inputChannels); + if (ix < 0) { + return false; } } - levelCaps = createFromProfileLevel(mMime, profile, maxLevel); - // We must remove the profile from this format otherwise levelCaps.isFormatSupported - // will get into this same condition and loop forever. Furthermore, since levelCaps - // does not contain features and bitrate specific keys, keep only keys relevant for - // a level check. - Map<String, Object> levelCriticalFormatMap = new HashMap<>(map); - final Set<String> criticalKeys = - isVideo() ? VideoCapabilities.VIDEO_LEVEL_CRITICAL_FORMAT_KEYS : - isAudio() ? AudioCapabilities.AUDIO_LEVEL_CRITICAL_FORMAT_KEYS : - null; - - // critical keys will always contain KEY_MIME, but should also contain others to be - // meaningful - if (criticalKeys != null && criticalKeys.size() > 1 && levelCaps != null) { - levelCriticalFormatMap.keySet().retainAll(criticalKeys); - - MediaFormat levelCriticalFormat = new MediaFormat(levelCriticalFormatMap); - if (!levelCaps.isFormatSupported(levelCriticalFormat)) { + if (sampleRate != null) { + int ix = Utils.binarySearchDistinctRanges( + mSampleRateRanges, sampleRate); + if (ix < 0) { return false; } } + return true; } - if (mAudioCaps != null && !mAudioCaps.supportsFormat(format)) { - return false; - } - if (mVideoCaps != null && !mVideoCaps.supportsFormat(format)) { - return false; - } - if (mEncoderCaps != null && !mEncoderCaps.supportsFormat(format)) { - return false; - } - return true; - } - private static boolean supportsBitrate( - Range<Integer> bitrateRange, MediaFormat format) { - Map<String, Object> map = format.getMap(); + public boolean isSampleRateSupported(int sampleRate) { + return supports(sampleRate, null); + } - // consider max bitrate over average bitrate for support - Integer maxBitrate = (Integer)map.get(MediaFormat.KEY_MAX_BIT_RATE); - Integer bitrate = (Integer)map.get(MediaFormat.KEY_BIT_RATE); - if (bitrate == null) { - bitrate = maxBitrate; - } else if (maxBitrate != null) { - bitrate = Math.max(bitrate, maxBitrate); + /** modifies rates */ + private void limitSampleRates(int[] rates) { + Arrays.sort(rates); + ArrayList<Range<Integer>> ranges = new ArrayList<Range<Integer>>(); + for (int rate: rates) { + if (supports(rate, null /* channels */)) { + ranges.add(Range.create(rate, rate)); + } + } + mSampleRateRanges = ranges.toArray(new Range[ranges.size()]); + createDiscreteSampleRates(); } - if (bitrate != null && bitrate > 0) { - return bitrateRange.contains(bitrate); + private void createDiscreteSampleRates() { + mSampleRates = new int[mSampleRateRanges.length]; + for (int i = 0; i < mSampleRateRanges.length; i++) { + mSampleRates[i] = mSampleRateRanges[i].getLower(); + } } - return true; - } + /** modifies rateRanges */ + private void limitSampleRates(Range<Integer>[] rateRanges) { + sortDistinctRanges(rateRanges); + mSampleRateRanges = intersectSortedDistinctRanges(mSampleRateRanges, rateRanges); - private boolean supportsProfileLevel(int profile, Integer level) { - for (CodecProfileLevel pl: profileLevels) { - if (pl.profile != profile) { - continue; + // check if all values are discrete + for (Range<Integer> range: mSampleRateRanges) { + if (!range.getLower().equals(range.getUpper())) { + mSampleRates = null; + return; + } } + createDiscreteSampleRates(); + } - // No specific level requested - if (level == null) { - return true; + private void applyLevelLimits() { + int[] sampleRates = null; + Range<Integer> sampleRateRange = null, bitRates = null; + int maxChannels = MAX_INPUT_CHANNEL_COUNT; + CodecProfileLevel[] profileLevels = mParent.getProfileLevels(); + String mime = mParent.getMimeType(); + + if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MPEG)) { + sampleRates = new int[] { + 8000, 11025, 12000, + 16000, 22050, 24000, + 32000, 44100, 48000 }; + bitRates = Range.create(8000, 320000); + maxChannels = 2; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_NB)) { + sampleRates = new int[] { 8000 }; + bitRates = Range.create(4750, 12200); + maxChannels = 1; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_WB)) { + sampleRates = new int[] { 16000 }; + bitRates = Range.create(6600, 23850); + maxChannels = 1; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AAC)) { + sampleRates = new int[] { + 7350, 8000, + 11025, 12000, 16000, + 22050, 24000, 32000, + 44100, 48000, 64000, + 88200, 96000 }; + bitRates = Range.create(8000, 510000); + maxChannels = 48; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_VORBIS)) { + bitRates = Range.create(32000, 500000); + sampleRateRange = Range.create(8000, 192000); + maxChannels = 255; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_OPUS)) { + bitRates = Range.create(6000, 510000); + sampleRates = new int[] { 8000, 12000, 16000, 24000, 48000 }; + maxChannels = 255; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_RAW)) { + sampleRateRange = Range.create(1, 192000); + bitRates = Range.create(1, 10000000); + maxChannels = AudioSystem.OUT_CHANNEL_COUNT_MAX; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_FLAC)) { + sampleRateRange = Range.create(1, 655350); + // lossless codec, so bitrate is ignored + maxChannels = 255; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_ALAW) + || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_MLAW)) { + sampleRates = new int[] { 8000 }; + bitRates = Range.create(64000, 64000); + // platform allows multiple channels for this format + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MSGSM)) { + sampleRates = new int[] { 8000 }; + bitRates = Range.create(13000, 13000); + maxChannels = 1; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC3)) { + maxChannels = 6; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3)) { + maxChannels = 16; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3_JOC)) { + sampleRates = new int[] { 48000 }; + bitRates = Range.create(32000, 6144000); + maxChannels = 16; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC4)) { + sampleRates = new int[] { 44100, 48000, 96000, 192000 }; + bitRates = Range.create(16000, 2688000); + maxChannels = 24; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS)) { + sampleRates = new int[] { 44100, 48000 }; + bitRates = Range.create(96000, 1524000); + maxChannels = 6; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_HD)) { + for (CodecProfileLevel profileLevel: profileLevels) { + switch (profileLevel.profile) { + case CodecProfileLevel.DTS_HDProfileLBR: + sampleRates = new int[]{ 22050, 24000, 44100, 48000 }; + bitRates = Range.create(32000, 768000); + break; + case CodecProfileLevel.DTS_HDProfileHRA: + case CodecProfileLevel.DTS_HDProfileMA: + sampleRates + = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; + bitRates = Range.create(96000, 24500000); + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + mParent.mError |= ERROR_UNRECOGNIZED; + sampleRates + = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; + bitRates = Range.create(96000, 24500000); + } + } + maxChannels = 8; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_UHD)) { + for (CodecProfileLevel profileLevel: profileLevels) { + switch (profileLevel.profile) { + case CodecProfileLevel.DTS_UHDProfileP2: + sampleRates = new int[]{ 48000 }; + bitRates = Range.create(96000, 768000); + maxChannels = 10; + break; + case CodecProfileLevel.DTS_UHDProfileP1: + sampleRates + = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; + bitRates = Range.create(96000, 24500000); + maxChannels = 32; + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + mParent.mError |= ERROR_UNRECOGNIZED; + sampleRates + = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; + bitRates = Range.create(96000, 24500000); + maxChannels = 32; + } + } + } else { + Log.w(TAG, "Unsupported mime " + mime); + mParent.mError |= ERROR_UNSUPPORTED; } - // AAC doesn't use levels - if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AAC)) { - return true; + // restrict ranges + if (sampleRates != null) { + limitSampleRates(sampleRates); + } else if (sampleRateRange != null) { + limitSampleRates(new Range[] { sampleRateRange }); } - // DTS doesn't use levels - if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS) - || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_HD) - || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_UHD)) { - return true; + Range<Integer> channelRange = Range.create(1, maxChannels); + + applyLimits(new Range[] { channelRange }, bitRates); + } + + private void applyLimits(Range<Integer>[] inputChannels, Range<Integer> bitRates) { + + // clamp & make a local copy + Range<Integer>[] myInputChannels = new Range[inputChannels.length]; + for (int i = 0; i < inputChannels.length; i++) { + int lower = inputChannels[i].clamp(1); + int upper = inputChannels[i].clamp(MAX_INPUT_CHANNEL_COUNT); + myInputChannels[i] = Range.create(lower, upper); } - // H.263 levels are not completely ordered: - // Level45 support only implies Level10 support - if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263)) { - if (pl.level != level && pl.level == CodecProfileLevel.H263Level45 - && level > CodecProfileLevel.H263Level10) { - continue; - } + // sort, intersect with existing, & save channel list + sortDistinctRanges(myInputChannels); + Range<Integer>[] joinedChannelList = + intersectSortedDistinctRanges(myInputChannels, mInputChannelRanges); + mInputChannelRanges = joinedChannelList; + + if (bitRates != null) { + mBitrateRange = mBitrateRange.intersect(bitRates); } + } - // MPEG4 levels are not completely ordered: - // Level1 support only implies Level0 (and not Level0b) support - if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG4)) { - if (pl.level != level && pl.level == CodecProfileLevel.MPEG4Level1 - && level > CodecProfileLevel.MPEG4Level0) { - continue; + private void parseFromInfo(MediaFormat info) { + int maxInputChannels = MAX_INPUT_CHANNEL_COUNT; + Range<Integer>[] channels = new Range[] { Range.create(1, maxInputChannels)}; + Range<Integer> bitRates = POSITIVE_INTEGERS; + + if (info.containsKey("sample-rate-ranges")) { + String[] rateStrings = info.getString("sample-rate-ranges").split(","); + Range<Integer>[] rateRanges = new Range[rateStrings.length]; + for (int i = 0; i < rateStrings.length; i++) { + rateRanges[i] = Utils.parseIntRange(rateStrings[i], null); } + limitSampleRates(rateRanges); } - // HEVC levels incorporate both tiers and levels. Verify tier support. - if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) { - boolean supportsHighTier = - (pl.level & CodecProfileLevel.HEVCHighTierLevels) != 0; - boolean checkingHighTier = (level & CodecProfileLevel.HEVCHighTierLevels) != 0; - // high tier levels are only supported by other high tier levels - if (checkingHighTier && !supportsHighTier) { - continue; + // we will prefer channel-ranges over max-channel-count + if (info.containsKey("channel-ranges")) { + String[] channelStrings = info.getString("channel-ranges").split(","); + Range<Integer>[] channelRanges = new Range[channelStrings.length]; + for (int i = 0; i < channelStrings.length; i++) { + channelRanges[i] = Utils.parseIntRange(channelStrings[i], null); + } + channels = channelRanges; + } else if (info.containsKey("channel-range")) { + Range<Integer> oneRange = Utils.parseIntRange(info.getString("channel-range"), + null); + channels = new Range[] { oneRange }; + } else if (info.containsKey("max-channel-count")) { + maxInputChannels = Utils.parseIntSafely( + info.getString("max-channel-count"), maxInputChannels); + if (maxInputChannels == 0) { + channels = new Range[] {Range.create(0, 0)}; + } else { + channels = new Range[] {Range.create(1, maxInputChannels)}; } + } else if ((mParent.mError & ERROR_UNSUPPORTED) != 0) { + maxInputChannels = 0; + channels = new Range[] {Range.create(0, 0)}; } - if (pl.level >= level) { - // if we recognize the listed profile/level, we must also recognize the - // profile/level arguments. - if (createFromProfileLevel(mMime, profile, pl.level) != null) { - return createFromProfileLevel(mMime, profile, level) != null; - } - return true; + if (info.containsKey("bitrate-range")) { + bitRates = bitRates.intersect( + Utils.parseIntRange(info.getString("bitrate-range"), bitRates)); } + + applyLimits(channels, bitRates); } - return false; - } - // errors while reading profile levels - accessed from sister capabilities - int mError; + /** @hide */ + public void getDefaultFormat(MediaFormat format) { + // report settings that have only a single choice + if (mBitrateRange.getLower().equals(mBitrateRange.getUpper())) { + format.setInteger(MediaFormat.KEY_BIT_RATE, mBitrateRange.getLower()); + } + if (getMaxInputChannelCount() == 1) { + // mono-only format + format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); + } + if (mSampleRates != null && mSampleRates.length == 1) { + format.setInteger(MediaFormat.KEY_SAMPLE_RATE, mSampleRates[0]); + } + } - private static final String TAG = "CodecCapabilities"; + /* package private */ + // must not contain KEY_PROFILE + static final Set<String> AUDIO_LEVEL_CRITICAL_FORMAT_KEYS = Set.of( + // We don't set level-specific limits for audio codecs today. Key candidates + // would be sample rate, bit rate or channel count. + // MediaFormat.KEY_SAMPLE_RATE, + // MediaFormat.KEY_CHANNEL_COUNT, + // MediaFormat.KEY_BIT_RATE, + MediaFormat.KEY_MIME); + + /** @hide */ + public boolean supportsFormat(MediaFormat format) { + Map<String, Object> map = format.getMap(); + Integer sampleRate = (Integer)map.get(MediaFormat.KEY_SAMPLE_RATE); + Integer channels = (Integer)map.get(MediaFormat.KEY_CHANNEL_COUNT); + + if (!supports(sampleRate, channels)) { + return false; + } - // NEW-STYLE CAPABILITIES - private AudioCapabilities mAudioCaps; - private VideoCapabilities mVideoCaps; - private EncoderCapabilities mEncoderCaps; - private MediaFormat mDefaultFormat; + if (!CodecCapabilities.CodecCapsLegacyImpl.supportsBitrate(mBitrateRange, format)) { + return false; + } - /** - * Returns a MediaFormat object with default values for configurations that have - * defaults. - */ - public MediaFormat getDefaultFormat() { - return mDefaultFormat; + // nothing to do for: + // KEY_CHANNEL_MASK: codecs don't get this + // KEY_IS_ADTS: required feature for all AAC decoders + return true; + } } - /** - * Returns the mime type for which this codec-capability object was created. - */ - public String getMimeType() { - return mMime; - } + /* package private */ static final class AudioCapsNativeImpl implements AudioCapsIntf { + private long mNativeContext; // accessed by native methods - /** - * Returns the max number of the supported concurrent codec instances. - * <p> - * This is a hint for an upper bound. Applications should not expect to successfully - * operate more instances than the returned value, but the actual number of - * concurrently operable instances may be less as it depends on the available - * resources at time of use. - */ - public int getMaxSupportedInstances() { - return mMaxSupportedInstances; - } + private Range<Integer> mBitrateRange; + private int[] mSampleRates; + private Range<Integer>[] mSampleRateRanges; + private Range<Integer>[] mInputChannelRanges; - private boolean isAudio() { - return mAudioCaps != null; - } + /** + * Constructor used by JNI. + * + * The Java AudioCapabilities object keeps these subobjects to avoid recontruction. + */ + /* package private */ AudioCapsNativeImpl(Range<Integer> bitrateRange, + int[] sampleRates, Range<Integer>[] sampleRateRanges, + Range<Integer>[] inputChannelRanges) { + mBitrateRange = bitrateRange; + mSampleRates = sampleRates; + mSampleRateRanges = sampleRateRanges; + mInputChannelRanges = inputChannelRanges; + } - /** - * Returns the audio capabilities or {@code null} if this is not an audio codec. - */ - public AudioCapabilities getAudioCapabilities() { - return mAudioCaps; - } + /* no public constructor */ + private AudioCapsNativeImpl() { } - private boolean isEncoder() { - return mEncoderCaps != null; - } + public Range<Integer> getBitrateRange() { + return mBitrateRange; + } - /** - * Returns the encoding capabilities or {@code null} if this is not an encoder. - */ - public EncoderCapabilities getEncoderCapabilities() { - return mEncoderCaps; - } + public int[] getSupportedSampleRates() { + return mSampleRates != null ? Arrays.copyOf(mSampleRates, mSampleRates.length) + : null; + } - private boolean isVideo() { - return mVideoCaps != null; - } + public Range<Integer>[] getSupportedSampleRateRanges() { + return Arrays.copyOf(mSampleRateRanges, mSampleRateRanges.length); + } - /** - * Returns the video capabilities or {@code null} if this is not a video codec. - */ - public VideoCapabilities getVideoCapabilities() { - return mVideoCaps; - } + public Range<Integer>[] getInputChannelCountRanges() { + return Arrays.copyOf(mInputChannelRanges, mInputChannelRanges.length); + } - /** @hide */ - public CodecCapabilities dup() { - CodecCapabilities caps = new CodecCapabilities(); + public int getMaxInputChannelCount() { + return native_getMaxInputChannelCount(); + } - // profileLevels and colorFormats may be modified by client. - caps.profileLevels = Arrays.copyOf(profileLevels, profileLevels.length); - caps.colorFormats = Arrays.copyOf(colorFormats, colorFormats.length); + public int getMinInputChannelCount() { + return native_getMinInputChannelCount(); + } - caps.mMime = mMime; - caps.mMaxSupportedInstances = mMaxSupportedInstances; - caps.mFlagsRequired = mFlagsRequired; - caps.mFlagsSupported = mFlagsSupported; - caps.mFlagsVerified = mFlagsVerified; - caps.mAudioCaps = mAudioCaps; - caps.mVideoCaps = mVideoCaps; - caps.mEncoderCaps = mEncoderCaps; - caps.mDefaultFormat = mDefaultFormat; - caps.mCapabilitiesInfo = mCapabilitiesInfo; + public boolean isSampleRateSupported(int sampleRate) { + return native_isSampleRateSupported(sampleRate); + } - return caps; - } + // This API is for internal Java implementation only. Should not be called. + public void getDefaultFormat(MediaFormat format) { + throw new UnsupportedOperationException( + "Java Implementation should not call native implemenatation"); + } - /** - * Retrieve the codec capabilities for a certain {@code mime type}, {@code - * profile} and {@code level}. If the type, or profile-level combination - * is not understood by the framework, it returns null. - * <p class=note> In {@link android.os.Build.VERSION_CODES#M}, calling this - * method without calling any method of the {@link MediaCodecList} class beforehand - * results in a {@link NullPointerException}.</p> - */ - public static CodecCapabilities createFromProfileLevel( - String mime, int profile, int level) { - CodecProfileLevel pl = new CodecProfileLevel(); - pl.profile = profile; - pl.level = level; - MediaFormat defaultFormat = new MediaFormat(); - defaultFormat.setString(MediaFormat.KEY_MIME, mime); - - CodecCapabilities ret = new CodecCapabilities( - new CodecProfileLevel[] { pl }, new int[0], true /* encoder */, - defaultFormat, new MediaFormat() /* info */); - if (ret.mError != 0) { - return null; + // This API is for internal Java implementation only. Should not be called. + public boolean supportsFormat(MediaFormat format) { + throw new UnsupportedOperationException( + "Java Implementation should not call native implemenatation"); } - return ret; - } - /* package private */ CodecCapabilities( - CodecProfileLevel[] profLevs, int[] colFmts, - boolean encoder, - Map<String, Object>defaultFormatMap, - Map<String, Object>capabilitiesMap) { - this(profLevs, colFmts, encoder, - new MediaFormat(defaultFormatMap), - new MediaFormat(capabilitiesMap)); - } + private native int native_getMaxInputChannelCount(); + private native int native_getMinInputChannelCount(); + private native boolean native_isSampleRateSupported(int sampleRate); + private static native void native_init(); - private MediaFormat mCapabilitiesInfo; - - /* package private */ CodecCapabilities( - CodecProfileLevel[] profLevs, int[] colFmts, boolean encoder, - MediaFormat defaultFormat, MediaFormat info) { - final Map<String, Object> map = info.getMap(); - colorFormats = colFmts; - mFlagsVerified = 0; // TODO: remove as it is unused - mDefaultFormat = defaultFormat; - mCapabilitiesInfo = info; - mMime = mDefaultFormat.getString(MediaFormat.KEY_MIME); - - /* VP9 introduced profiles around 2016, so some VP9 codecs may not advertise any - supported profiles. Determine the level for them using the info they provide. */ - if (profLevs.length == 0 && mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP9)) { - CodecProfileLevel profLev = new CodecProfileLevel(); - profLev.profile = CodecProfileLevel.VP9Profile0; - profLev.level = VideoCapabilities.equivalentVP9Level(info); - profLevs = new CodecProfileLevel[] { profLev }; - } - profileLevels = profLevs; - - if (mMime.toLowerCase().startsWith("audio/")) { - mAudioCaps = AudioCapabilities.create(info, this); - mAudioCaps.getDefaultFormat(mDefaultFormat); - } else if (mMime.toLowerCase().startsWith("video/") - || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC)) { - mVideoCaps = VideoCapabilities.create(info, this); - } - if (encoder) { - mEncoderCaps = EncoderCapabilities.create(info, this); - mEncoderCaps.getDefaultFormat(mDefaultFormat); - } - - final Map<String, Object> global = MediaCodecList.getGlobalSettings(); - mMaxSupportedInstances = Utils.parseIntSafely( - global.get("max-concurrent-instances"), DEFAULT_MAX_SUPPORTED_INSTANCES); - - int maxInstances = Utils.parseIntSafely( - map.get("max-concurrent-instances"), mMaxSupportedInstances); - mMaxSupportedInstances = - Range.create(1, MAX_SUPPORTED_INSTANCES_LIMIT).clamp(maxInstances); - - for (Feature feat: getValidFeatures()) { - String key = MediaFormat.KEY_FEATURE_ + feat.mName; - Integer yesNo = (Integer)map.get(key); - if (yesNo == null) { - continue; - } - if (yesNo > 0) { - mFlagsRequired |= feat.mValue; - } - mFlagsSupported |= feat.mValue; - if (!feat.mInternal) { - mDefaultFormat.setInteger(key, 1); - } - // TODO restrict features by mFlagsVerified once all codecs reliably verify them + static { + System.loadLibrary("media_jni"); + native_init(); } } - } - /** - * A class that supports querying the audio capabilities of a codec. - */ - public static final class AudioCapabilities { - private static final String TAG = "AudioCapabilities"; - private CodecCapabilities mParent; - private Range<Integer> mBitrateRange; + private AudioCapsIntf mImpl; + + /** @hide */ + public static AudioCapabilities create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + AudioCapsLegacyImpl impl = AudioCapsLegacyImpl.create(info, parent); + AudioCapabilities caps = new AudioCapabilities(impl); + return caps; + } - private int[] mSampleRates; - private Range<Integer>[] mSampleRateRanges; - private Range<Integer>[] mInputChannelRanges; + /* package private */ AudioCapabilities(AudioCapsIntf impl) { + mImpl = impl; + } - private static final int MAX_INPUT_CHANNEL_COUNT = 30; + /* no public constructor */ + private AudioCapabilities() { } /** * Returns the range of supported bitrates in bits/second. */ public Range<Integer> getBitrateRange() { - return mBitrateRange; + return mImpl.getBitrateRange(); } /** @@ -1433,7 +2169,7 @@ public final class MediaCodecInfo { * {@code null}. The array is sorted in ascending order. */ public int[] getSupportedSampleRates() { - return mSampleRates != null ? Arrays.copyOf(mSampleRates, mSampleRates.length) : null; + return mImpl.getSupportedSampleRates(); } /** @@ -1442,7 +2178,21 @@ public final class MediaCodecInfo { * distinct. */ public Range<Integer>[] getSupportedSampleRateRanges() { - return Arrays.copyOf(mSampleRateRanges, mSampleRateRanges.length); + return mImpl.getSupportedSampleRateRanges(); + } + + /* + * Returns an array of ranges representing the number of input channels supported. + * The codec supports any number of input channels within this range. + * + * This supersedes the {@link #getMaxInputChannelCount} method. + * + * For many codecs, this will be a single range [1..N], for some N. + */ + @SuppressLint("ArrayReturn") + @NonNull + public Range<Integer>[] getInputChannelCountRanges() { + return mImpl.getInputChannelCountRanges(); } /** @@ -1462,14 +2212,7 @@ public final class MediaCodecInfo { */ @IntRange(from = 1, to = 255) public int getMaxInputChannelCount() { - int overall_max = 0; - for (int i = mInputChannelRanges.length - 1; i >= 0; i--) { - int lmax = mInputChannelRanges[i].getUpper(); - if (lmax > overall_max) { - overall_max = lmax; - } - } - return overall_max; + return mImpl.getMaxInputChannelCount(); } /** @@ -1481,364 +2224,24 @@ public final class MediaCodecInfo { */ @IntRange(from = 1, to = 255) public int getMinInputChannelCount() { - int overall_min = MAX_INPUT_CHANNEL_COUNT; - for (int i = mInputChannelRanges.length - 1; i >= 0; i--) { - int lmin = mInputChannelRanges[i].getLower(); - if (lmin < overall_min) { - overall_min = lmin; - } - } - return overall_min; - } - - /* - * Returns an array of ranges representing the number of input channels supported. - * The codec supports any number of input channels within this range. - * - * This supersedes the {@link #getMaxInputChannelCount} method. - * - * For many codecs, this will be a single range [1..N], for some N. - */ - @SuppressLint("ArrayReturn") - @NonNull - public Range<Integer>[] getInputChannelCountRanges() { - return Arrays.copyOf(mInputChannelRanges, mInputChannelRanges.length); - } - - /* no public constructor */ - private AudioCapabilities() { } - - /** @hide */ - public static AudioCapabilities create( - MediaFormat info, CodecCapabilities parent) { - AudioCapabilities caps = new AudioCapabilities(); - caps.init(info, parent); - return caps; - } - - private void init(MediaFormat info, CodecCapabilities parent) { - mParent = parent; - initWithPlatformLimits(); - applyLevelLimits(); - parseFromInfo(info); - } - - private void initWithPlatformLimits() { - mBitrateRange = Range.create(0, Integer.MAX_VALUE); - mInputChannelRanges = new Range[] {Range.create(1, MAX_INPUT_CHANNEL_COUNT)}; - // mBitrateRange = Range.create(1, 320000); - final int minSampleRate = SystemProperties. - getInt("ro.mediacodec.min_sample_rate", 7350); - final int maxSampleRate = SystemProperties. - getInt("ro.mediacodec.max_sample_rate", 192000); - mSampleRateRanges = new Range[] { Range.create(minSampleRate, maxSampleRate) }; - mSampleRates = null; - } - - private boolean supports(Integer sampleRate, Integer inputChannels) { - // channels and sample rates are checked orthogonally - if (inputChannels != null) { - int ix = Utils.binarySearchDistinctRanges( - mInputChannelRanges, inputChannels); - if (ix < 0) { - return false; - } - } - if (sampleRate != null) { - int ix = Utils.binarySearchDistinctRanges( - mSampleRateRanges, sampleRate); - if (ix < 0) { - return false; - } - } - return true; + return mImpl.getMinInputChannelCount(); } /** * Query whether the sample rate is supported by the codec. */ public boolean isSampleRateSupported(int sampleRate) { - return supports(sampleRate, null); - } - - /** modifies rates */ - private void limitSampleRates(int[] rates) { - Arrays.sort(rates); - ArrayList<Range<Integer>> ranges = new ArrayList<Range<Integer>>(); - for (int rate: rates) { - if (supports(rate, null /* channels */)) { - ranges.add(Range.create(rate, rate)); - } - } - mSampleRateRanges = ranges.toArray(new Range[ranges.size()]); - createDiscreteSampleRates(); - } - - private void createDiscreteSampleRates() { - mSampleRates = new int[mSampleRateRanges.length]; - for (int i = 0; i < mSampleRateRanges.length; i++) { - mSampleRates[i] = mSampleRateRanges[i].getLower(); - } - } - - /** modifies rateRanges */ - private void limitSampleRates(Range<Integer>[] rateRanges) { - sortDistinctRanges(rateRanges); - mSampleRateRanges = intersectSortedDistinctRanges(mSampleRateRanges, rateRanges); - - // check if all values are discrete - for (Range<Integer> range: mSampleRateRanges) { - if (!range.getLower().equals(range.getUpper())) { - mSampleRates = null; - return; - } - } - createDiscreteSampleRates(); - } - - private void applyLevelLimits() { - int[] sampleRates = null; - Range<Integer> sampleRateRange = null, bitRates = null; - int maxChannels = MAX_INPUT_CHANNEL_COUNT; - CodecProfileLevel[] profileLevels = mParent.profileLevels; - String mime = mParent.getMimeType(); - - if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MPEG)) { - sampleRates = new int[] { - 8000, 11025, 12000, - 16000, 22050, 24000, - 32000, 44100, 48000 }; - bitRates = Range.create(8000, 320000); - maxChannels = 2; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_NB)) { - sampleRates = new int[] { 8000 }; - bitRates = Range.create(4750, 12200); - maxChannels = 1; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_WB)) { - sampleRates = new int[] { 16000 }; - bitRates = Range.create(6600, 23850); - maxChannels = 1; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AAC)) { - sampleRates = new int[] { - 7350, 8000, - 11025, 12000, 16000, - 22050, 24000, 32000, - 44100, 48000, 64000, - 88200, 96000 }; - bitRates = Range.create(8000, 510000); - maxChannels = 48; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_VORBIS)) { - bitRates = Range.create(32000, 500000); - sampleRateRange = Range.create(8000, 192000); - maxChannels = 255; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_OPUS)) { - bitRates = Range.create(6000, 510000); - sampleRates = new int[] { 8000, 12000, 16000, 24000, 48000 }; - maxChannels = 255; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_RAW)) { - sampleRateRange = Range.create(1, 192000); - bitRates = Range.create(1, 10000000); - maxChannels = AudioSystem.OUT_CHANNEL_COUNT_MAX; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_FLAC)) { - sampleRateRange = Range.create(1, 655350); - // lossless codec, so bitrate is ignored - maxChannels = 255; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_ALAW) - || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_MLAW)) { - sampleRates = new int[] { 8000 }; - bitRates = Range.create(64000, 64000); - // platform allows multiple channels for this format - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MSGSM)) { - sampleRates = new int[] { 8000 }; - bitRates = Range.create(13000, 13000); - maxChannels = 1; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC3)) { - maxChannels = 6; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3)) { - maxChannels = 16; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3_JOC)) { - sampleRates = new int[] { 48000 }; - bitRates = Range.create(32000, 6144000); - maxChannels = 16; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC4)) { - sampleRates = new int[] { 44100, 48000, 96000, 192000 }; - bitRates = Range.create(16000, 2688000); - maxChannels = 24; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS)) { - sampleRates = new int[] { 44100, 48000 }; - bitRates = Range.create(96000, 1524000); - maxChannels = 6; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_HD)) { - for (CodecProfileLevel profileLevel: profileLevels) { - switch (profileLevel.profile) { - case CodecProfileLevel.DTS_HDProfileLBR: - sampleRates = new int[]{ 22050, 24000, 44100, 48000 }; - bitRates = Range.create(32000, 768000); - break; - case CodecProfileLevel.DTS_HDProfileHRA: - case CodecProfileLevel.DTS_HDProfileMA: - sampleRates = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; - bitRates = Range.create(96000, 24500000); - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - mParent.mError |= ERROR_UNRECOGNIZED; - sampleRates = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; - bitRates = Range.create(96000, 24500000); - } - } - maxChannels = 8; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_UHD)) { - for (CodecProfileLevel profileLevel: profileLevels) { - switch (profileLevel.profile) { - case CodecProfileLevel.DTS_UHDProfileP2: - sampleRates = new int[]{ 48000 }; - bitRates = Range.create(96000, 768000); - maxChannels = 10; - break; - case CodecProfileLevel.DTS_UHDProfileP1: - sampleRates = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; - bitRates = Range.create(96000, 24500000); - maxChannels = 32; - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - mParent.mError |= ERROR_UNRECOGNIZED; - sampleRates = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; - bitRates = Range.create(96000, 24500000); - maxChannels = 32; - } - } - } else { - Log.w(TAG, "Unsupported mime " + mime); - mParent.mError |= ERROR_UNSUPPORTED; - } - - // restrict ranges - if (sampleRates != null) { - limitSampleRates(sampleRates); - } else if (sampleRateRange != null) { - limitSampleRates(new Range[] { sampleRateRange }); - } - - Range<Integer> channelRange = Range.create(1, maxChannels); - - applyLimits(new Range[] { channelRange }, bitRates); - } - - private void applyLimits(Range<Integer>[] inputChannels, Range<Integer> bitRates) { - - // clamp & make a local copy - Range<Integer>[] myInputChannels = new Range[inputChannels.length]; - for (int i = 0; i < inputChannels.length; i++) { - int lower = inputChannels[i].clamp(1); - int upper = inputChannels[i].clamp(MAX_INPUT_CHANNEL_COUNT); - myInputChannels[i] = Range.create(lower, upper); - } - - // sort, intersect with existing, & save channel list - sortDistinctRanges(myInputChannels); - Range<Integer>[] joinedChannelList = - intersectSortedDistinctRanges(myInputChannels, mInputChannelRanges); - mInputChannelRanges = joinedChannelList; - - if (bitRates != null) { - mBitrateRange = mBitrateRange.intersect(bitRates); - } - } - - private void parseFromInfo(MediaFormat info) { - int maxInputChannels = MAX_INPUT_CHANNEL_COUNT; - Range<Integer>[] channels = new Range[] { Range.create(1, maxInputChannels)}; - Range<Integer> bitRates = POSITIVE_INTEGERS; - - if (info.containsKey("sample-rate-ranges")) { - String[] rateStrings = info.getString("sample-rate-ranges").split(","); - Range<Integer>[] rateRanges = new Range[rateStrings.length]; - for (int i = 0; i < rateStrings.length; i++) { - rateRanges[i] = Utils.parseIntRange(rateStrings[i], null); - } - limitSampleRates(rateRanges); - } - - // we will prefer channel-ranges over max-channel-count - if (info.containsKey("channel-ranges")) { - String[] channelStrings = info.getString("channel-ranges").split(","); - Range<Integer>[] channelRanges = new Range[channelStrings.length]; - for (int i = 0; i < channelStrings.length; i++) { - channelRanges[i] = Utils.parseIntRange(channelStrings[i], null); - } - channels = channelRanges; - } else if (info.containsKey("channel-range")) { - Range<Integer> oneRange = Utils.parseIntRange(info.getString("channel-range"), - null); - channels = new Range[] { oneRange }; - } else if (info.containsKey("max-channel-count")) { - maxInputChannels = Utils.parseIntSafely( - info.getString("max-channel-count"), maxInputChannels); - if (maxInputChannels == 0) { - channels = new Range[] {Range.create(0, 0)}; - } else { - channels = new Range[] {Range.create(1, maxInputChannels)}; - } - } else if ((mParent.mError & ERROR_UNSUPPORTED) != 0) { - maxInputChannels = 0; - channels = new Range[] {Range.create(0, 0)}; - } - - if (info.containsKey("bitrate-range")) { - bitRates = bitRates.intersect( - Utils.parseIntRange(info.getString("bitrate-range"), bitRates)); - } - - applyLimits(channels, bitRates); + return mImpl.isSampleRateSupported(sampleRate); } /** @hide */ public void getDefaultFormat(MediaFormat format) { - // report settings that have only a single choice - if (mBitrateRange.getLower().equals(mBitrateRange.getUpper())) { - format.setInteger(MediaFormat.KEY_BIT_RATE, mBitrateRange.getLower()); - } - if (getMaxInputChannelCount() == 1) { - // mono-only format - format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); - } - if (mSampleRates != null && mSampleRates.length == 1) { - format.setInteger(MediaFormat.KEY_SAMPLE_RATE, mSampleRates[0]); - } + mImpl.getDefaultFormat(format); } - /* package private */ - // must not contain KEY_PROFILE - static final Set<String> AUDIO_LEVEL_CRITICAL_FORMAT_KEYS = Set.of( - // We don't set level-specific limits for audio codecs today. Key candidates would - // be sample rate, bit rate or channel count. - // MediaFormat.KEY_SAMPLE_RATE, - // MediaFormat.KEY_CHANNEL_COUNT, - // MediaFormat.KEY_BIT_RATE, - MediaFormat.KEY_MIME); - /** @hide */ public boolean supportsFormat(MediaFormat format) { - Map<String, Object> map = format.getMap(); - Integer sampleRate = (Integer)map.get(MediaFormat.KEY_SAMPLE_RATE); - Integer channels = (Integer)map.get(MediaFormat.KEY_CHANNEL_COUNT); - - if (!supports(sampleRate, channels)) { - return false; - } - - if (!CodecCapabilities.supportsBitrate(mBitrateRange, format)) { - return false; - } - - // nothing to do for: - // KEY_CHANNEL_MASK: codecs don't get this - // KEY_IS_ADTS: required feature for all AAC decoders - return true; + return mImpl.supportsFormat(format); } } @@ -1898,304 +2301,6 @@ public final class MediaCodecInfo { */ public static final class VideoCapabilities { private static final String TAG = "VideoCapabilities"; - private CodecCapabilities mParent; - private Range<Integer> mBitrateRange; - - private Range<Integer> mHeightRange; - private Range<Integer> mWidthRange; - private Range<Integer> mBlockCountRange; - private Range<Integer> mHorizontalBlockRange; - private Range<Integer> mVerticalBlockRange; - private Range<Rational> mAspectRatioRange; - private Range<Rational> mBlockAspectRatioRange; - private Range<Long> mBlocksPerSecondRange; - private Map<Size, Range<Long>> mMeasuredFrameRates; - private List<PerformancePoint> mPerformancePoints; - private Range<Integer> mFrameRateRange; - - private int mBlockWidth; - private int mBlockHeight; - private int mWidthAlignment; - private int mHeightAlignment; - private int mSmallerDimensionUpperLimit; - - private boolean mAllowMbOverride; // allow XML to override calculated limits - - /** - * Returns the range of supported bitrates in bits/second. - */ - public Range<Integer> getBitrateRange() { - return mBitrateRange; - } - - /** - * Returns the range of supported video widths. - * <p class=note> - * 32-bit processes will not support resolutions larger than 4096x4096 due to - * the limited address space. - */ - public Range<Integer> getSupportedWidths() { - return mWidthRange; - } - - /** - * Returns the range of supported video heights. - * <p class=note> - * 32-bit processes will not support resolutions larger than 4096x4096 due to - * the limited address space. - */ - public Range<Integer> getSupportedHeights() { - return mHeightRange; - } - - /** - * Returns the alignment requirement for video width (in pixels). - * - * This is a power-of-2 value that video width must be a - * multiple of. - */ - public int getWidthAlignment() { - return mWidthAlignment; - } - - /** - * Returns the alignment requirement for video height (in pixels). - * - * This is a power-of-2 value that video height must be a - * multiple of. - */ - public int getHeightAlignment() { - return mHeightAlignment; - } - - /** - * Return the upper limit on the smaller dimension of width or height. - * <p></p> - * Some codecs have a limit on the smaller dimension, whether it be - * the width or the height. E.g. a codec may only be able to handle - * up to 1920x1080 both in landscape and portrait mode (1080x1920). - * In this case the maximum width and height are both 1920, but the - * smaller dimension limit will be 1080. For other codecs, this is - * {@code Math.min(getSupportedWidths().getUpper(), - * getSupportedHeights().getUpper())}. - * - * @hide - */ - public int getSmallerDimensionUpperLimit() { - return mSmallerDimensionUpperLimit; - } - - /** - * Returns the range of supported frame rates. - * <p> - * This is not a performance indicator. Rather, it expresses the - * limits specified in the coding standard, based on the complexities - * of encoding material for later playback at a certain frame rate, - * or the decoding of such material in non-realtime. - */ - public Range<Integer> getSupportedFrameRates() { - return mFrameRateRange; - } - - /** - * Returns the range of supported video widths for a video height. - * @param height the height of the video - */ - public Range<Integer> getSupportedWidthsFor(int height) { - try { - Range<Integer> range = mWidthRange; - if (!mHeightRange.contains(height) - || (height % mHeightAlignment) != 0) { - throw new IllegalArgumentException("unsupported height"); - } - final int heightInBlocks = Utils.divUp(height, mBlockHeight); - - // constrain by block count and by block aspect ratio - final int minWidthInBlocks = Math.max( - Utils.divUp(mBlockCountRange.getLower(), heightInBlocks), - (int)Math.ceil(mBlockAspectRatioRange.getLower().doubleValue() - * heightInBlocks)); - final int maxWidthInBlocks = Math.min( - mBlockCountRange.getUpper() / heightInBlocks, - (int)(mBlockAspectRatioRange.getUpper().doubleValue() - * heightInBlocks)); - range = range.intersect( - (minWidthInBlocks - 1) * mBlockWidth + mWidthAlignment, - maxWidthInBlocks * mBlockWidth); - - // constrain by smaller dimension limit - if (height > mSmallerDimensionUpperLimit) { - range = range.intersect(1, mSmallerDimensionUpperLimit); - } - - // constrain by aspect ratio - range = range.intersect( - (int)Math.ceil(mAspectRatioRange.getLower().doubleValue() - * height), - (int)(mAspectRatioRange.getUpper().doubleValue() * height)); - return range; - } catch (IllegalArgumentException e) { - // height is not supported because there are no suitable widths - Log.v(TAG, "could not get supported widths for " + height); - throw new IllegalArgumentException("unsupported height"); - } - } - - /** - * Returns the range of supported video heights for a video width - * @param width the width of the video - */ - public Range<Integer> getSupportedHeightsFor(int width) { - try { - Range<Integer> range = mHeightRange; - if (!mWidthRange.contains(width) - || (width % mWidthAlignment) != 0) { - throw new IllegalArgumentException("unsupported width"); - } - final int widthInBlocks = Utils.divUp(width, mBlockWidth); - - // constrain by block count and by block aspect ratio - final int minHeightInBlocks = Math.max( - Utils.divUp(mBlockCountRange.getLower(), widthInBlocks), - (int)Math.ceil(widthInBlocks / - mBlockAspectRatioRange.getUpper().doubleValue())); - final int maxHeightInBlocks = Math.min( - mBlockCountRange.getUpper() / widthInBlocks, - (int)(widthInBlocks / - mBlockAspectRatioRange.getLower().doubleValue())); - range = range.intersect( - (minHeightInBlocks - 1) * mBlockHeight + mHeightAlignment, - maxHeightInBlocks * mBlockHeight); - - // constrain by smaller dimension limit - if (width > mSmallerDimensionUpperLimit) { - range = range.intersect(1, mSmallerDimensionUpperLimit); - } - - // constrain by aspect ratio - range = range.intersect( - (int)Math.ceil(width / - mAspectRatioRange.getUpper().doubleValue()), - (int)(width / mAspectRatioRange.getLower().doubleValue())); - return range; - } catch (IllegalArgumentException e) { - // width is not supported because there are no suitable heights - Log.v(TAG, "could not get supported heights for " + width); - throw new IllegalArgumentException("unsupported width"); - } - } - - /** - * Returns the range of supported video frame rates for a video size. - * <p> - * This is not a performance indicator. Rather, it expresses the limits specified in - * the coding standard, based on the complexities of encoding material of a given - * size for later playback at a certain frame rate, or the decoding of such material - * in non-realtime. - - * @param width the width of the video - * @param height the height of the video - */ - public Range<Double> getSupportedFrameRatesFor(int width, int height) { - Range<Integer> range = mHeightRange; - if (!supports(width, height, null)) { - throw new IllegalArgumentException("unsupported size"); - } - final int blockCount = - Utils.divUp(width, mBlockWidth) * Utils.divUp(height, mBlockHeight); - - return Range.create( - Math.max(mBlocksPerSecondRange.getLower() / (double) blockCount, - (double) mFrameRateRange.getLower()), - Math.min(mBlocksPerSecondRange.getUpper() / (double) blockCount, - (double) mFrameRateRange.getUpper())); - } - - private int getBlockCount(int width, int height) { - return Utils.divUp(width, mBlockWidth) * Utils.divUp(height, mBlockHeight); - } - - @NonNull - private Size findClosestSize(int width, int height) { - int targetBlockCount = getBlockCount(width, height); - Size closestSize = null; - int minDiff = Integer.MAX_VALUE; - for (Size size : mMeasuredFrameRates.keySet()) { - int diff = Math.abs(targetBlockCount - - getBlockCount(size.getWidth(), size.getHeight())); - if (diff < minDiff) { - minDiff = diff; - closestSize = size; - } - } - return closestSize; - } - - private Range<Double> estimateFrameRatesFor(int width, int height) { - Size size = findClosestSize(width, height); - Range<Long> range = mMeasuredFrameRates.get(size); - Double ratio = getBlockCount(size.getWidth(), size.getHeight()) - / (double)Math.max(getBlockCount(width, height), 1); - return Range.create(range.getLower() * ratio, range.getUpper() * ratio); - } - - /** - * Returns the range of achievable video frame rates for a video size. - * May return {@code null}, if the codec did not publish any measurement - * data. - * <p> - * This is a performance estimate provided by the device manufacturer based on statistical - * sampling of full-speed decoding and encoding measurements in various configurations - * of common video sizes supported by the codec. As such it should only be used to - * compare individual codecs on the device. The value is not suitable for comparing - * different devices or even different android releases for the same device. - * <p> - * <em>On {@link android.os.Build.VERSION_CODES#M} release</em> the returned range - * corresponds to the fastest frame rates achieved in the tested configurations. As - * such, it should not be used to gauge guaranteed or even average codec performance - * on the device. - * <p> - * <em>On {@link android.os.Build.VERSION_CODES#N} release</em> the returned range - * corresponds closer to sustained performance <em>in tested configurations</em>. - * One can expect to achieve sustained performance higher than the lower limit more than - * 50% of the time, and higher than half of the lower limit at least 90% of the time - * <em>in tested configurations</em>. - * Conversely, one can expect performance lower than twice the upper limit at least - * 90% of the time. - * <p class=note> - * Tested configurations use a single active codec. For use cases where multiple - * codecs are active, applications can expect lower and in most cases significantly lower - * performance. - * <p class=note> - * The returned range value is interpolated from the nearest frame size(s) tested. - * Codec performance is severely impacted by other activity on the device as well - * as environmental factors (such as battery level, temperature or power source), and can - * vary significantly even in a steady environment. - * <p class=note> - * Use this method in cases where only codec performance matters, e.g. to evaluate if - * a codec has any chance of meeting a performance target. Codecs are listed - * in {@link MediaCodecList} in the preferred order as defined by the device - * manufacturer. As such, applications should use the first suitable codec in the - * list to achieve the best balance between power use and performance. - * - * @param width the width of the video - * @param height the height of the video - * - * @throws IllegalArgumentException if the video size is not supported. - */ - @Nullable - public Range<Double> getAchievableFrameRatesFor(int width, int height) { - if (!supports(width, height, null)) { - throw new IllegalArgumentException("unsupported size"); - } - - if (mMeasuredFrameRates == null || mMeasuredFrameRates.size() <= 0) { - Log.w(TAG, "Codec did not publish any measurement data."); - return null; - } - - return estimateFrameRatesFor(width, height); - } /** * Video performance points are a set of standard performance points defined by number of @@ -2225,6 +2330,24 @@ public final class MediaCodecInfo { } /** + * Width in macroblocks. + * + * @hide + */ + /** package private */ int getWidth() { + return mWidth; + } + + /** + * Height in macroblocks. + * + * @hide + */ + /** package private */ int getHeight() { + return mHeight; + } + + /** * Maximum frame rate in frames per second. * * @hide @@ -2244,6 +2367,24 @@ public final class MediaCodecInfo { return mMaxMacroBlockRate; } + /** + * Codec block width in macroblocks. + * + * @hide + */ + /** package private */ int getBlockWidth() { + return mBlockSize.getWidth(); + } + + /** + * Codec block height in macroblocks. + * + * @hide + */ + /** package private */ int getBlockHeight() { + return mBlockSize.getHeight(); + } + /** Convert to a debug string */ public String toString() { int blockWidth = 16 * mBlockSize.getWidth(); @@ -2331,6 +2472,20 @@ public final class MediaCodecInfo { this(width, height, frameRate, frameRate /* maxFrameRate */, new Size(16, 16)); } + /* package private */ PerformancePoint(int width, int height, int maxFrameRate, + long maxMacroBlockRate, int blockSizeWidth, int blockSizeHeight) { + mWidth = width; + mHeight = height; + mMaxFrameRate = maxFrameRate; + mMaxMacroBlockRate = maxMacroBlockRate; + mBlockSize = new Size(blockSizeWidth, blockSizeHeight); + } + + private PerformancePoint(PerformancePoint pp) { + this(pp.mWidth, pp.mHeight, pp.mMaxFrameRate, pp.mMaxMacroBlockRate, + pp.mBlockSize.getWidth(), pp.mBlockSize.getHeight()); + } + /** Saturates a long value to int */ private int saturateLongToInt(long value) { if (value < Integer.MIN_VALUE) { @@ -2384,14 +2539,18 @@ public final class MediaCodecInfo { * @return {@code true} if the performance point covers the other. */ public boolean covers(@NonNull PerformancePoint other) { - // convert performance points to common block size - Size commonSize = getCommonBlockSize(other); - PerformancePoint aligned = new PerformancePoint(this, commonSize); - PerformancePoint otherAligned = new PerformancePoint(other, commonSize); + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + return native_covers(other); + } else { + // convert performance points to common block size + Size commonSize = getCommonBlockSize(other); + PerformancePoint aligned = new PerformancePoint(this, commonSize); + PerformancePoint otherAligned = new PerformancePoint(other, commonSize); - return (aligned.getMaxMacroBlocks() >= otherAligned.getMaxMacroBlocks() - && aligned.mMaxFrameRate >= otherAligned.mMaxFrameRate - && aligned.mMaxMacroBlockRate >= otherAligned.mMaxMacroBlockRate); + return (aligned.getMaxMacroBlocks() >= otherAligned.getMaxMacroBlocks() + && aligned.mMaxFrameRate >= otherAligned.mMaxFrameRate + && aligned.mMaxMacroBlockRate >= otherAligned.mMaxMacroBlockRate); + } } private @NonNull Size getCommonBlockSize(@NonNull PerformancePoint other) { @@ -2405,17 +2564,28 @@ public final class MediaCodecInfo { if (o instanceof PerformancePoint) { // convert performance points to common block size PerformancePoint other = (PerformancePoint)o; - Size commonSize = getCommonBlockSize(other); - PerformancePoint aligned = new PerformancePoint(this, commonSize); - PerformancePoint otherAligned = new PerformancePoint(other, commonSize); + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + return native_equals(other); + } else { + Size commonSize = getCommonBlockSize(other); + PerformancePoint aligned = new PerformancePoint(this, commonSize); + PerformancePoint otherAligned = new PerformancePoint(other, commonSize); - return (aligned.getMaxMacroBlocks() == otherAligned.getMaxMacroBlocks() - && aligned.mMaxFrameRate == otherAligned.mMaxFrameRate - && aligned.mMaxMacroBlockRate == otherAligned.mMaxMacroBlockRate); + return (aligned.getMaxMacroBlocks() == otherAligned.getMaxMacroBlocks() + && aligned.mMaxFrameRate == otherAligned.mMaxFrameRate + && aligned.mMaxMacroBlockRate == otherAligned.mMaxMacroBlockRate); + } } return false; } + private native boolean native_covers(PerformancePoint other); + private native boolean native_equals(PerformancePoint other); + + static { + System.loadLibrary("media_jni"); + } + /** 480p 24fps */ @NonNull public static final PerformancePoint SD_24 = new PerformancePoint(720, 480, 24); @@ -2520,1351 +2690,1870 @@ public final class MediaCodecInfo { public static final PerformancePoint UHD_240 = new PerformancePoint(3840, 2160, 240); } - /** - * Returns the supported performance points. May return {@code null} if the codec did not - * publish any performance point information (e.g. the vendor codecs have not been updated - * to the latest android release). May return an empty list if the codec published that - * if does not guarantee any performance points. - * <p> - * This is a performance guarantee provided by the device manufacturer for hardware codecs - * based on hardware capabilities of the device. - * <p> - * The returned list is sorted first by decreasing number of pixels, then by decreasing - * width, and finally by decreasing frame rate. - * Performance points assume a single active codec. For use cases where multiple - * codecs are active, should use that highest pixel count, and add the frame rates of - * each individual codec. - * <p class=note> - * 32-bit processes will not support resolutions larger than 4096x4096 due to - * the limited address space, but performance points will be presented as is. - * In other words, even though a component publishes a performance point for - * a resolution higher than 4096x4096, it does not mean that the resolution is supported - * for 32-bit processes. - */ - @Nullable - public List<PerformancePoint> getSupportedPerformancePoints() { - return mPerformancePoints; - } + /* package private */ interface VideoCapsIntf { + public Range<Integer> getBitrateRange(); - /** - * Returns whether a given video size ({@code width} and - * {@code height}) and {@code frameRate} combination is supported. - */ - public boolean areSizeAndRateSupported( - int width, int height, double frameRate) { - return supports(width, height, frameRate); - } + public Range<Integer> getSupportedWidths(); - /** - * Returns whether a given video size ({@code width} and - * {@code height}) is supported. - */ - public boolean isSizeSupported(int width, int height) { - return supports(width, height, null); - } + public Range<Integer> getSupportedHeights(); - private boolean supports(Integer width, Integer height, Number rate) { - boolean ok = true; + public int getWidthAlignment(); - if (ok && width != null) { - ok = mWidthRange.contains(width) - && (width % mWidthAlignment == 0); - } - if (ok && height != null) { - ok = mHeightRange.contains(height) - && (height % mHeightAlignment == 0); - } - if (ok && rate != null) { - ok = mFrameRateRange.contains(Utils.intRangeFor(rate.doubleValue())); - } - if (ok && height != null && width != null) { - ok = Math.min(height, width) <= mSmallerDimensionUpperLimit; + public int getHeightAlignment(); - final int widthInBlocks = Utils.divUp(width, mBlockWidth); - final int heightInBlocks = Utils.divUp(height, mBlockHeight); - final int blockCount = widthInBlocks * heightInBlocks; - ok = ok && mBlockCountRange.contains(blockCount) - && mBlockAspectRatioRange.contains( - new Rational(widthInBlocks, heightInBlocks)) - && mAspectRatioRange.contains(new Rational(width, height)); - if (ok && rate != null) { - double blocksPerSec = blockCount * rate.doubleValue(); - ok = mBlocksPerSecondRange.contains( - Utils.longRangeFor(blocksPerSec)); - } - } - return ok; - } + public int getSmallerDimensionUpperLimit(); - /* package private */ - // must not contain KEY_PROFILE - static final Set<String> VIDEO_LEVEL_CRITICAL_FORMAT_KEYS = Set.of( - MediaFormat.KEY_WIDTH, - MediaFormat.KEY_HEIGHT, - MediaFormat.KEY_FRAME_RATE, - MediaFormat.KEY_BIT_RATE, - MediaFormat.KEY_MIME); + public Range<Integer> getSupportedFrameRates(); - /** - * @hide - * @throws java.lang.ClassCastException */ - public boolean supportsFormat(MediaFormat format) { - final Map<String, Object> map = format.getMap(); - Integer width = (Integer)map.get(MediaFormat.KEY_WIDTH); - Integer height = (Integer)map.get(MediaFormat.KEY_HEIGHT); - Number rate = (Number)map.get(MediaFormat.KEY_FRAME_RATE); + public Range<Integer> getSupportedWidthsFor(int height); - if (!supports(width, height, rate)) { - return false; - } + public Range<Integer> getSupportedHeightsFor(int width); - if (!CodecCapabilities.supportsBitrate(mBitrateRange, format)) { - return false; - } + public Range<Double> getSupportedFrameRatesFor(int width, int height); - // we ignore color-format for now as it is not reliably reported by codec - return true; - } + public Range<Double> getAchievableFrameRatesFor(int width, int height); - /* no public constructor */ - private VideoCapabilities() { } + public boolean areSizeAndRateSupported(int width, int height, double frameRate); - /** @hide */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) - public static VideoCapabilities create( - MediaFormat info, CodecCapabilities parent) { - VideoCapabilities caps = new VideoCapabilities(); - caps.init(info, parent); - return caps; - } + public boolean isSizeSupported(int width, int height); - private void init(MediaFormat info, CodecCapabilities parent) { - mParent = parent; - initWithPlatformLimits(); - applyLevelLimits(); - parseFromInfo(info); - updateLimits(); - } + public boolean supportsFormat(MediaFormat format); - /** @hide */ - public Size getBlockSize() { - return new Size(mBlockWidth, mBlockHeight); + public List<PerformancePoint> getSupportedPerformancePoints(); } - /** @hide */ - public Range<Integer> getBlockCountRange() { - return mBlockCountRange; - } + /* package private */ static final class VideoCapsLegacyImpl implements VideoCapsIntf { + /* package private */ + // must not contain KEY_PROFILE + static final Set<String> VIDEO_LEVEL_CRITICAL_FORMAT_KEYS = Set.of( + MediaFormat.KEY_WIDTH, + MediaFormat.KEY_HEIGHT, + MediaFormat.KEY_FRAME_RATE, + MediaFormat.KEY_BIT_RATE, + MediaFormat.KEY_MIME); + + private CodecCapabilities.CodecCapsLegacyImpl mParent; + private Range<Integer> mBitrateRange; + + private Range<Integer> mHeightRange; + private Range<Integer> mWidthRange; + private Range<Integer> mBlockCountRange; + private Range<Integer> mHorizontalBlockRange; + private Range<Integer> mVerticalBlockRange; + private Range<Rational> mAspectRatioRange; + private Range<Rational> mBlockAspectRatioRange; + private Range<Long> mBlocksPerSecondRange; + private Map<Size, Range<Long>> mMeasuredFrameRates; + private List<PerformancePoint> mPerformancePoints; + private Range<Integer> mFrameRateRange; + + private int mBlockWidth; + private int mBlockHeight; + private int mWidthAlignment; + private int mHeightAlignment; + private int mSmallerDimensionUpperLimit; + + private boolean mAllowMbOverride; // allow XML to override calculated limits + + /* no public constructor */ + private VideoCapsLegacyImpl() { } + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + public static VideoCapsLegacyImpl create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + Log.d(TAG, "Legacy implementation is called while native flag is on."); + } - /** @hide */ - public Range<Long> getBlocksPerSecondRange() { - return mBlocksPerSecondRange; - } + VideoCapsLegacyImpl caps = new VideoCapsLegacyImpl(); + caps.init(info, parent); + return caps; + } - /** @hide */ - public Range<Rational> getAspectRatioRange(boolean blocks) { - return blocks ? mBlockAspectRatioRange : mAspectRatioRange; - } + private void init(MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + mParent = parent; + initWithPlatformLimits(); + applyLevelLimits(); + parseFromInfo(info); + updateLimits(); + } - private void initWithPlatformLimits() { - mBitrateRange = BITRATE_RANGE; + public Range<Integer> getBitrateRange() { + return mBitrateRange; + } - mWidthRange = getSizeRange(); - mHeightRange = getSizeRange(); - mFrameRateRange = FRAME_RATE_RANGE; + public Range<Integer> getSupportedWidths() { + return mWidthRange; + } - mHorizontalBlockRange = getSizeRange(); - mVerticalBlockRange = getSizeRange(); + public Range<Integer> getSupportedHeights() { + return mHeightRange; + } - // full positive ranges are supported as these get calculated - mBlockCountRange = POSITIVE_INTEGERS; - mBlocksPerSecondRange = POSITIVE_LONGS; + public int getWidthAlignment() { + return mWidthAlignment; + } - mBlockAspectRatioRange = POSITIVE_RATIONALS; - mAspectRatioRange = POSITIVE_RATIONALS; + public int getHeightAlignment() { + return mHeightAlignment; + } - mWidthAlignment = 1; - mHeightAlignment = 1; - mBlockWidth = 1; - mBlockHeight = 1; - mSmallerDimensionUpperLimit = getSizeRange().getUpper(); - } + /** @hide */ + public int getSmallerDimensionUpperLimit() { + return mSmallerDimensionUpperLimit; + } + + public Range<Integer> getSupportedFrameRates() { + return mFrameRateRange; + } - private @Nullable List<PerformancePoint> getPerformancePoints(Map<String, Object> map) { - Vector<PerformancePoint> ret = new Vector<>(); - final String prefix = "performance-point-"; - Set<String> keys = map.keySet(); - for (String key : keys) { - // looking for: performance-point-WIDTHxHEIGHT-range - if (!key.startsWith(prefix)) { - continue; + public Range<Integer> getSupportedWidthsFor(int height) { + try { + Range<Integer> range = mWidthRange; + if (!mHeightRange.contains(height) + || (height % mHeightAlignment) != 0) { + throw new IllegalArgumentException("unsupported height"); + } + final int heightInBlocks = Utils.divUp(height, mBlockHeight); + + // constrain by block count and by block aspect ratio + final int minWidthInBlocks = Math.max( + Utils.divUp(mBlockCountRange.getLower(), heightInBlocks), + (int) Math.ceil(mBlockAspectRatioRange.getLower().doubleValue() + * heightInBlocks)); + final int maxWidthInBlocks = Math.min( + mBlockCountRange.getUpper() / heightInBlocks, + (int) (mBlockAspectRatioRange.getUpper().doubleValue() + * heightInBlocks)); + range = range.intersect( + (minWidthInBlocks - 1) * mBlockWidth + mWidthAlignment, + maxWidthInBlocks * mBlockWidth); + + // constrain by smaller dimension limit + if (height > mSmallerDimensionUpperLimit) { + range = range.intersect(1, mSmallerDimensionUpperLimit); + } + + // constrain by aspect ratio + range = range.intersect( + (int) Math.ceil(mAspectRatioRange.getLower().doubleValue() + * height), + (int) (mAspectRatioRange.getUpper().doubleValue() * height)); + return range; + } catch (IllegalArgumentException e) { + // height is not supported because there are no suitable widths + Log.v(TAG, "could not get supported widths for " + height); + throw new IllegalArgumentException("unsupported height"); } - String subKey = key.substring(prefix.length()); - if (subKey.equals("none") && ret.size() == 0) { - // This means that component knowingly did not publish performance points. - // This is different from when the component forgot to publish performance - // points. - return Collections.unmodifiableList(ret); + } + + public Range<Integer> getSupportedHeightsFor(int width) { + try { + Range<Integer> range = mHeightRange; + if (!mWidthRange.contains(width) + || (width % mWidthAlignment) != 0) { + throw new IllegalArgumentException("unsupported width"); + } + final int widthInBlocks = Utils.divUp(width, mBlockWidth); + + // constrain by block count and by block aspect ratio + final int minHeightInBlocks = Math.max( + Utils.divUp(mBlockCountRange.getLower(), widthInBlocks), + (int) Math.ceil(widthInBlocks + / mBlockAspectRatioRange.getUpper().doubleValue())); + final int maxHeightInBlocks = Math.min( + mBlockCountRange.getUpper() / widthInBlocks, + (int) (widthInBlocks + / mBlockAspectRatioRange.getLower().doubleValue())); + range = range.intersect( + (minHeightInBlocks - 1) * mBlockHeight + mHeightAlignment, + maxHeightInBlocks * mBlockHeight); + + // constrain by smaller dimension limit + if (width > mSmallerDimensionUpperLimit) { + range = range.intersect(1, mSmallerDimensionUpperLimit); + } + + // constrain by aspect ratio + range = range.intersect( + (int) Math.ceil(width + / mAspectRatioRange.getUpper().doubleValue()), + (int) (width / mAspectRatioRange.getLower().doubleValue())); + return range; + } catch (IllegalArgumentException e) { + // width is not supported because there are no suitable heights + Log.v(TAG, "could not get supported heights for " + width); + throw new IllegalArgumentException("unsupported width"); } - String[] temp = key.split("-"); - if (temp.length != 4) { - continue; + } + + public Range<Double> getSupportedFrameRatesFor(int width, int height) { + Range<Integer> range = mHeightRange; + if (!supports(width, height, null)) { + throw new IllegalArgumentException("unsupported size"); } - String sizeStr = temp[2]; - Size size = Utils.parseSize(sizeStr, null); - if (size == null || size.getWidth() * size.getHeight() <= 0) { - continue; + final int blockCount = + Utils.divUp(width, mBlockWidth) * Utils.divUp(height, mBlockHeight); + + return Range.create( + Math.max(mBlocksPerSecondRange.getLower() / (double) blockCount, + (double) mFrameRateRange.getLower()), + Math.min(mBlocksPerSecondRange.getUpper() / (double) blockCount, + (double) mFrameRateRange.getUpper())); + } + + private int getBlockCount(int width, int height) { + return Utils.divUp(width, mBlockWidth) * Utils.divUp(height, mBlockHeight); + } + + @NonNull + private Size findClosestSize(int width, int height) { + int targetBlockCount = getBlockCount(width, height); + Size closestSize = null; + int minDiff = Integer.MAX_VALUE; + for (Size size : mMeasuredFrameRates.keySet()) { + int diff = Math.abs(targetBlockCount - + getBlockCount(size.getWidth(), size.getHeight())); + if (diff < minDiff) { + minDiff = diff; + closestSize = size; + } } - Range<Long> range = Utils.parseLongRange(map.get(key), null); - if (range == null || range.getLower() < 0 || range.getUpper() < 0) { - continue; + return closestSize; + } + + private Range<Double> estimateFrameRatesFor(int width, int height) { + Size size = findClosestSize(width, height); + Range<Long> range = mMeasuredFrameRates.get(size); + Double ratio = getBlockCount(size.getWidth(), size.getHeight()) + / (double)Math.max(getBlockCount(width, height), 1); + return Range.create(range.getLower() * ratio, range.getUpper() * ratio); + } + + /** @throws IllegalArgumentException if the video size is not supported. */ + @Nullable + public Range<Double> getAchievableFrameRatesFor(int width, int height) { + if (!supports(width, height, null)) { + throw new IllegalArgumentException("unsupported size"); } - PerformancePoint given = new PerformancePoint( - size.getWidth(), size.getHeight(), range.getLower().intValue(), - range.getUpper().intValue(), new Size(mBlockWidth, mBlockHeight)); - PerformancePoint rotated = new PerformancePoint( - size.getHeight(), size.getWidth(), range.getLower().intValue(), - range.getUpper().intValue(), new Size(mBlockWidth, mBlockHeight)); - ret.add(given); - if (!given.covers(rotated)) { - ret.add(rotated); + + if (mMeasuredFrameRates == null || mMeasuredFrameRates.size() <= 0) { + Log.w(TAG, "Codec did not publish any measurement data."); + return null; } + + return estimateFrameRatesFor(width, height); } - // check if the component specified no performance point indication - if (ret.size() == 0) { - return null; + @Nullable + public List<PerformancePoint> getSupportedPerformancePoints() { + return mPerformancePoints; } - // sort reversed by area first, then by frame rate - ret.sort((a, b) -> - -((a.getMaxMacroBlocks() != b.getMaxMacroBlocks()) ? - (a.getMaxMacroBlocks() < b.getMaxMacroBlocks() ? -1 : 1) : - (a.getMaxMacroBlockRate() != b.getMaxMacroBlockRate()) ? - (a.getMaxMacroBlockRate() < b.getMaxMacroBlockRate() ? -1 : 1) : - (a.getMaxFrameRate() != b.getMaxFrameRate()) ? - (a.getMaxFrameRate() < b.getMaxFrameRate() ? -1 : 1) : 0)); + public boolean areSizeAndRateSupported( + int width, int height, double frameRate) { + return supports(width, height, frameRate); + } - return Collections.unmodifiableList(ret); - } + public boolean isSizeSupported(int width, int height) { + return supports(width, height, null); + } + + private boolean supports(Integer width, Integer height, Number rate) { + boolean ok = true; - private Map<Size, Range<Long>> getMeasuredFrameRates(Map<String, Object> map) { - Map<Size, Range<Long>> ret = new HashMap<Size, Range<Long>>(); - final String prefix = "measured-frame-rate-"; - Set<String> keys = map.keySet(); - for (String key : keys) { - // looking for: measured-frame-rate-WIDTHxHEIGHT-range - if (!key.startsWith(prefix)) { - continue; + if (ok && width != null) { + ok = mWidthRange.contains(width) + && (width % mWidthAlignment == 0); } - String subKey = key.substring(prefix.length()); - String[] temp = key.split("-"); - if (temp.length != 5) { - continue; + if (ok && height != null) { + ok = mHeightRange.contains(height) + && (height % mHeightAlignment == 0); } - String sizeStr = temp[3]; - Size size = Utils.parseSize(sizeStr, null); - if (size == null || size.getWidth() * size.getHeight() <= 0) { - continue; + if (ok && rate != null) { + ok = mFrameRateRange.contains(Utils.intRangeFor(rate.doubleValue())); } - Range<Long> range = Utils.parseLongRange(map.get(key), null); - if (range == null || range.getLower() < 0 || range.getUpper() < 0) { - continue; + if (ok && height != null && width != null) { + ok = Math.min(height, width) <= mSmallerDimensionUpperLimit; + + final int widthInBlocks = Utils.divUp(width, mBlockWidth); + final int heightInBlocks = Utils.divUp(height, mBlockHeight); + final int blockCount = widthInBlocks * heightInBlocks; + ok = ok && mBlockCountRange.contains(blockCount) + && mBlockAspectRatioRange.contains( + new Rational(widthInBlocks, heightInBlocks)) + && mAspectRatioRange.contains(new Rational(width, height)); + if (ok && rate != null) { + double blocksPerSec = blockCount * rate.doubleValue(); + ok = mBlocksPerSecondRange.contains( + Utils.longRangeFor(blocksPerSec)); + } } - ret.put(size, range); + return ok; } - return ret; - } - private static Pair<Range<Integer>, Range<Integer>> parseWidthHeightRanges(Object o) { - Pair<Size, Size> range = Utils.parseSizeRange(o); - if (range != null) { - try { - return Pair.create( - Range.create(range.first.getWidth(), range.second.getWidth()), - Range.create(range.first.getHeight(), range.second.getHeight())); - } catch (IllegalArgumentException e) { - Log.w(TAG, "could not parse size range '" + o + "'"); + /** + * @hide + * @throws java.lang.ClassCastException */ + public boolean supportsFormat(MediaFormat format) { + final Map<String, Object> map = format.getMap(); + Integer width = (Integer)map.get(MediaFormat.KEY_WIDTH); + Integer height = (Integer)map.get(MediaFormat.KEY_HEIGHT); + Number rate = (Number)map.get(MediaFormat.KEY_FRAME_RATE); + + if (!supports(width, height, rate)) { + return false; + } + + if (!CodecCapabilities.CodecCapsLegacyImpl.supportsBitrate(mBitrateRange, format)) { + return false; } + + // we ignore color-format for now as it is not reliably reported by codec + return true; } - return null; - } - /** @hide */ - public static int equivalentVP9Level(MediaFormat info) { - final Map<String, Object> map = info.getMap(); - - Size blockSize = Utils.parseSize(map.get("block-size"), new Size(8, 8)); - int BS = blockSize.getWidth() * blockSize.getHeight(); - - Range<Integer> counts = Utils.parseIntRange(map.get("block-count-range"), null); - int FS = counts == null ? 0 : BS * counts.getUpper(); - - Range<Long> blockRates = - Utils.parseLongRange(map.get("blocks-per-second-range"), null); - long SR = blockRates == null ? 0 : BS * blockRates.getUpper(); - - Pair<Range<Integer>, Range<Integer>> dimensionRanges = - parseWidthHeightRanges(map.get("size-range")); - int D = dimensionRanges == null ? 0 : Math.max( - dimensionRanges.first.getUpper(), dimensionRanges.second.getUpper()); - - Range<Integer> bitRates = Utils.parseIntRange(map.get("bitrate-range"), null); - int BR = bitRates == null ? 0 : Utils.divUp(bitRates.getUpper(), 1000); - - if (SR <= 829440 && FS <= 36864 && BR <= 200 && D <= 512) - return CodecProfileLevel.VP9Level1; - if (SR <= 2764800 && FS <= 73728 && BR <= 800 && D <= 768) - return CodecProfileLevel.VP9Level11; - if (SR <= 4608000 && FS <= 122880 && BR <= 1800 && D <= 960) - return CodecProfileLevel.VP9Level2; - if (SR <= 9216000 && FS <= 245760 && BR <= 3600 && D <= 1344) - return CodecProfileLevel.VP9Level21; - if (SR <= 20736000 && FS <= 552960 && BR <= 7200 && D <= 2048) - return CodecProfileLevel.VP9Level3; - if (SR <= 36864000 && FS <= 983040 && BR <= 12000 && D <= 2752) - return CodecProfileLevel.VP9Level31; - if (SR <= 83558400 && FS <= 2228224 && BR <= 18000 && D <= 4160) - return CodecProfileLevel.VP9Level4; - if (SR <= 160432128 && FS <= 2228224 && BR <= 30000 && D <= 4160) - return CodecProfileLevel.VP9Level41; - if (SR <= 311951360 && FS <= 8912896 && BR <= 60000 && D <= 8384) - return CodecProfileLevel.VP9Level5; - if (SR <= 588251136 && FS <= 8912896 && BR <= 120000 && D <= 8384) - return CodecProfileLevel.VP9Level51; - if (SR <= 1176502272 && FS <= 8912896 && BR <= 180000 && D <= 8384) - return CodecProfileLevel.VP9Level52; - if (SR <= 1176502272 && FS <= 35651584 && BR <= 180000 && D <= 16832) - return CodecProfileLevel.VP9Level6; - if (SR <= 2353004544L && FS <= 35651584 && BR <= 240000 && D <= 16832) - return CodecProfileLevel.VP9Level61; - if (SR <= 4706009088L && FS <= 35651584 && BR <= 480000 && D <= 16832) - return CodecProfileLevel.VP9Level62; - // returning largest level - return CodecProfileLevel.VP9Level62; - } + /** @hide */ + public Size getBlockSize() { + return new Size(mBlockWidth, mBlockHeight); + } - private void parseFromInfo(MediaFormat info) { - final Map<String, Object> map = info.getMap(); - Size blockSize = new Size(mBlockWidth, mBlockHeight); - Size alignment = new Size(mWidthAlignment, mHeightAlignment); - Range<Integer> counts = null, widths = null, heights = null; - Range<Integer> frameRates = null, bitRates = null; - Range<Long> blockRates = null; - Range<Rational> ratios = null, blockRatios = null; - - blockSize = Utils.parseSize(map.get("block-size"), blockSize); - alignment = Utils.parseSize(map.get("alignment"), alignment); - counts = Utils.parseIntRange(map.get("block-count-range"), null); - blockRates = - Utils.parseLongRange(map.get("blocks-per-second-range"), null); - mMeasuredFrameRates = getMeasuredFrameRates(map); - mPerformancePoints = getPerformancePoints(map); - Pair<Range<Integer>, Range<Integer>> sizeRanges = - parseWidthHeightRanges(map.get("size-range")); - if (sizeRanges != null) { - widths = sizeRanges.first; - heights = sizeRanges.second; - } - // for now this just means using the smaller max size as 2nd - // upper limit. - // for now we are keeping the profile specific "width/height - // in macroblocks" limits. - if (map.containsKey("feature-can-swap-width-height")) { - if (widths != null) { - mSmallerDimensionUpperLimit = - Math.min(widths.getUpper(), heights.getUpper()); - widths = heights = widths.extend(heights); - } else { - Log.w(TAG, "feature can-swap-width-height is best used with size-range"); - mSmallerDimensionUpperLimit = - Math.min(mWidthRange.getUpper(), mHeightRange.getUpper()); - mWidthRange = mHeightRange = mWidthRange.extend(mHeightRange); - } + /** @hide */ + public Range<Integer> getBlockCountRange() { + return mBlockCountRange; } - ratios = Utils.parseRationalRange( - map.get("block-aspect-ratio-range"), null); - blockRatios = Utils.parseRationalRange( - map.get("pixel-aspect-ratio-range"), null); - frameRates = Utils.parseIntRange(map.get("frame-rate-range"), null); - if (frameRates != null) { - try { - frameRates = frameRates.intersect(FRAME_RATE_RANGE); - } catch (IllegalArgumentException e) { - Log.w(TAG, "frame rate range (" + frameRates - + ") is out of limits: " + FRAME_RATE_RANGE); - frameRates = null; - } + /** @hide */ + public Range<Long> getBlocksPerSecondRange() { + return mBlocksPerSecondRange; } - bitRates = Utils.parseIntRange(map.get("bitrate-range"), null); - if (bitRates != null) { - try { - bitRates = bitRates.intersect(BITRATE_RANGE); - } catch (IllegalArgumentException e) { - Log.w(TAG, "bitrate range (" + bitRates - + ") is out of limits: " + BITRATE_RANGE); - bitRates = null; - } + + /** @hide */ + public Range<Rational> getAspectRatioRange(boolean blocks) { + return blocks ? mBlockAspectRatioRange : mAspectRatioRange; } - checkPowerOfTwo( - blockSize.getWidth(), "block-size width must be power of two"); - checkPowerOfTwo( - blockSize.getHeight(), "block-size height must be power of two"); + private void initWithPlatformLimits() { + mBitrateRange = BITRATE_RANGE; - checkPowerOfTwo( - alignment.getWidth(), "alignment width must be power of two"); - checkPowerOfTwo( - alignment.getHeight(), "alignment height must be power of two"); + mWidthRange = getSizeRange(); + mHeightRange = getSizeRange(); + mFrameRateRange = FRAME_RATE_RANGE; - // update block-size and alignment - applyMacroBlockLimits( - Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, - Long.MAX_VALUE, blockSize.getWidth(), blockSize.getHeight(), - alignment.getWidth(), alignment.getHeight()); + mHorizontalBlockRange = getSizeRange(); + mVerticalBlockRange = getSizeRange(); - if ((mParent.mError & ERROR_UNSUPPORTED) != 0 || mAllowMbOverride) { - // codec supports profiles that we don't know. - // Use supplied values clipped to platform limits - if (widths != null) { - mWidthRange = getSizeRange().intersect(widths); - } - if (heights != null) { - mHeightRange = getSizeRange().intersect(heights); - } - if (counts != null) { - mBlockCountRange = POSITIVE_INTEGERS.intersect( - Utils.factorRange(counts, mBlockWidth * mBlockHeight - / blockSize.getWidth() / blockSize.getHeight())); + // full positive ranges are supported as these get calculated + mBlockCountRange = POSITIVE_INTEGERS; + mBlocksPerSecondRange = POSITIVE_LONGS; + + mBlockAspectRatioRange = POSITIVE_RATIONALS; + mAspectRatioRange = POSITIVE_RATIONALS; + + mWidthAlignment = 1; + mHeightAlignment = 1; + mBlockWidth = 1; + mBlockHeight = 1; + mSmallerDimensionUpperLimit = getSizeRange().getUpper(); + } + + private @Nullable List<PerformancePoint> getPerformancePoints(Map<String, Object> map) { + Vector<PerformancePoint> ret = new Vector<>(); + final String prefix = "performance-point-"; + Set<String> keys = map.keySet(); + for (String key : keys) { + // looking for: performance-point-WIDTHxHEIGHT-range + if (!key.startsWith(prefix)) { + continue; + } + String subKey = key.substring(prefix.length()); + if (subKey.equals("none") && ret.size() == 0) { + // This means that component knowingly did not publish performance points. + // This is different from when the component forgot to publish performance + // points. + return Collections.unmodifiableList(ret); + } + String[] temp = key.split("-"); + if (temp.length != 4) { + continue; + } + String sizeStr = temp[2]; + Size size = Utils.parseSize(sizeStr, null); + if (size == null || size.getWidth() * size.getHeight() <= 0) { + continue; + } + Range<Long> range = Utils.parseLongRange(map.get(key), null); + if (range == null || range.getLower() < 0 || range.getUpper() < 0) { + continue; + } + PerformancePoint given = new PerformancePoint( + size.getWidth(), size.getHeight(), range.getLower().intValue(), + range.getUpper().intValue(), new Size(mBlockWidth, mBlockHeight)); + PerformancePoint rotated = new PerformancePoint( + size.getHeight(), size.getWidth(), range.getLower().intValue(), + range.getUpper().intValue(), new Size(mBlockWidth, mBlockHeight)); + ret.add(given); + if (!given.covers(rotated)) { + ret.add(rotated); + } } - if (blockRates != null) { - mBlocksPerSecondRange = POSITIVE_LONGS.intersect( - Utils.factorRange(blockRates, mBlockWidth * mBlockHeight - / blockSize.getWidth() / blockSize.getHeight())); + + // check if the component specified no performance point indication + if (ret.size() == 0) { + return null; } - if (blockRatios != null) { - mBlockAspectRatioRange = POSITIVE_RATIONALS.intersect( - Utils.scaleRange(blockRatios, - mBlockHeight / blockSize.getHeight(), - mBlockWidth / blockSize.getWidth())); + + // sort reversed by area first, then by frame rate + ret.sort((a, b) -> + -((a.getMaxMacroBlocks() != b.getMaxMacroBlocks()) ? + (a.getMaxMacroBlocks() < b.getMaxMacroBlocks() ? -1 : 1) : + (a.getMaxMacroBlockRate() != b.getMaxMacroBlockRate()) ? + (a.getMaxMacroBlockRate() < b.getMaxMacroBlockRate() ? -1 : 1) : + (a.getMaxFrameRate() != b.getMaxFrameRate()) ? + (a.getMaxFrameRate() < b.getMaxFrameRate() ? -1 : 1) : 0)); + + return Collections.unmodifiableList(ret); + } + + private Map<Size, Range<Long>> getMeasuredFrameRates(Map<String, Object> map) { + Map<Size, Range<Long>> ret = new HashMap<Size, Range<Long>>(); + final String prefix = "measured-frame-rate-"; + Set<String> keys = map.keySet(); + for (String key : keys) { + // looking for: measured-frame-rate-WIDTHxHEIGHT-range + if (!key.startsWith(prefix)) { + continue; + } + String subKey = key.substring(prefix.length()); + String[] temp = key.split("-"); + if (temp.length != 5) { + continue; + } + String sizeStr = temp[3]; + Size size = Utils.parseSize(sizeStr, null); + if (size == null || size.getWidth() * size.getHeight() <= 0) { + continue; + } + Range<Long> range = Utils.parseLongRange(map.get(key), null); + if (range == null || range.getLower() < 0 || range.getUpper() < 0) { + continue; + } + ret.put(size, range); } - if (ratios != null) { - mAspectRatioRange = POSITIVE_RATIONALS.intersect(ratios); + return ret; + } + + private static Pair<Range<Integer>, Range<Integer>> parseWidthHeightRanges(Object o) { + Pair<Size, Size> range = Utils.parseSizeRange(o); + if (range != null) { + try { + return Pair.create( + Range.create(range.first.getWidth(), range.second.getWidth()), + Range.create(range.first.getHeight(), range.second.getHeight())); + } catch (IllegalArgumentException e) { + Log.w(TAG, "could not parse size range '" + o + "'"); + } } - if (frameRates != null) { - mFrameRateRange = FRAME_RATE_RANGE.intersect(frameRates); + return null; + } + + /** @hide */ + public static int equivalentVP9Level(MediaFormat info) { + final Map<String, Object> map = info.getMap(); + + Size blockSize = Utils.parseSize(map.get("block-size"), new Size(8, 8)); + int BS = blockSize.getWidth() * blockSize.getHeight(); + + Range<Integer> counts = Utils.parseIntRange(map.get("block-count-range"), null); + int FS = counts == null ? 0 : BS * counts.getUpper(); + + Range<Long> blockRates = + Utils.parseLongRange(map.get("blocks-per-second-range"), null); + long SR = blockRates == null ? 0 : BS * blockRates.getUpper(); + + Pair<Range<Integer>, Range<Integer>> dimensionRanges = + parseWidthHeightRanges(map.get("size-range")); + int D = dimensionRanges == null ? 0 : Math.max( + dimensionRanges.first.getUpper(), dimensionRanges.second.getUpper()); + + Range<Integer> bitRates = Utils.parseIntRange(map.get("bitrate-range"), null); + int BR = bitRates == null ? 0 : Utils.divUp(bitRates.getUpper(), 1000); + + if (SR <= 829440 && FS <= 36864 && BR <= 200 && D <= 512) + return CodecProfileLevel.VP9Level1; + if (SR <= 2764800 && FS <= 73728 && BR <= 800 && D <= 768) + return CodecProfileLevel.VP9Level11; + if (SR <= 4608000 && FS <= 122880 && BR <= 1800 && D <= 960) + return CodecProfileLevel.VP9Level2; + if (SR <= 9216000 && FS <= 245760 && BR <= 3600 && D <= 1344) + return CodecProfileLevel.VP9Level21; + if (SR <= 20736000 && FS <= 552960 && BR <= 7200 && D <= 2048) + return CodecProfileLevel.VP9Level3; + if (SR <= 36864000 && FS <= 983040 && BR <= 12000 && D <= 2752) + return CodecProfileLevel.VP9Level31; + if (SR <= 83558400 && FS <= 2228224 && BR <= 18000 && D <= 4160) + return CodecProfileLevel.VP9Level4; + if (SR <= 160432128 && FS <= 2228224 && BR <= 30000 && D <= 4160) + return CodecProfileLevel.VP9Level41; + if (SR <= 311951360 && FS <= 8912896 && BR <= 60000 && D <= 8384) + return CodecProfileLevel.VP9Level5; + if (SR <= 588251136 && FS <= 8912896 && BR <= 120000 && D <= 8384) + return CodecProfileLevel.VP9Level51; + if (SR <= 1176502272 && FS <= 8912896 && BR <= 180000 && D <= 8384) + return CodecProfileLevel.VP9Level52; + if (SR <= 1176502272 && FS <= 35651584 && BR <= 180000 && D <= 16832) + return CodecProfileLevel.VP9Level6; + if (SR <= 2353004544L && FS <= 35651584 && BR <= 240000 && D <= 16832) + return CodecProfileLevel.VP9Level61; + if (SR <= 4706009088L && FS <= 35651584 && BR <= 480000 && D <= 16832) + return CodecProfileLevel.VP9Level62; + // returning largest level + return CodecProfileLevel.VP9Level62; + } + + private void parseFromInfo(MediaFormat info) { + final Map<String, Object> map = info.getMap(); + Size blockSize = new Size(mBlockWidth, mBlockHeight); + Size alignment = new Size(mWidthAlignment, mHeightAlignment); + Range<Integer> counts = null, widths = null, heights = null; + Range<Integer> frameRates = null, bitRates = null; + Range<Long> blockRates = null; + Range<Rational> ratios = null, blockRatios = null; + + blockSize = Utils.parseSize(map.get("block-size"), blockSize); + alignment = Utils.parseSize(map.get("alignment"), alignment); + counts = Utils.parseIntRange(map.get("block-count-range"), null); + blockRates = + Utils.parseLongRange(map.get("blocks-per-second-range"), null); + mMeasuredFrameRates = getMeasuredFrameRates(map); + mPerformancePoints = getPerformancePoints(map); + Pair<Range<Integer>, Range<Integer>> sizeRanges = + parseWidthHeightRanges(map.get("size-range")); + if (sizeRanges != null) { + widths = sizeRanges.first; + heights = sizeRanges.second; } - if (bitRates != null) { - // only allow bitrate override if unsupported profiles were encountered - if ((mParent.mError & ERROR_UNSUPPORTED) != 0) { - mBitrateRange = BITRATE_RANGE.intersect(bitRates); + // for now this just means using the smaller max size as 2nd + // upper limit. + // for now we are keeping the profile specific "width/height + // in macroblocks" limits. + if (map.containsKey("feature-can-swap-width-height")) { + if (widths != null) { + mSmallerDimensionUpperLimit = + Math.min(widths.getUpper(), heights.getUpper()); + widths = heights = widths.extend(heights); } else { - mBitrateRange = mBitrateRange.intersect(bitRates); + Log.w(TAG, "feature can-swap-width-height is best used with size-range"); + mSmallerDimensionUpperLimit = + Math.min(mWidthRange.getUpper(), mHeightRange.getUpper()); + mWidthRange = mHeightRange = mWidthRange.extend(mHeightRange); } } - } else { - // no unsupported profile/levels, so restrict values to known limits - if (widths != null) { - mWidthRange = mWidthRange.intersect(widths); - } - if (heights != null) { - mHeightRange = mHeightRange.intersect(heights); - } - if (counts != null) { - mBlockCountRange = mBlockCountRange.intersect( - Utils.factorRange(counts, mBlockWidth * mBlockHeight - / blockSize.getWidth() / blockSize.getHeight())); - } - if (blockRates != null) { - mBlocksPerSecondRange = mBlocksPerSecondRange.intersect( - Utils.factorRange(blockRates, mBlockWidth * mBlockHeight - / blockSize.getWidth() / blockSize.getHeight())); - } - if (blockRatios != null) { - mBlockAspectRatioRange = mBlockAspectRatioRange.intersect( - Utils.scaleRange(blockRatios, - mBlockHeight / blockSize.getHeight(), - mBlockWidth / blockSize.getWidth())); - } - if (ratios != null) { - mAspectRatioRange = mAspectRatioRange.intersect(ratios); - } + + ratios = Utils.parseRationalRange( + map.get("block-aspect-ratio-range"), null); + blockRatios = Utils.parseRationalRange( + map.get("pixel-aspect-ratio-range"), null); + frameRates = Utils.parseIntRange(map.get("frame-rate-range"), null); if (frameRates != null) { - mFrameRateRange = mFrameRateRange.intersect(frameRates); + try { + frameRates = frameRates.intersect(FRAME_RATE_RANGE); + } catch (IllegalArgumentException e) { + Log.w(TAG, "frame rate range (" + frameRates + + ") is out of limits: " + FRAME_RATE_RANGE); + frameRates = null; + } } + bitRates = Utils.parseIntRange(map.get("bitrate-range"), null); if (bitRates != null) { - mBitrateRange = mBitrateRange.intersect(bitRates); + try { + bitRates = bitRates.intersect(BITRATE_RANGE); + } catch (IllegalArgumentException e) { + Log.w(TAG, "bitrate range (" + bitRates + + ") is out of limits: " + BITRATE_RANGE); + bitRates = null; + } } - } - updateLimits(); - } - private void applyBlockLimits( - int blockWidth, int blockHeight, - Range<Integer> counts, Range<Long> rates, Range<Rational> ratios) { - checkPowerOfTwo(blockWidth, "blockWidth must be a power of two"); - checkPowerOfTwo(blockHeight, "blockHeight must be a power of two"); - - final int newBlockWidth = Math.max(blockWidth, mBlockWidth); - final int newBlockHeight = Math.max(blockHeight, mBlockHeight); - - // factor will always be a power-of-2 - int factor = - newBlockWidth * newBlockHeight / mBlockWidth / mBlockHeight; - if (factor != 1) { - mBlockCountRange = Utils.factorRange(mBlockCountRange, factor); - mBlocksPerSecondRange = Utils.factorRange( - mBlocksPerSecondRange, factor); - mBlockAspectRatioRange = Utils.scaleRange( - mBlockAspectRatioRange, - newBlockHeight / mBlockHeight, - newBlockWidth / mBlockWidth); - mHorizontalBlockRange = Utils.factorRange( - mHorizontalBlockRange, newBlockWidth / mBlockWidth); - mVerticalBlockRange = Utils.factorRange( - mVerticalBlockRange, newBlockHeight / mBlockHeight); - } - factor = newBlockWidth * newBlockHeight / blockWidth / blockHeight; - if (factor != 1) { - counts = Utils.factorRange(counts, factor); - rates = Utils.factorRange(rates, factor); - ratios = Utils.scaleRange( - ratios, newBlockHeight / blockHeight, - newBlockWidth / blockWidth); - } - mBlockCountRange = mBlockCountRange.intersect(counts); - mBlocksPerSecondRange = mBlocksPerSecondRange.intersect(rates); - mBlockAspectRatioRange = mBlockAspectRatioRange.intersect(ratios); - mBlockWidth = newBlockWidth; - mBlockHeight = newBlockHeight; - } + checkPowerOfTwo( + blockSize.getWidth(), "block-size width must be power of two"); + checkPowerOfTwo( + blockSize.getHeight(), "block-size height must be power of two"); - private void applyAlignment(int widthAlignment, int heightAlignment) { - checkPowerOfTwo(widthAlignment, "widthAlignment must be a power of two"); - checkPowerOfTwo(heightAlignment, "heightAlignment must be a power of two"); + checkPowerOfTwo( + alignment.getWidth(), "alignment width must be power of two"); + checkPowerOfTwo( + alignment.getHeight(), "alignment height must be power of two"); - if (widthAlignment > mBlockWidth || heightAlignment > mBlockHeight) { - // maintain assumption that 0 < alignment <= block-size - applyBlockLimits( - Math.max(widthAlignment, mBlockWidth), - Math.max(heightAlignment, mBlockHeight), - POSITIVE_INTEGERS, POSITIVE_LONGS, POSITIVE_RATIONALS); + // update block-size and alignment + applyMacroBlockLimits( + Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, + Long.MAX_VALUE, blockSize.getWidth(), blockSize.getHeight(), + alignment.getWidth(), alignment.getHeight()); + + if ((mParent.mError & ERROR_UNSUPPORTED) != 0 || mAllowMbOverride) { + // codec supports profiles that we don't know. + // Use supplied values clipped to platform limits + if (widths != null) { + mWidthRange = getSizeRange().intersect(widths); + } + if (heights != null) { + mHeightRange = getSizeRange().intersect(heights); + } + if (counts != null) { + mBlockCountRange = POSITIVE_INTEGERS.intersect( + Utils.factorRange(counts, mBlockWidth * mBlockHeight + / blockSize.getWidth() / blockSize.getHeight())); + } + if (blockRates != null) { + mBlocksPerSecondRange = POSITIVE_LONGS.intersect( + Utils.factorRange(blockRates, mBlockWidth * mBlockHeight + / blockSize.getWidth() / blockSize.getHeight())); + } + if (blockRatios != null) { + mBlockAspectRatioRange = POSITIVE_RATIONALS.intersect( + Utils.scaleRange(blockRatios, + mBlockHeight / blockSize.getHeight(), + mBlockWidth / blockSize.getWidth())); + } + if (ratios != null) { + mAspectRatioRange = POSITIVE_RATIONALS.intersect(ratios); + } + if (frameRates != null) { + mFrameRateRange = FRAME_RATE_RANGE.intersect(frameRates); + } + if (bitRates != null) { + // only allow bitrate override if unsupported profiles were encountered + if ((mParent.mError & ERROR_UNSUPPORTED) != 0) { + mBitrateRange = BITRATE_RANGE.intersect(bitRates); + } else { + mBitrateRange = mBitrateRange.intersect(bitRates); + } + } + } else { + // no unsupported profile/levels, so restrict values to known limits + if (widths != null) { + mWidthRange = mWidthRange.intersect(widths); + } + if (heights != null) { + mHeightRange = mHeightRange.intersect(heights); + } + if (counts != null) { + mBlockCountRange = mBlockCountRange.intersect( + Utils.factorRange(counts, mBlockWidth * mBlockHeight + / blockSize.getWidth() / blockSize.getHeight())); + } + if (blockRates != null) { + mBlocksPerSecondRange = mBlocksPerSecondRange.intersect( + Utils.factorRange(blockRates, mBlockWidth * mBlockHeight + / blockSize.getWidth() / blockSize.getHeight())); + } + if (blockRatios != null) { + mBlockAspectRatioRange = mBlockAspectRatioRange.intersect( + Utils.scaleRange(blockRatios, + mBlockHeight / blockSize.getHeight(), + mBlockWidth / blockSize.getWidth())); + } + if (ratios != null) { + mAspectRatioRange = mAspectRatioRange.intersect(ratios); + } + if (frameRates != null) { + mFrameRateRange = mFrameRateRange.intersect(frameRates); + } + if (bitRates != null) { + mBitrateRange = mBitrateRange.intersect(bitRates); + } + } + updateLimits(); } - mWidthAlignment = Math.max(widthAlignment, mWidthAlignment); - mHeightAlignment = Math.max(heightAlignment, mHeightAlignment); + private void applyBlockLimits( + int blockWidth, int blockHeight, + Range<Integer> counts, Range<Long> rates, Range<Rational> ratios) { + checkPowerOfTwo(blockWidth, "blockWidth must be a power of two"); + checkPowerOfTwo(blockHeight, "blockHeight must be a power of two"); + + final int newBlockWidth = Math.max(blockWidth, mBlockWidth); + final int newBlockHeight = Math.max(blockHeight, mBlockHeight); + + // factor will always be a power-of-2 + int factor = + newBlockWidth * newBlockHeight / mBlockWidth / mBlockHeight; + if (factor != 1) { + mBlockCountRange = Utils.factorRange(mBlockCountRange, factor); + mBlocksPerSecondRange = Utils.factorRange( + mBlocksPerSecondRange, factor); + mBlockAspectRatioRange = Utils.scaleRange( + mBlockAspectRatioRange, + newBlockHeight / mBlockHeight, + newBlockWidth / mBlockWidth); + mHorizontalBlockRange = Utils.factorRange( + mHorizontalBlockRange, newBlockWidth / mBlockWidth); + mVerticalBlockRange = Utils.factorRange( + mVerticalBlockRange, newBlockHeight / mBlockHeight); + } + factor = newBlockWidth * newBlockHeight / blockWidth / blockHeight; + if (factor != 1) { + counts = Utils.factorRange(counts, factor); + rates = Utils.factorRange(rates, factor); + ratios = Utils.scaleRange( + ratios, newBlockHeight / blockHeight, + newBlockWidth / blockWidth); + } + mBlockCountRange = mBlockCountRange.intersect(counts); + mBlocksPerSecondRange = mBlocksPerSecondRange.intersect(rates); + mBlockAspectRatioRange = mBlockAspectRatioRange.intersect(ratios); + mBlockWidth = newBlockWidth; + mBlockHeight = newBlockHeight; + } - mWidthRange = Utils.alignRange(mWidthRange, mWidthAlignment); - mHeightRange = Utils.alignRange(mHeightRange, mHeightAlignment); - } + private void applyAlignment(int widthAlignment, int heightAlignment) { + checkPowerOfTwo(widthAlignment, "widthAlignment must be a power of two"); + checkPowerOfTwo(heightAlignment, "heightAlignment must be a power of two"); - private void updateLimits() { - // pixels -> blocks <- counts - mHorizontalBlockRange = mHorizontalBlockRange.intersect( - Utils.factorRange(mWidthRange, mBlockWidth)); - mHorizontalBlockRange = mHorizontalBlockRange.intersect( - Range.create( - mBlockCountRange.getLower() / mVerticalBlockRange.getUpper(), - mBlockCountRange.getUpper() / mVerticalBlockRange.getLower())); - mVerticalBlockRange = mVerticalBlockRange.intersect( - Utils.factorRange(mHeightRange, mBlockHeight)); - mVerticalBlockRange = mVerticalBlockRange.intersect( - Range.create( - mBlockCountRange.getLower() / mHorizontalBlockRange.getUpper(), - mBlockCountRange.getUpper() / mHorizontalBlockRange.getLower())); - mBlockCountRange = mBlockCountRange.intersect( - Range.create( - mHorizontalBlockRange.getLower() - * mVerticalBlockRange.getLower(), - mHorizontalBlockRange.getUpper() - * mVerticalBlockRange.getUpper())); - mBlockAspectRatioRange = mBlockAspectRatioRange.intersect( - new Rational( - mHorizontalBlockRange.getLower(), mVerticalBlockRange.getUpper()), - new Rational( - mHorizontalBlockRange.getUpper(), mVerticalBlockRange.getLower())); - - // blocks -> pixels - mWidthRange = mWidthRange.intersect( - (mHorizontalBlockRange.getLower() - 1) * mBlockWidth + mWidthAlignment, - mHorizontalBlockRange.getUpper() * mBlockWidth); - mHeightRange = mHeightRange.intersect( - (mVerticalBlockRange.getLower() - 1) * mBlockHeight + mHeightAlignment, - mVerticalBlockRange.getUpper() * mBlockHeight); - mAspectRatioRange = mAspectRatioRange.intersect( - new Rational(mWidthRange.getLower(), mHeightRange.getUpper()), - new Rational(mWidthRange.getUpper(), mHeightRange.getLower())); - - mSmallerDimensionUpperLimit = Math.min( - mSmallerDimensionUpperLimit, - Math.min(mWidthRange.getUpper(), mHeightRange.getUpper())); - - // blocks -> rate - mBlocksPerSecondRange = mBlocksPerSecondRange.intersect( - mBlockCountRange.getLower() * (long)mFrameRateRange.getLower(), - mBlockCountRange.getUpper() * (long)mFrameRateRange.getUpper()); - mFrameRateRange = mFrameRateRange.intersect( - (int)(mBlocksPerSecondRange.getLower() - / mBlockCountRange.getUpper()), - (int)(mBlocksPerSecondRange.getUpper() - / (double)mBlockCountRange.getLower())); - } + if (widthAlignment > mBlockWidth || heightAlignment > mBlockHeight) { + // maintain assumption that 0 < alignment <= block-size + applyBlockLimits( + Math.max(widthAlignment, mBlockWidth), + Math.max(heightAlignment, mBlockHeight), + POSITIVE_INTEGERS, POSITIVE_LONGS, POSITIVE_RATIONALS); + } - private void applyMacroBlockLimits( - int maxHorizontalBlocks, int maxVerticalBlocks, - int maxBlocks, long maxBlocksPerSecond, - int blockWidth, int blockHeight, - int widthAlignment, int heightAlignment) { - applyMacroBlockLimits( - 1 /* minHorizontalBlocks */, 1 /* minVerticalBlocks */, - maxHorizontalBlocks, maxVerticalBlocks, - maxBlocks, maxBlocksPerSecond, - blockWidth, blockHeight, widthAlignment, heightAlignment); - } + mWidthAlignment = Math.max(widthAlignment, mWidthAlignment); + mHeightAlignment = Math.max(heightAlignment, mHeightAlignment); - private void applyMacroBlockLimits( - int minHorizontalBlocks, int minVerticalBlocks, - int maxHorizontalBlocks, int maxVerticalBlocks, - int maxBlocks, long maxBlocksPerSecond, - int blockWidth, int blockHeight, - int widthAlignment, int heightAlignment) { - applyAlignment(widthAlignment, heightAlignment); - applyBlockLimits( - blockWidth, blockHeight, Range.create(1, maxBlocks), - Range.create(1L, maxBlocksPerSecond), - Range.create( - new Rational(1, maxVerticalBlocks), - new Rational(maxHorizontalBlocks, 1))); - mHorizontalBlockRange = - mHorizontalBlockRange.intersect( - Utils.divUp(minHorizontalBlocks, (mBlockWidth / blockWidth)), - maxHorizontalBlocks / (mBlockWidth / blockWidth)); - mVerticalBlockRange = - mVerticalBlockRange.intersect( - Utils.divUp(minVerticalBlocks, (mBlockHeight / blockHeight)), - maxVerticalBlocks / (mBlockHeight / blockHeight)); - } + mWidthRange = Utils.alignRange(mWidthRange, mWidthAlignment); + mHeightRange = Utils.alignRange(mHeightRange, mHeightAlignment); + } - private void applyLevelLimits() { - long maxBlocksPerSecond = 0; - int maxBlocks = 0; - int maxBps = 0; - int maxDPBBlocks = 0; - - int errors = ERROR_NONE_SUPPORTED; - CodecProfileLevel[] profileLevels = mParent.profileLevels; - String mime = mParent.getMimeType(); - - if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AVC)) { - maxBlocks = 99; - maxBlocksPerSecond = 1485; - maxBps = 64000; - maxDPBBlocks = 396; - for (CodecProfileLevel profileLevel: profileLevels) { - int MBPS = 0, FS = 0, BR = 0, DPB = 0; - boolean supported = true; - switch (profileLevel.level) { - case CodecProfileLevel.AVCLevel1: - MBPS = 1485; FS = 99; BR = 64; DPB = 396; break; - case CodecProfileLevel.AVCLevel1b: - MBPS = 1485; FS = 99; BR = 128; DPB = 396; break; - case CodecProfileLevel.AVCLevel11: - MBPS = 3000; FS = 396; BR = 192; DPB = 900; break; - case CodecProfileLevel.AVCLevel12: - MBPS = 6000; FS = 396; BR = 384; DPB = 2376; break; - case CodecProfileLevel.AVCLevel13: - MBPS = 11880; FS = 396; BR = 768; DPB = 2376; break; - case CodecProfileLevel.AVCLevel2: - MBPS = 11880; FS = 396; BR = 2000; DPB = 2376; break; - case CodecProfileLevel.AVCLevel21: - MBPS = 19800; FS = 792; BR = 4000; DPB = 4752; break; - case CodecProfileLevel.AVCLevel22: - MBPS = 20250; FS = 1620; BR = 4000; DPB = 8100; break; - case CodecProfileLevel.AVCLevel3: - MBPS = 40500; FS = 1620; BR = 10000; DPB = 8100; break; - case CodecProfileLevel.AVCLevel31: - MBPS = 108000; FS = 3600; BR = 14000; DPB = 18000; break; - case CodecProfileLevel.AVCLevel32: - MBPS = 216000; FS = 5120; BR = 20000; DPB = 20480; break; - case CodecProfileLevel.AVCLevel4: - MBPS = 245760; FS = 8192; BR = 20000; DPB = 32768; break; - case CodecProfileLevel.AVCLevel41: - MBPS = 245760; FS = 8192; BR = 50000; DPB = 32768; break; - case CodecProfileLevel.AVCLevel42: - MBPS = 522240; FS = 8704; BR = 50000; DPB = 34816; break; - case CodecProfileLevel.AVCLevel5: - MBPS = 589824; FS = 22080; BR = 135000; DPB = 110400; break; - case CodecProfileLevel.AVCLevel51: - MBPS = 983040; FS = 36864; BR = 240000; DPB = 184320; break; - case CodecProfileLevel.AVCLevel52: - MBPS = 2073600; FS = 36864; BR = 240000; DPB = 184320; break; - case CodecProfileLevel.AVCLevel6: - MBPS = 4177920; FS = 139264; BR = 240000; DPB = 696320; break; - case CodecProfileLevel.AVCLevel61: - MBPS = 8355840; FS = 139264; BR = 480000; DPB = 696320; break; - case CodecProfileLevel.AVCLevel62: - MBPS = 16711680; FS = 139264; BR = 800000; DPB = 696320; break; - default: - Log.w(TAG, "Unrecognized level " - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - switch (profileLevel.profile) { - case CodecProfileLevel.AVCProfileConstrainedHigh: - case CodecProfileLevel.AVCProfileHigh: - BR *= 1250; break; - case CodecProfileLevel.AVCProfileHigh10: - BR *= 3000; break; - case CodecProfileLevel.AVCProfileExtended: - case CodecProfileLevel.AVCProfileHigh422: - case CodecProfileLevel.AVCProfileHigh444: - Log.w(TAG, "Unsupported profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNSUPPORTED; - supported = false; - // fall through - treat as base profile - case CodecProfileLevel.AVCProfileConstrainedBaseline: - case CodecProfileLevel.AVCProfileBaseline: - case CodecProfileLevel.AVCProfileMain: - BR *= 1000; break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - BR *= 1000; - } - if (supported) { - errors &= ~ERROR_NONE_SUPPORTED; - } - maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR, maxBps); - maxDPBBlocks = Math.max(maxDPBBlocks, DPB); - } + private void updateLimits() { + // pixels -> blocks <- counts + mHorizontalBlockRange = mHorizontalBlockRange.intersect( + Utils.factorRange(mWidthRange, mBlockWidth)); + mHorizontalBlockRange = mHorizontalBlockRange.intersect( + Range.create( + mBlockCountRange.getLower() / mVerticalBlockRange.getUpper(), + mBlockCountRange.getUpper() / mVerticalBlockRange.getLower())); + mVerticalBlockRange = mVerticalBlockRange.intersect( + Utils.factorRange(mHeightRange, mBlockHeight)); + mVerticalBlockRange = mVerticalBlockRange.intersect( + Range.create( + mBlockCountRange.getLower() / mHorizontalBlockRange.getUpper(), + mBlockCountRange.getUpper() / mHorizontalBlockRange.getLower())); + mBlockCountRange = mBlockCountRange.intersect( + Range.create( + mHorizontalBlockRange.getLower() + * mVerticalBlockRange.getLower(), + mHorizontalBlockRange.getUpper() + * mVerticalBlockRange.getUpper())); + mBlockAspectRatioRange = mBlockAspectRatioRange.intersect( + new Rational( + mHorizontalBlockRange.getLower(), mVerticalBlockRange.getUpper()), + new Rational( + mHorizontalBlockRange.getUpper(), mVerticalBlockRange.getLower())); + + // blocks -> pixels + mWidthRange = mWidthRange.intersect( + (mHorizontalBlockRange.getLower() - 1) * mBlockWidth + mWidthAlignment, + mHorizontalBlockRange.getUpper() * mBlockWidth); + mHeightRange = mHeightRange.intersect( + (mVerticalBlockRange.getLower() - 1) * mBlockHeight + mHeightAlignment, + mVerticalBlockRange.getUpper() * mBlockHeight); + mAspectRatioRange = mAspectRatioRange.intersect( + new Rational(mWidthRange.getLower(), mHeightRange.getUpper()), + new Rational(mWidthRange.getUpper(), mHeightRange.getLower())); + + mSmallerDimensionUpperLimit = Math.min( + mSmallerDimensionUpperLimit, + Math.min(mWidthRange.getUpper(), mHeightRange.getUpper())); + + // blocks -> rate + mBlocksPerSecondRange = mBlocksPerSecondRange.intersect( + mBlockCountRange.getLower() * (long)mFrameRateRange.getLower(), + mBlockCountRange.getUpper() * (long)mFrameRateRange.getUpper()); + mFrameRateRange = mFrameRateRange.intersect( + (int)(mBlocksPerSecondRange.getLower() + / mBlockCountRange.getUpper()), + (int)(mBlocksPerSecondRange.getUpper() + / (double)mBlockCountRange.getLower())); + } - int maxLengthInBlocks = (int)(Math.sqrt(maxBlocks * 8)); + private void applyMacroBlockLimits( + int maxHorizontalBlocks, int maxVerticalBlocks, + int maxBlocks, long maxBlocksPerSecond, + int blockWidth, int blockHeight, + int widthAlignment, int heightAlignment) { applyMacroBlockLimits( - maxLengthInBlocks, maxLengthInBlocks, + 1 /* minHorizontalBlocks */, 1 /* minVerticalBlocks */, + maxHorizontalBlocks, maxVerticalBlocks, maxBlocks, maxBlocksPerSecond, - 16 /* blockWidth */, 16 /* blockHeight */, - 1 /* widthAlignment */, 1 /* heightAlignment */); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG2)) { - int maxWidth = 11, maxHeight = 9, maxRate = 15; - maxBlocks = 99; - maxBlocksPerSecond = 1485; - maxBps = 64000; - for (CodecProfileLevel profileLevel: profileLevels) { - int MBPS = 0, FS = 0, BR = 0, FR = 0, W = 0, H = 0; - boolean supported = true; - switch (profileLevel.profile) { - case CodecProfileLevel.MPEG2ProfileSimple: - switch (profileLevel.level) { - case CodecProfileLevel.MPEG2LevelML: - FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 15000; break; - default: - Log.w(TAG, "Unrecognized profile/level " - + profileLevel.profile + "/" - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - break; - case CodecProfileLevel.MPEG2ProfileMain: - switch (profileLevel.level) { - case CodecProfileLevel.MPEG2LevelLL: - FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 4000; break; - case CodecProfileLevel.MPEG2LevelML: - FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 15000; break; - case CodecProfileLevel.MPEG2LevelH14: - FR = 60; W = 90; H = 68; MBPS = 183600; FS = 6120; BR = 60000; break; - case CodecProfileLevel.MPEG2LevelHL: - FR = 60; W = 120; H = 68; MBPS = 244800; FS = 8160; BR = 80000; break; - case CodecProfileLevel.MPEG2LevelHP: - FR = 60; W = 120; H = 68; MBPS = 489600; FS = 8160; BR = 80000; break; - default: - Log.w(TAG, "Unrecognized profile/level " - + profileLevel.profile + "/" - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - break; - case CodecProfileLevel.MPEG2Profile422: - case CodecProfileLevel.MPEG2ProfileSNR: - case CodecProfileLevel.MPEG2ProfileSpatial: - case CodecProfileLevel.MPEG2ProfileHigh: - Log.i(TAG, "Unsupported profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNSUPPORTED; - supported = false; - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + blockWidth, blockHeight, widthAlignment, heightAlignment); + } + + private void applyMacroBlockLimits( + int minHorizontalBlocks, int minVerticalBlocks, + int maxHorizontalBlocks, int maxVerticalBlocks, + int maxBlocks, long maxBlocksPerSecond, + int blockWidth, int blockHeight, + int widthAlignment, int heightAlignment) { + applyAlignment(widthAlignment, heightAlignment); + applyBlockLimits( + blockWidth, blockHeight, Range.create(1, maxBlocks), + Range.create(1L, maxBlocksPerSecond), + Range.create( + new Rational(1, maxVerticalBlocks), + new Rational(maxHorizontalBlocks, 1))); + mHorizontalBlockRange = + mHorizontalBlockRange.intersect( + Utils.divUp(minHorizontalBlocks, (mBlockWidth / blockWidth)), + maxHorizontalBlocks / (mBlockWidth / blockWidth)); + mVerticalBlockRange = + mVerticalBlockRange.intersect( + Utils.divUp(minVerticalBlocks, (mBlockHeight / blockHeight)), + maxVerticalBlocks / (mBlockHeight / blockHeight)); + } + + private void applyLevelLimits() { + long maxBlocksPerSecond = 0; + int maxBlocks = 0; + int maxBps = 0; + int maxDPBBlocks = 0; + + int errors = ERROR_NONE_SUPPORTED; + CodecProfileLevel[] profileLevels = mParent.getProfileLevels(); + String mime = mParent.getMimeType(); + + if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AVC)) { + maxBlocks = 99; + maxBlocksPerSecond = 1485; + maxBps = 64000; + maxDPBBlocks = 396; + for (CodecProfileLevel profileLevel: profileLevels) { + int MBPS = 0, FS = 0, BR = 0, DPB = 0; + boolean supported = true; + switch (profileLevel.level) { + case CodecProfileLevel.AVCLevel1: + MBPS = 1485; FS = 99; BR = 64; DPB = 396; break; + case CodecProfileLevel.AVCLevel1b: + MBPS = 1485; FS = 99; BR = 128; DPB = 396; break; + case CodecProfileLevel.AVCLevel11: + MBPS = 3000; FS = 396; BR = 192; DPB = 900; break; + case CodecProfileLevel.AVCLevel12: + MBPS = 6000; FS = 396; BR = 384; DPB = 2376; break; + case CodecProfileLevel.AVCLevel13: + MBPS = 11880; FS = 396; BR = 768; DPB = 2376; break; + case CodecProfileLevel.AVCLevel2: + MBPS = 11880; FS = 396; BR = 2000; DPB = 2376; break; + case CodecProfileLevel.AVCLevel21: + MBPS = 19800; FS = 792; BR = 4000; DPB = 4752; break; + case CodecProfileLevel.AVCLevel22: + MBPS = 20250; FS = 1620; BR = 4000; DPB = 8100; break; + case CodecProfileLevel.AVCLevel3: + MBPS = 40500; FS = 1620; BR = 10000; DPB = 8100; break; + case CodecProfileLevel.AVCLevel31: + MBPS = 108000; FS = 3600; BR = 14000; DPB = 18000; break; + case CodecProfileLevel.AVCLevel32: + MBPS = 216000; FS = 5120; BR = 20000; DPB = 20480; break; + case CodecProfileLevel.AVCLevel4: + MBPS = 245760; FS = 8192; BR = 20000; DPB = 32768; break; + case CodecProfileLevel.AVCLevel41: + MBPS = 245760; FS = 8192; BR = 50000; DPB = 32768; break; + case CodecProfileLevel.AVCLevel42: + MBPS = 522240; FS = 8704; BR = 50000; DPB = 34816; break; + case CodecProfileLevel.AVCLevel5: + MBPS = 589824; FS = 22080; BR = 135000; DPB = 110400; break; + case CodecProfileLevel.AVCLevel51: + MBPS = 983040; FS = 36864; BR = 240000; DPB = 184320; break; + case CodecProfileLevel.AVCLevel52: + MBPS = 2073600; FS = 36864; BR = 240000; DPB = 184320; break; + case CodecProfileLevel.AVCLevel6: + MBPS = 4177920; FS = 139264; BR = 240000; DPB = 696320; break; + case CodecProfileLevel.AVCLevel61: + MBPS = 8355840; FS = 139264; BR = 480000; DPB = 696320; break; + case CodecProfileLevel.AVCLevel62: + MBPS = 16711680; FS = 139264; BR = 800000; DPB = 696320; break; + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.AVCProfileConstrainedHigh: + case CodecProfileLevel.AVCProfileHigh: + BR *= 1250; break; + case CodecProfileLevel.AVCProfileHigh10: + BR *= 3000; break; + case CodecProfileLevel.AVCProfileExtended: + case CodecProfileLevel.AVCProfileHigh422: + case CodecProfileLevel.AVCProfileHigh444: + Log.w(TAG, "Unsupported profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNSUPPORTED; + supported = false; + // fall through - treat as base profile + case CodecProfileLevel.AVCProfileConstrainedBaseline: + case CodecProfileLevel.AVCProfileBaseline: + case CodecProfileLevel.AVCProfileMain: + BR *= 1000; break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + BR *= 1000; + } + if (supported) { + errors &= ~ERROR_NONE_SUPPORTED; + } + maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR, maxBps); + maxDPBBlocks = Math.max(maxDPBBlocks, DPB); } - if (supported) { - errors &= ~ERROR_NONE_SUPPORTED; + + int maxLengthInBlocks = (int)(Math.sqrt(maxBlocks * 8)); + applyMacroBlockLimits( + maxLengthInBlocks, maxLengthInBlocks, + maxBlocks, maxBlocksPerSecond, + 16 /* blockWidth */, 16 /* blockHeight */, + 1 /* widthAlignment */, 1 /* heightAlignment */); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG2)) { + int maxWidth = 11, maxHeight = 9, maxRate = 15; + maxBlocks = 99; + maxBlocksPerSecond = 1485; + maxBps = 64000; + for (CodecProfileLevel profileLevel: profileLevels) { + int MBPS = 0, FS = 0, BR = 0, FR = 0, W = 0, H = 0; + boolean supported = true; + switch (profileLevel.profile) { + case CodecProfileLevel.MPEG2ProfileSimple: + switch (profileLevel.level) { + case CodecProfileLevel.MPEG2LevelML: + FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 15000; break; + default: + Log.w(TAG, "Unrecognized profile/level " + + profileLevel.profile + "/" + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + break; + case CodecProfileLevel.MPEG2ProfileMain: + switch (profileLevel.level) { + case CodecProfileLevel.MPEG2LevelLL: + FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 4000; break; + case CodecProfileLevel.MPEG2LevelML: + FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 15000; break; + case CodecProfileLevel.MPEG2LevelH14: + FR = 60; W = 90; H = 68; MBPS = 183600; FS = 6120; BR = 60000; break; + case CodecProfileLevel.MPEG2LevelHL: + FR = 60; W = 120; H = 68; MBPS = 244800; FS = 8160; BR = 80000; break; + case CodecProfileLevel.MPEG2LevelHP: + FR = 60; W = 120; H = 68; MBPS = 489600; FS = 8160; BR = 80000; break; + default: + Log.w(TAG, "Unrecognized profile/level " + + profileLevel.profile + "/" + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + break; + case CodecProfileLevel.MPEG2Profile422: + case CodecProfileLevel.MPEG2ProfileSNR: + case CodecProfileLevel.MPEG2ProfileSpatial: + case CodecProfileLevel.MPEG2ProfileHigh: + Log.i(TAG, "Unsupported profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNSUPPORTED; + supported = false; + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + if (supported) { + errors &= ~ERROR_NONE_SUPPORTED; + } + maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR * 1000, maxBps); + maxWidth = Math.max(W, maxWidth); + maxHeight = Math.max(H, maxHeight); + maxRate = Math.max(FR, maxRate); } - maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR * 1000, maxBps); - maxWidth = Math.max(W, maxWidth); - maxHeight = Math.max(H, maxHeight); - maxRate = Math.max(FR, maxRate); - } - applyMacroBlockLimits(maxWidth, maxHeight, - maxBlocks, maxBlocksPerSecond, - 16 /* blockWidth */, 16 /* blockHeight */, - 1 /* widthAlignment */, 1 /* heightAlignment */); - mFrameRateRange = mFrameRateRange.intersect(12, maxRate); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG4)) { - int maxWidth = 11, maxHeight = 9, maxRate = 15; - maxBlocks = 99; - maxBlocksPerSecond = 1485; - maxBps = 64000; - for (CodecProfileLevel profileLevel: profileLevels) { - int MBPS = 0, FS = 0, BR = 0, FR = 0, W = 0, H = 0; - boolean strict = false; // true: W, H and FR are individual max limits - boolean supported = true; - switch (profileLevel.profile) { - case CodecProfileLevel.MPEG4ProfileSimple: - switch (profileLevel.level) { - case CodecProfileLevel.MPEG4Level0: - strict = true; - FR = 15; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 64; break; - case CodecProfileLevel.MPEG4Level1: - FR = 30; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 64; break; - case CodecProfileLevel.MPEG4Level0b: - strict = true; - FR = 15; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 128; break; - case CodecProfileLevel.MPEG4Level2: - FR = 30; W = 22; H = 18; MBPS = 5940; FS = 396; BR = 128; break; - case CodecProfileLevel.MPEG4Level3: - FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 384; break; - case CodecProfileLevel.MPEG4Level4a: - FR = 30; W = 40; H = 30; MBPS = 36000; FS = 1200; BR = 4000; break; - case CodecProfileLevel.MPEG4Level5: - FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 8000; break; - case CodecProfileLevel.MPEG4Level6: - FR = 30; W = 80; H = 45; MBPS = 108000; FS = 3600; BR = 12000; break; - default: - Log.w(TAG, "Unrecognized profile/level " - + profileLevel.profile + "/" - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - break; - case CodecProfileLevel.MPEG4ProfileAdvancedSimple: - switch (profileLevel.level) { - case CodecProfileLevel.MPEG4Level0: - case CodecProfileLevel.MPEG4Level1: - FR = 30; W = 11; H = 9; MBPS = 2970; FS = 99; BR = 128; break; - case CodecProfileLevel.MPEG4Level2: - FR = 30; W = 22; H = 18; MBPS = 5940; FS = 396; BR = 384; break; - case CodecProfileLevel.MPEG4Level3: - FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 768; break; - case CodecProfileLevel.MPEG4Level3b: - FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 1500; break; - case CodecProfileLevel.MPEG4Level4: - FR = 30; W = 44; H = 36; MBPS = 23760; FS = 792; BR = 3000; break; - case CodecProfileLevel.MPEG4Level5: - FR = 30; W = 45; H = 36; MBPS = 48600; FS = 1620; BR = 8000; break; - default: - Log.w(TAG, "Unrecognized profile/level " - + profileLevel.profile + "/" - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - break; - case CodecProfileLevel.MPEG4ProfileMain: // 2-4 - case CodecProfileLevel.MPEG4ProfileNbit: // 2 - case CodecProfileLevel.MPEG4ProfileAdvancedRealTime: // 1-4 - case CodecProfileLevel.MPEG4ProfileCoreScalable: // 1-3 - case CodecProfileLevel.MPEG4ProfileAdvancedCoding: // 1-4 - case CodecProfileLevel.MPEG4ProfileCore: // 1-2 - case CodecProfileLevel.MPEG4ProfileAdvancedCore: // 1-4 - case CodecProfileLevel.MPEG4ProfileSimpleScalable: // 0-2 - case CodecProfileLevel.MPEG4ProfileHybrid: // 1-2 - - // Studio profiles are not supported by our codecs. - - // Only profiles that can decode simple object types are considered. - // The following profiles are not able to. - case CodecProfileLevel.MPEG4ProfileBasicAnimated: // 1-2 - case CodecProfileLevel.MPEG4ProfileScalableTexture: // 1 - case CodecProfileLevel.MPEG4ProfileSimpleFace: // 1-2 - case CodecProfileLevel.MPEG4ProfileAdvancedScalable: // 1-3 - case CodecProfileLevel.MPEG4ProfileSimpleFBA: // 1-2 - Log.i(TAG, "Unsupported profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNSUPPORTED; - supported = false; - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + applyMacroBlockLimits(maxWidth, maxHeight, + maxBlocks, maxBlocksPerSecond, + 16 /* blockWidth */, 16 /* blockHeight */, + 1 /* widthAlignment */, 1 /* heightAlignment */); + mFrameRateRange = mFrameRateRange.intersect(12, maxRate); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG4)) { + int maxWidth = 11, maxHeight = 9, maxRate = 15; + maxBlocks = 99; + maxBlocksPerSecond = 1485; + maxBps = 64000; + for (CodecProfileLevel profileLevel: profileLevels) { + int MBPS = 0, FS = 0, BR = 0, FR = 0, W = 0, H = 0; + boolean strict = false; // true: W, H and FR are individual max limits + boolean supported = true; + switch (profileLevel.profile) { + case CodecProfileLevel.MPEG4ProfileSimple: + switch (profileLevel.level) { + case CodecProfileLevel.MPEG4Level0: + strict = true; + FR = 15; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 64; break; + case CodecProfileLevel.MPEG4Level1: + FR = 30; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 64; break; + case CodecProfileLevel.MPEG4Level0b: + strict = true; + FR = 15; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 128; break; + case CodecProfileLevel.MPEG4Level2: + FR = 30; W = 22; H = 18; MBPS = 5940; FS = 396; BR = 128; break; + case CodecProfileLevel.MPEG4Level3: + FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 384; break; + case CodecProfileLevel.MPEG4Level4a: + FR = 30; W = 40; H = 30; MBPS = 36000; FS = 1200; BR = 4000; break; + case CodecProfileLevel.MPEG4Level5: + FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 8000; break; + case CodecProfileLevel.MPEG4Level6: + FR = 30; W = 80; H = 45; MBPS = 108000; FS = 3600; BR = 12000; break; + default: + Log.w(TAG, "Unrecognized profile/level " + + profileLevel.profile + "/" + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + break; + case CodecProfileLevel.MPEG4ProfileAdvancedSimple: + switch (profileLevel.level) { + case CodecProfileLevel.MPEG4Level0: + case CodecProfileLevel.MPEG4Level1: + FR = 30; W = 11; H = 9; MBPS = 2970; FS = 99; BR = 128; break; + case CodecProfileLevel.MPEG4Level2: + FR = 30; W = 22; H = 18; MBPS = 5940; FS = 396; BR = 384; break; + case CodecProfileLevel.MPEG4Level3: + FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 768; break; + case CodecProfileLevel.MPEG4Level3b: + FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 1500; break; + case CodecProfileLevel.MPEG4Level4: + FR = 30; W = 44; H = 36; MBPS = 23760; FS = 792; BR = 3000; break; + case CodecProfileLevel.MPEG4Level5: + FR = 30; W = 45; H = 36; MBPS = 48600; FS = 1620; BR = 8000; break; + default: + Log.w(TAG, "Unrecognized profile/level " + + profileLevel.profile + "/" + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + break; + case CodecProfileLevel.MPEG4ProfileMain: // 2-4 + case CodecProfileLevel.MPEG4ProfileNbit: // 2 + case CodecProfileLevel.MPEG4ProfileAdvancedRealTime: // 1-4 + case CodecProfileLevel.MPEG4ProfileCoreScalable: // 1-3 + case CodecProfileLevel.MPEG4ProfileAdvancedCoding: // 1-4 + case CodecProfileLevel.MPEG4ProfileCore: // 1-2 + case CodecProfileLevel.MPEG4ProfileAdvancedCore: // 1-4 + case CodecProfileLevel.MPEG4ProfileSimpleScalable: // 0-2 + case CodecProfileLevel.MPEG4ProfileHybrid: // 1-2 + + // Studio profiles are not supported by our codecs. + + // Only profiles that can decode simple object types are considered. + // The following profiles are not able to. + case CodecProfileLevel.MPEG4ProfileBasicAnimated: // 1-2 + case CodecProfileLevel.MPEG4ProfileScalableTexture: // 1 + case CodecProfileLevel.MPEG4ProfileSimpleFace: // 1-2 + case CodecProfileLevel.MPEG4ProfileAdvancedScalable: // 1-3 + case CodecProfileLevel.MPEG4ProfileSimpleFBA: // 1-2 + Log.i(TAG, "Unsupported profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNSUPPORTED; + supported = false; + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + if (supported) { + errors &= ~ERROR_NONE_SUPPORTED; + } + maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR * 1000, maxBps); + if (strict) { + maxWidth = Math.max(W, maxWidth); + maxHeight = Math.max(H, maxHeight); + maxRate = Math.max(FR, maxRate); + } else { + // assuming max 60 fps frame rate and 1:2 aspect ratio + int maxDim = (int)Math.sqrt(FS * 2); + maxWidth = Math.max(maxDim, maxWidth); + maxHeight = Math.max(maxDim, maxHeight); + maxRate = Math.max(Math.max(FR, 60), maxRate); + } } - if (supported) { + applyMacroBlockLimits(maxWidth, maxHeight, + maxBlocks, maxBlocksPerSecond, + 16 /* blockWidth */, 16 /* blockHeight */, + 1 /* widthAlignment */, 1 /* heightAlignment */); + mFrameRateRange = mFrameRateRange.intersect(12, maxRate); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263)) { + int maxWidth = 11, maxHeight = 9, maxRate = 15; + int minWidth = maxWidth, minHeight = maxHeight; + int minAlignment = 16; + maxBlocks = 99; + maxBlocksPerSecond = 1485; + maxBps = 64000; + for (CodecProfileLevel profileLevel: profileLevels) { + int MBPS = 0, BR = 0, FR = 0, W = 0, H = 0, minW = minWidth, minH = minHeight; + boolean strict = false; // true: support only sQCIF, QCIF (maybe CIF) + switch (profileLevel.level) { + case CodecProfileLevel.H263Level10: + strict = true; // only supports sQCIF & QCIF + FR = 15; W = 11; H = 9; BR = 1; MBPS = W * H * FR; break; + case CodecProfileLevel.H263Level20: + strict = true; // only supports sQCIF, QCIF & CIF + FR = 30; W = 22; H = 18; BR = 2; MBPS = W * H * 15; break; + case CodecProfileLevel.H263Level30: + strict = true; // only supports sQCIF, QCIF & CIF + FR = 30; W = 22; H = 18; BR = 6; MBPS = W * H * FR; break; + case CodecProfileLevel.H263Level40: + strict = true; // only supports sQCIF, QCIF & CIF + FR = 30; W = 22; H = 18; BR = 32; MBPS = W * H * FR; break; + case CodecProfileLevel.H263Level45: + // only implies level 10 support + strict = profileLevel.profile == CodecProfileLevel.H263ProfileBaseline + || profileLevel.profile == + CodecProfileLevel.H263ProfileBackwardCompatible; + if (!strict) { + minW = 1; minH = 1; minAlignment = 4; + } + FR = 15; W = 11; H = 9; BR = 2; MBPS = W * H * FR; break; + case CodecProfileLevel.H263Level50: + // only supports 50fps for H > 15 + minW = 1; minH = 1; minAlignment = 4; + FR = 60; W = 22; H = 18; BR = 64; MBPS = W * H * 50; break; + case CodecProfileLevel.H263Level60: + // only supports 50fps for H > 15 + minW = 1; minH = 1; minAlignment = 4; + FR = 60; W = 45; H = 18; BR = 128; MBPS = W * H * 50; break; + case CodecProfileLevel.H263Level70: + // only supports 50fps for H > 30 + minW = 1; minH = 1; minAlignment = 4; + FR = 60; W = 45; H = 36; BR = 256; MBPS = W * H * 50; break; + default: + Log.w(TAG, "Unrecognized profile/level " + profileLevel.profile + + "/" + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.H263ProfileBackwardCompatible: + case CodecProfileLevel.H263ProfileBaseline: + case CodecProfileLevel.H263ProfileH320Coding: + case CodecProfileLevel.H263ProfileHighCompression: + case CodecProfileLevel.H263ProfileHighLatency: + case CodecProfileLevel.H263ProfileInterlace: + case CodecProfileLevel.H263ProfileInternet: + case CodecProfileLevel.H263ProfileISWV2: + case CodecProfileLevel.H263ProfileISWV3: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + if (strict) { + // Strict levels define sub-QCIF min size and enumerated sizes. We + // cannot express support for "only sQCIF & QCIF (& CIF)" using + // VideoCapabilities but we can express "only QCIF (& CIF)", so set + // minimume size at QCIF.minW = 8; minH = 6; + minW = 11; minH = 9; + } else { + // any support for non-strict levels (including unrecognized profiles or + // levels) allow custom frame size support beyond supported limits + // (other than bitrate) + mAllowMbOverride = true; + } errors &= ~ERROR_NONE_SUPPORTED; - } - maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR * 1000, maxBps); - if (strict) { + maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); + maxBlocks = Math.max(W * H, maxBlocks); + maxBps = Math.max(BR * 64000, maxBps); maxWidth = Math.max(W, maxWidth); maxHeight = Math.max(H, maxHeight); maxRate = Math.max(FR, maxRate); - } else { - // assuming max 60 fps frame rate and 1:2 aspect ratio - int maxDim = (int)Math.sqrt(FS * 2); - maxWidth = Math.max(maxDim, maxWidth); - maxHeight = Math.max(maxDim, maxHeight); - maxRate = Math.max(Math.max(FR, 60), maxRate); - } - } - applyMacroBlockLimits(maxWidth, maxHeight, - maxBlocks, maxBlocksPerSecond, - 16 /* blockWidth */, 16 /* blockHeight */, - 1 /* widthAlignment */, 1 /* heightAlignment */); - mFrameRateRange = mFrameRateRange.intersect(12, maxRate); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263)) { - int maxWidth = 11, maxHeight = 9, maxRate = 15; - int minWidth = maxWidth, minHeight = maxHeight; - int minAlignment = 16; - maxBlocks = 99; - maxBlocksPerSecond = 1485; - maxBps = 64000; - for (CodecProfileLevel profileLevel: profileLevels) { - int MBPS = 0, BR = 0, FR = 0, W = 0, H = 0, minW = minWidth, minH = minHeight; - boolean strict = false; // true: support only sQCIF, QCIF (maybe CIF) - switch (profileLevel.level) { - case CodecProfileLevel.H263Level10: - strict = true; // only supports sQCIF & QCIF - FR = 15; W = 11; H = 9; BR = 1; MBPS = W * H * FR; break; - case CodecProfileLevel.H263Level20: - strict = true; // only supports sQCIF, QCIF & CIF - FR = 30; W = 22; H = 18; BR = 2; MBPS = W * H * 15; break; - case CodecProfileLevel.H263Level30: - strict = true; // only supports sQCIF, QCIF & CIF - FR = 30; W = 22; H = 18; BR = 6; MBPS = W * H * FR; break; - case CodecProfileLevel.H263Level40: - strict = true; // only supports sQCIF, QCIF & CIF - FR = 30; W = 22; H = 18; BR = 32; MBPS = W * H * FR; break; - case CodecProfileLevel.H263Level45: - // only implies level 10 support - strict = profileLevel.profile == CodecProfileLevel.H263ProfileBaseline - || profileLevel.profile == - CodecProfileLevel.H263ProfileBackwardCompatible; - if (!strict) { - minW = 1; minH = 1; minAlignment = 4; - } - FR = 15; W = 11; H = 9; BR = 2; MBPS = W * H * FR; break; - case CodecProfileLevel.H263Level50: - // only supports 50fps for H > 15 - minW = 1; minH = 1; minAlignment = 4; - FR = 60; W = 22; H = 18; BR = 64; MBPS = W * H * 50; break; - case CodecProfileLevel.H263Level60: - // only supports 50fps for H > 15 - minW = 1; minH = 1; minAlignment = 4; - FR = 60; W = 45; H = 18; BR = 128; MBPS = W * H * 50; break; - case CodecProfileLevel.H263Level70: - // only supports 50fps for H > 30 - minW = 1; minH = 1; minAlignment = 4; - FR = 60; W = 45; H = 36; BR = 256; MBPS = W * H * 50; break; - default: - Log.w(TAG, "Unrecognized profile/level " + profileLevel.profile - + "/" + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - switch (profileLevel.profile) { - case CodecProfileLevel.H263ProfileBackwardCompatible: - case CodecProfileLevel.H263ProfileBaseline: - case CodecProfileLevel.H263ProfileH320Coding: - case CodecProfileLevel.H263ProfileHighCompression: - case CodecProfileLevel.H263ProfileHighLatency: - case CodecProfileLevel.H263ProfileInterlace: - case CodecProfileLevel.H263ProfileInternet: - case CodecProfileLevel.H263ProfileISWV2: - case CodecProfileLevel.H263ProfileISWV3: - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - if (strict) { - // Strict levels define sub-QCIF min size and enumerated sizes. We cannot - // express support for "only sQCIF & QCIF (& CIF)" using VideoCapabilities - // but we can express "only QCIF (& CIF)", so set minimume size at QCIF. - // minW = 8; minH = 6; - minW = 11; minH = 9; - } else { - // any support for non-strict levels (including unrecognized profiles or - // levels) allow custom frame size support beyond supported limits - // (other than bitrate) - mAllowMbOverride = true; + minWidth = Math.min(minW, minWidth); + minHeight = Math.min(minH, minHeight); } - errors &= ~ERROR_NONE_SUPPORTED; - maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); - maxBlocks = Math.max(W * H, maxBlocks); - maxBps = Math.max(BR * 64000, maxBps); - maxWidth = Math.max(W, maxWidth); - maxHeight = Math.max(H, maxHeight); - maxRate = Math.max(FR, maxRate); - minWidth = Math.min(minW, minWidth); - minHeight = Math.min(minH, minHeight); - } - // unless we encountered custom frame size support, limit size to QCIF and CIF - // using aspect ratio. - if (!mAllowMbOverride) { - mBlockAspectRatioRange = - Range.create(new Rational(11, 9), new Rational(11, 9)); - } - applyMacroBlockLimits( - minWidth, minHeight, - maxWidth, maxHeight, - maxBlocks, maxBlocksPerSecond, - 16 /* blockWidth */, 16 /* blockHeight */, - minAlignment /* widthAlignment */, minAlignment /* heightAlignment */); - mFrameRateRange = Range.create(1, maxRate); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP8)) { - maxBlocks = Integer.MAX_VALUE; - maxBlocksPerSecond = Integer.MAX_VALUE; - - // TODO: set to 100Mbps for now, need a number for VP8 - maxBps = 100000000; - - // profile levels are not indicative for VPx, but verify - // them nonetheless - for (CodecProfileLevel profileLevel: profileLevels) { - switch (profileLevel.level) { - case CodecProfileLevel.VP8Level_Version0: - case CodecProfileLevel.VP8Level_Version1: - case CodecProfileLevel.VP8Level_Version2: - case CodecProfileLevel.VP8Level_Version3: - break; - default: - Log.w(TAG, "Unrecognized level " - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + // unless we encountered custom frame size support, limit size to QCIF and CIF + // using aspect ratio. + if (!mAllowMbOverride) { + mBlockAspectRatioRange = + Range.create(new Rational(11, 9), new Rational(11, 9)); } - switch (profileLevel.profile) { - case CodecProfileLevel.VP8ProfileMain: - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + applyMacroBlockLimits( + minWidth, minHeight, + maxWidth, maxHeight, + maxBlocks, maxBlocksPerSecond, + 16 /* blockWidth */, 16 /* blockHeight */, + minAlignment /* widthAlignment */, minAlignment /* heightAlignment */); + mFrameRateRange = Range.create(1, maxRate); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP8)) { + maxBlocks = Integer.MAX_VALUE; + maxBlocksPerSecond = Integer.MAX_VALUE; + + // TODO: set to 100Mbps for now, need a number for VP8 + maxBps = 100000000; + + // profile levels are not indicative for VPx, but verify + // them nonetheless + for (CodecProfileLevel profileLevel: profileLevels) { + switch (profileLevel.level) { + case CodecProfileLevel.VP8Level_Version0: + case CodecProfileLevel.VP8Level_Version1: + case CodecProfileLevel.VP8Level_Version2: + case CodecProfileLevel.VP8Level_Version3: + break; + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.VP8ProfileMain: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + errors &= ~ERROR_NONE_SUPPORTED; } - errors &= ~ERROR_NONE_SUPPORTED; - } - final int blockSize = 16; - applyMacroBlockLimits(Short.MAX_VALUE, Short.MAX_VALUE, - maxBlocks, maxBlocksPerSecond, blockSize, blockSize, - 1 /* widthAlignment */, 1 /* heightAlignment */); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP9)) { - maxBlocksPerSecond = 829440; - maxBlocks = 36864; - maxBps = 200000; - int maxDim = 512; - - for (CodecProfileLevel profileLevel: profileLevels) { - long SR = 0; // luma sample rate - int FS = 0; // luma picture size - int BR = 0; // bit rate kbps - int D = 0; // luma dimension - switch (profileLevel.level) { - case CodecProfileLevel.VP9Level1: - SR = 829440; FS = 36864; BR = 200; D = 512; break; - case CodecProfileLevel.VP9Level11: - SR = 2764800; FS = 73728; BR = 800; D = 768; break; - case CodecProfileLevel.VP9Level2: - SR = 4608000; FS = 122880; BR = 1800; D = 960; break; - case CodecProfileLevel.VP9Level21: - SR = 9216000; FS = 245760; BR = 3600; D = 1344; break; - case CodecProfileLevel.VP9Level3: - SR = 20736000; FS = 552960; BR = 7200; D = 2048; break; - case CodecProfileLevel.VP9Level31: - SR = 36864000; FS = 983040; BR = 12000; D = 2752; break; - case CodecProfileLevel.VP9Level4: - SR = 83558400; FS = 2228224; BR = 18000; D = 4160; break; - case CodecProfileLevel.VP9Level41: - SR = 160432128; FS = 2228224; BR = 30000; D = 4160; break; - case CodecProfileLevel.VP9Level5: - SR = 311951360; FS = 8912896; BR = 60000; D = 8384; break; - case CodecProfileLevel.VP9Level51: - SR = 588251136; FS = 8912896; BR = 120000; D = 8384; break; - case CodecProfileLevel.VP9Level52: - SR = 1176502272; FS = 8912896; BR = 180000; D = 8384; break; - case CodecProfileLevel.VP9Level6: - SR = 1176502272; FS = 35651584; BR = 180000; D = 16832; break; - case CodecProfileLevel.VP9Level61: - SR = 2353004544L; FS = 35651584; BR = 240000; D = 16832; break; - case CodecProfileLevel.VP9Level62: - SR = 4706009088L; FS = 35651584; BR = 480000; D = 16832; break; - default: - Log.w(TAG, "Unrecognized level " - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - switch (profileLevel.profile) { - case CodecProfileLevel.VP9Profile0: - case CodecProfileLevel.VP9Profile1: - case CodecProfileLevel.VP9Profile2: - case CodecProfileLevel.VP9Profile3: - case CodecProfileLevel.VP9Profile2HDR: - case CodecProfileLevel.VP9Profile3HDR: - case CodecProfileLevel.VP9Profile2HDR10Plus: - case CodecProfileLevel.VP9Profile3HDR10Plus: - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + final int blockSize = 16; + applyMacroBlockLimits(Short.MAX_VALUE, Short.MAX_VALUE, + maxBlocks, maxBlocksPerSecond, blockSize, blockSize, + 1 /* widthAlignment */, 1 /* heightAlignment */); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP9)) { + maxBlocksPerSecond = 829440; + maxBlocks = 36864; + maxBps = 200000; + int maxDim = 512; + + for (CodecProfileLevel profileLevel: profileLevels) { + long SR = 0; // luma sample rate + int FS = 0; // luma picture size + int BR = 0; // bit rate kbps + int D = 0; // luma dimension + switch (profileLevel.level) { + case CodecProfileLevel.VP9Level1: + SR = 829440; FS = 36864; BR = 200; D = 512; break; + case CodecProfileLevel.VP9Level11: + SR = 2764800; FS = 73728; BR = 800; D = 768; break; + case CodecProfileLevel.VP9Level2: + SR = 4608000; FS = 122880; BR = 1800; D = 960; break; + case CodecProfileLevel.VP9Level21: + SR = 9216000; FS = 245760; BR = 3600; D = 1344; break; + case CodecProfileLevel.VP9Level3: + SR = 20736000; FS = 552960; BR = 7200; D = 2048; break; + case CodecProfileLevel.VP9Level31: + SR = 36864000; FS = 983040; BR = 12000; D = 2752; break; + case CodecProfileLevel.VP9Level4: + SR = 83558400; FS = 2228224; BR = 18000; D = 4160; break; + case CodecProfileLevel.VP9Level41: + SR = 160432128; FS = 2228224; BR = 30000; D = 4160; break; + case CodecProfileLevel.VP9Level5: + SR = 311951360; FS = 8912896; BR = 60000; D = 8384; break; + case CodecProfileLevel.VP9Level51: + SR = 588251136; FS = 8912896; BR = 120000; D = 8384; break; + case CodecProfileLevel.VP9Level52: + SR = 1176502272; FS = 8912896; BR = 180000; D = 8384; break; + case CodecProfileLevel.VP9Level6: + SR = 1176502272; FS = 35651584; BR = 180000; D = 16832; break; + case CodecProfileLevel.VP9Level61: + SR = 2353004544L; FS = 35651584; BR = 240000; D = 16832; break; + case CodecProfileLevel.VP9Level62: + SR = 4706009088L; FS = 35651584; BR = 480000; D = 16832; break; + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.VP9Profile0: + case CodecProfileLevel.VP9Profile1: + case CodecProfileLevel.VP9Profile2: + case CodecProfileLevel.VP9Profile3: + case CodecProfileLevel.VP9Profile2HDR: + case CodecProfileLevel.VP9Profile3HDR: + case CodecProfileLevel.VP9Profile2HDR10Plus: + case CodecProfileLevel.VP9Profile3HDR10Plus: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + errors &= ~ERROR_NONE_SUPPORTED; + maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR * 1000, maxBps); + maxDim = Math.max(D, maxDim); } - errors &= ~ERROR_NONE_SUPPORTED; - maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR * 1000, maxBps); - maxDim = Math.max(D, maxDim); - } - final int blockSize = 8; - int maxLengthInBlocks = Utils.divUp(maxDim, blockSize); - maxBlocks = Utils.divUp(maxBlocks, blockSize * blockSize); - maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize); + final int blockSize = 8; + int maxLengthInBlocks = Utils.divUp(maxDim, blockSize); + maxBlocks = Utils.divUp(maxBlocks, blockSize * blockSize); + maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize); + + applyMacroBlockLimits( + maxLengthInBlocks, maxLengthInBlocks, + maxBlocks, maxBlocksPerSecond, + blockSize, blockSize, + 1 /* widthAlignment */, 1 /* heightAlignment */); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) { + // CTBs are at least 8x8 so use 8x8 block size + maxBlocks = 36864 >> 6; // 192x192 pixels == 576 8x8 blocks + maxBlocksPerSecond = maxBlocks * 15; + maxBps = 128000; + for (CodecProfileLevel profileLevel: profileLevels) { + double FR = 0; + int FS = 0; + int BR = 0; + switch (profileLevel.level) { + /* The HEVC spec talks only in a very convoluted manner about the + existence of levels 1-3.1 for High tier, which could also be + understood as 'decoders and encoders should treat these levels + as if they were Main tier', so we do that. */ + case CodecProfileLevel.HEVCMainTierLevel1: + case CodecProfileLevel.HEVCHighTierLevel1: + FR = 15; FS = 36864; BR = 128; break; + case CodecProfileLevel.HEVCMainTierLevel2: + case CodecProfileLevel.HEVCHighTierLevel2: + FR = 30; FS = 122880; BR = 1500; break; + case CodecProfileLevel.HEVCMainTierLevel21: + case CodecProfileLevel.HEVCHighTierLevel21: + FR = 30; FS = 245760; BR = 3000; break; + case CodecProfileLevel.HEVCMainTierLevel3: + case CodecProfileLevel.HEVCHighTierLevel3: + FR = 30; FS = 552960; BR = 6000; break; + case CodecProfileLevel.HEVCMainTierLevel31: + case CodecProfileLevel.HEVCHighTierLevel31: + FR = 33.75; FS = 983040; BR = 10000; break; + case CodecProfileLevel.HEVCMainTierLevel4: + FR = 30; FS = 2228224; BR = 12000; break; + case CodecProfileLevel.HEVCHighTierLevel4: + FR = 30; FS = 2228224; BR = 30000; break; + case CodecProfileLevel.HEVCMainTierLevel41: + FR = 60; FS = 2228224; BR = 20000; break; + case CodecProfileLevel.HEVCHighTierLevel41: + FR = 60; FS = 2228224; BR = 50000; break; + case CodecProfileLevel.HEVCMainTierLevel5: + FR = 30; FS = 8912896; BR = 25000; break; + case CodecProfileLevel.HEVCHighTierLevel5: + FR = 30; FS = 8912896; BR = 100000; break; + case CodecProfileLevel.HEVCMainTierLevel51: + FR = 60; FS = 8912896; BR = 40000; break; + case CodecProfileLevel.HEVCHighTierLevel51: + FR = 60; FS = 8912896; BR = 160000; break; + case CodecProfileLevel.HEVCMainTierLevel52: + FR = 120; FS = 8912896; BR = 60000; break; + case CodecProfileLevel.HEVCHighTierLevel52: + FR = 120; FS = 8912896; BR = 240000; break; + case CodecProfileLevel.HEVCMainTierLevel6: + FR = 30; FS = 35651584; BR = 60000; break; + case CodecProfileLevel.HEVCHighTierLevel6: + FR = 30; FS = 35651584; BR = 240000; break; + case CodecProfileLevel.HEVCMainTierLevel61: + FR = 60; FS = 35651584; BR = 120000; break; + case CodecProfileLevel.HEVCHighTierLevel61: + FR = 60; FS = 35651584; BR = 480000; break; + case CodecProfileLevel.HEVCMainTierLevel62: + FR = 120; FS = 35651584; BR = 240000; break; + case CodecProfileLevel.HEVCHighTierLevel62: + FR = 120; FS = 35651584; BR = 800000; break; + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.HEVCProfileMain: + case CodecProfileLevel.HEVCProfileMain10: + case CodecProfileLevel.HEVCProfileMainStill: + case CodecProfileLevel.HEVCProfileMain10HDR10: + case CodecProfileLevel.HEVCProfileMain10HDR10Plus: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } - applyMacroBlockLimits( - maxLengthInBlocks, maxLengthInBlocks, - maxBlocks, maxBlocksPerSecond, - blockSize, blockSize, - 1 /* widthAlignment */, 1 /* heightAlignment */); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) { - // CTBs are at least 8x8 so use 8x8 block size - maxBlocks = 36864 >> 6; // 192x192 pixels == 576 8x8 blocks - maxBlocksPerSecond = maxBlocks * 15; - maxBps = 128000; - for (CodecProfileLevel profileLevel: profileLevels) { - double FR = 0; - int FS = 0; - int BR = 0; - switch (profileLevel.level) { - /* The HEVC spec talks only in a very convoluted manner about the - existence of levels 1-3.1 for High tier, which could also be - understood as 'decoders and encoders should treat these levels - as if they were Main tier', so we do that. */ - case CodecProfileLevel.HEVCMainTierLevel1: - case CodecProfileLevel.HEVCHighTierLevel1: - FR = 15; FS = 36864; BR = 128; break; - case CodecProfileLevel.HEVCMainTierLevel2: - case CodecProfileLevel.HEVCHighTierLevel2: - FR = 30; FS = 122880; BR = 1500; break; - case CodecProfileLevel.HEVCMainTierLevel21: - case CodecProfileLevel.HEVCHighTierLevel21: - FR = 30; FS = 245760; BR = 3000; break; - case CodecProfileLevel.HEVCMainTierLevel3: - case CodecProfileLevel.HEVCHighTierLevel3: - FR = 30; FS = 552960; BR = 6000; break; - case CodecProfileLevel.HEVCMainTierLevel31: - case CodecProfileLevel.HEVCHighTierLevel31: - FR = 33.75; FS = 983040; BR = 10000; break; - case CodecProfileLevel.HEVCMainTierLevel4: - FR = 30; FS = 2228224; BR = 12000; break; - case CodecProfileLevel.HEVCHighTierLevel4: - FR = 30; FS = 2228224; BR = 30000; break; - case CodecProfileLevel.HEVCMainTierLevel41: - FR = 60; FS = 2228224; BR = 20000; break; - case CodecProfileLevel.HEVCHighTierLevel41: - FR = 60; FS = 2228224; BR = 50000; break; - case CodecProfileLevel.HEVCMainTierLevel5: - FR = 30; FS = 8912896; BR = 25000; break; - case CodecProfileLevel.HEVCHighTierLevel5: - FR = 30; FS = 8912896; BR = 100000; break; - case CodecProfileLevel.HEVCMainTierLevel51: - FR = 60; FS = 8912896; BR = 40000; break; - case CodecProfileLevel.HEVCHighTierLevel51: - FR = 60; FS = 8912896; BR = 160000; break; - case CodecProfileLevel.HEVCMainTierLevel52: - FR = 120; FS = 8912896; BR = 60000; break; - case CodecProfileLevel.HEVCHighTierLevel52: - FR = 120; FS = 8912896; BR = 240000; break; - case CodecProfileLevel.HEVCMainTierLevel6: - FR = 30; FS = 35651584; BR = 60000; break; - case CodecProfileLevel.HEVCHighTierLevel6: - FR = 30; FS = 35651584; BR = 240000; break; - case CodecProfileLevel.HEVCMainTierLevel61: - FR = 60; FS = 35651584; BR = 120000; break; - case CodecProfileLevel.HEVCHighTierLevel61: - FR = 60; FS = 35651584; BR = 480000; break; - case CodecProfileLevel.HEVCMainTierLevel62: - FR = 120; FS = 35651584; BR = 240000; break; - case CodecProfileLevel.HEVCHighTierLevel62: - FR = 120; FS = 35651584; BR = 800000; break; - default: - Log.w(TAG, "Unrecognized level " - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + /* DPB logic: + if (width * height <= FS / 4) DPB = 16; + else if (width * height <= FS / 2) DPB = 12; + else if (width * height <= FS * 0.75) DPB = 8; + else DPB = 6; + */ + + FS >>= 6; // convert pixels to blocks + errors &= ~ERROR_NONE_SUPPORTED; + maxBlocksPerSecond = Math.max((int)(FR * FS), maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR * 1000, maxBps); } - switch (profileLevel.profile) { - case CodecProfileLevel.HEVCProfileMain: - case CodecProfileLevel.HEVCProfileMain10: - case CodecProfileLevel.HEVCProfileMainStill: - case CodecProfileLevel.HEVCProfileMain10HDR10: - case CodecProfileLevel.HEVCProfileMain10HDR10Plus: - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + + int maxLengthInBlocks = (int)(Math.sqrt(maxBlocks * 8)); + applyMacroBlockLimits( + maxLengthInBlocks, maxLengthInBlocks, + maxBlocks, maxBlocksPerSecond, + 8 /* blockWidth */, 8 /* blockHeight */, + 1 /* widthAlignment */, 1 /* heightAlignment */); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AV1)) { + maxBlocksPerSecond = 829440; + maxBlocks = 36864; + maxBps = 200000; + int maxDim = 512; + + // Sample rate, Picture Size, Bit rate and luma dimension for AV1 Codec, + // corresponding to the definitions in + // "AV1 Bitstream & Decoding Process Specification", Annex A + // found at https://aomedia.org/av1-bitstream-and-decoding-process-specification/ + for (CodecProfileLevel profileLevel: profileLevels) { + long SR = 0; // luma sample rate + int FS = 0; // luma picture size + int BR = 0; // bit rate kbps + int D = 0; // luma D + switch (profileLevel.level) { + case CodecProfileLevel.AV1Level2: + SR = 5529600; FS = 147456; BR = 1500; D = 2048; break; + case CodecProfileLevel.AV1Level21: + case CodecProfileLevel.AV1Level22: + case CodecProfileLevel.AV1Level23: + SR = 10454400; FS = 278784; BR = 3000; D = 2816; break; + + case CodecProfileLevel.AV1Level3: + SR = 24969600; FS = 665856; BR = 6000; D = 4352; break; + case CodecProfileLevel.AV1Level31: + case CodecProfileLevel.AV1Level32: + case CodecProfileLevel.AV1Level33: + SR = 39938400; FS = 1065024; BR = 10000; D = 5504; break; + + case CodecProfileLevel.AV1Level4: + SR = 77856768; FS = 2359296; BR = 12000; D = 6144; break; + case CodecProfileLevel.AV1Level41: + case CodecProfileLevel.AV1Level42: + case CodecProfileLevel.AV1Level43: + SR = 155713536; FS = 2359296; BR = 20000; D = 6144; break; + + case CodecProfileLevel.AV1Level5: + SR = 273715200; FS = 8912896; BR = 30000; D = 8192; break; + case CodecProfileLevel.AV1Level51: + SR = 547430400; FS = 8912896; BR = 40000; D = 8192; break; + case CodecProfileLevel.AV1Level52: + SR = 1094860800; FS = 8912896; BR = 60000; D = 8192; break; + case CodecProfileLevel.AV1Level53: + SR = 1176502272; FS = 8912896; BR = 60000; D = 8192; break; + + case CodecProfileLevel.AV1Level6: + SR = 1176502272; FS = 35651584; BR = 60000; D = 16384; break; + case CodecProfileLevel.AV1Level61: + SR = 2189721600L; FS = 35651584; BR = 100000; D = 16384; break; + case CodecProfileLevel.AV1Level62: + SR = 4379443200L; FS = 35651584; BR = 160000; D = 16384; break; + case CodecProfileLevel.AV1Level63: + SR = 4706009088L; FS = 35651584; BR = 160000; D = 16384; break; + + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.AV1ProfileMain8: + case CodecProfileLevel.AV1ProfileMain10: + case CodecProfileLevel.AV1ProfileMain10HDR10: + case CodecProfileLevel.AV1ProfileMain10HDR10Plus: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + errors &= ~ERROR_NONE_SUPPORTED; + maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR * 1000, maxBps); + maxDim = Math.max(D, maxDim); } - /* DPB logic: - if (width * height <= FS / 4) DPB = 16; - else if (width * height <= FS / 2) DPB = 12; - else if (width * height <= FS * 0.75) DPB = 8; - else DPB = 6; - */ - - FS >>= 6; // convert pixels to blocks - errors &= ~ERROR_NONE_SUPPORTED; - maxBlocksPerSecond = Math.max((int)(FR * FS), maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR * 1000, maxBps); + final int blockSize = 8; + int maxLengthInBlocks = Utils.divUp(maxDim, blockSize); + maxBlocks = Utils.divUp(maxBlocks, blockSize * blockSize); + maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize); + applyMacroBlockLimits( + maxLengthInBlocks, maxLengthInBlocks, + maxBlocks, maxBlocksPerSecond, + blockSize, blockSize, + 1 /* widthAlignment */, 1 /* heightAlignment */); + } else { + Log.w(TAG, "Unsupported mime " + mime); + // using minimal bitrate here. should be overridden by + // info from media_codecs.xml + maxBps = 64000; + errors |= ERROR_UNSUPPORTED; } + mBitrateRange = Range.create(1, maxBps); + mParent.mError |= errors; + } + } - int maxLengthInBlocks = (int)(Math.sqrt(maxBlocks * 8)); - applyMacroBlockLimits( - maxLengthInBlocks, maxLengthInBlocks, - maxBlocks, maxBlocksPerSecond, - 8 /* blockWidth */, 8 /* blockHeight */, - 1 /* widthAlignment */, 1 /* heightAlignment */); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AV1)) { - maxBlocksPerSecond = 829440; - maxBlocks = 36864; - maxBps = 200000; - int maxDim = 512; - - // Sample rate, Picture Size, Bit rate and luma dimension for AV1 Codec, - // corresponding to the definitions in - // "AV1 Bitstream & Decoding Process Specification", Annex A - // found at https://aomedia.org/av1-bitstream-and-decoding-process-specification/ - for (CodecProfileLevel profileLevel: profileLevels) { - long SR = 0; // luma sample rate - int FS = 0; // luma picture size - int BR = 0; // bit rate kbps - int D = 0; // luma D - switch (profileLevel.level) { - case CodecProfileLevel.AV1Level2: - SR = 5529600; FS = 147456; BR = 1500; D = 2048; break; - case CodecProfileLevel.AV1Level21: - case CodecProfileLevel.AV1Level22: - case CodecProfileLevel.AV1Level23: - SR = 10454400; FS = 278784; BR = 3000; D = 2816; break; - - case CodecProfileLevel.AV1Level3: - SR = 24969600; FS = 665856; BR = 6000; D = 4352; break; - case CodecProfileLevel.AV1Level31: - case CodecProfileLevel.AV1Level32: - case CodecProfileLevel.AV1Level33: - SR = 39938400; FS = 1065024; BR = 10000; D = 5504; break; - - case CodecProfileLevel.AV1Level4: - SR = 77856768; FS = 2359296; BR = 12000; D = 6144; break; - case CodecProfileLevel.AV1Level41: - case CodecProfileLevel.AV1Level42: - case CodecProfileLevel.AV1Level43: - SR = 155713536; FS = 2359296; BR = 20000; D = 6144; break; - - case CodecProfileLevel.AV1Level5: - SR = 273715200; FS = 8912896; BR = 30000; D = 8192; break; - case CodecProfileLevel.AV1Level51: - SR = 547430400; FS = 8912896; BR = 40000; D = 8192; break; - case CodecProfileLevel.AV1Level52: - SR = 1094860800; FS = 8912896; BR = 60000; D = 8192; break; - case CodecProfileLevel.AV1Level53: - SR = 1176502272; FS = 8912896; BR = 60000; D = 8192; break; - - case CodecProfileLevel.AV1Level6: - SR = 1176502272; FS = 35651584; BR = 60000; D = 16384; break; - case CodecProfileLevel.AV1Level61: - SR = 2189721600L; FS = 35651584; BR = 100000; D = 16384; break; - case CodecProfileLevel.AV1Level62: - SR = 4379443200L; FS = 35651584; BR = 160000; D = 16384; break; - case CodecProfileLevel.AV1Level63: - SR = 4706009088L; FS = 35651584; BR = 160000; D = 16384; break; - - default: - Log.w(TAG, "Unrecognized level " - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - switch (profileLevel.profile) { - case CodecProfileLevel.AV1ProfileMain8: - case CodecProfileLevel.AV1ProfileMain10: - case CodecProfileLevel.AV1ProfileMain10HDR10: - case CodecProfileLevel.AV1ProfileMain10HDR10Plus: - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - errors &= ~ERROR_NONE_SUPPORTED; - maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR * 1000, maxBps); - maxDim = Math.max(D, maxDim); + /* package private */ static final class VideoCapsNativeImpl implements VideoCapsIntf { + private long mNativeContext; // accessed by native methods + + private Range<Integer> mBitrateRange; + private Range<Integer> mHeightRange; + private Range<Integer> mWidthRange; + private Range<Integer> mFrameRateRange; + private List<PerformancePoint> mPerformancePoints; + + private int mWidthAlignment; + private int mHeightAlignment; + + // Used by JNI to construct Java VideoCapsNativeImpl + /** package private */ VideoCapsNativeImpl(Range<Integer> bitrateRange, + Range<Integer> widthRange, Range<Integer> heightRange, + Range<Integer> frameRateRange, List<PerformancePoint> performancePoints, + int widthAlignment, int heightAlignment) { + mBitrateRange = new Range<Integer>(bitrateRange.getLower(), + bitrateRange.getUpper()); + mWidthRange = new Range<Integer>(widthRange.getLower(), widthRange.getUpper()); + mHeightRange = new Range<Integer>(heightRange.getLower(), heightRange.getUpper()); + mFrameRateRange = new Range<Integer>(frameRateRange.getLower(), + frameRateRange.getUpper()); + mPerformancePoints = new ArrayList<PerformancePoint>(); + for (PerformancePoint pp : performancePoints) { + mPerformancePoints.add(new PerformancePoint(pp)); } + mWidthAlignment = widthAlignment; + mHeightAlignment = heightAlignment; + } - final int blockSize = 8; - int maxLengthInBlocks = Utils.divUp(maxDim, blockSize); - maxBlocks = Utils.divUp(maxBlocks, blockSize * blockSize); - maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize); - applyMacroBlockLimits( - maxLengthInBlocks, maxLengthInBlocks, - maxBlocks, maxBlocksPerSecond, - blockSize, blockSize, - 1 /* widthAlignment */, 1 /* heightAlignment */); - } else { - Log.w(TAG, "Unsupported mime " + mime); - // using minimal bitrate here. should be overriden by - // info from media_codecs.xml - maxBps = 64000; - errors |= ERROR_UNSUPPORTED; - } - mBitrateRange = Range.create(1, maxBps); - mParent.mError |= errors; + /* no public constructor */ + private VideoCapsNativeImpl() { } + + public Range<Integer> getBitrateRange() { + return mBitrateRange; + } + + public Range<Integer> getSupportedWidths() { + return mWidthRange; + } + + public Range<Integer> getSupportedHeights() { + return mHeightRange; + } + + public int getWidthAlignment() { + return mWidthAlignment; + } + + public int getHeightAlignment() { + return mHeightAlignment; + } + + /** @hide */ + public int getSmallerDimensionUpperLimit() { + return native_getSmallerDimensionUpperLimit(); + } + + public Range<Integer> getSupportedFrameRates() { + return mFrameRateRange; + } + + @Nullable + public List<PerformancePoint> getSupportedPerformancePoints() { + return mPerformancePoints; + } + + public Range<Integer> getSupportedWidthsFor(int height) { + return native_getSupportedWidthsFor(height); + } + + public Range<Integer> getSupportedHeightsFor(int width) { + return native_getSupportedHeightsFor(width); + } + + public Range<Double> getSupportedFrameRatesFor(int width, int height) { + return native_getSupportedFrameRatesFor(width, height); + } + + /** @throws IllegalArgumentException if the video size is not supported. */ + @Nullable + public Range<Double> getAchievableFrameRatesFor(int width, int height) { + return native_getAchievableFrameRatesFor(width, height); + } + + public boolean areSizeAndRateSupported(int width, int height, double frameRate) { + return native_areSizeAndRateSupported(width, height, frameRate); + } + + public boolean isSizeSupported(int width, int height) { + return native_isSizeSupported(width, height); + } + + /** @hide */ + public boolean supportsFormat(MediaFormat format) { + throw new UnsupportedOperationException( + "Java Implementation should not call native implemenatation"); + } + + private native Range<Integer> native_getSupportedWidthsFor(int height); + private native Range<Integer> native_getSupportedHeightsFor(int width); + private native Range<Double> native_getSupportedFrameRatesFor(int width, int height); + private native Range<Double> native_getAchievableFrameRatesFor(int width, int height); + private native boolean native_areSizeAndRateSupported( + int width, int height, double frameRate); + private native boolean native_isSizeSupported(int width, int height); + private native int native_getSmallerDimensionUpperLimit(); + + private static native void native_init(); + + static { + System.loadLibrary("media_jni"); + native_init(); + } } - } - /** - * A class that supports querying the encoding capabilities of a codec. - */ - public static final class EncoderCapabilities { + private VideoCapsIntf mImpl; + + /** @hide */ + public static VideoCapabilities create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + VideoCapsLegacyImpl impl = VideoCapsLegacyImpl.create(info, parent); + VideoCapabilities caps = new VideoCapabilities(impl); + return caps; + } + + /* package private */ VideoCapabilities(VideoCapsIntf impl) { + mImpl = impl; + } + + /* no public constructor */ + private VideoCapabilities() { } + /** - * Returns the supported range of quality values. + * Returns the range of supported bitrates in bits/second. + */ + public Range<Integer> getBitrateRange() { + return mImpl.getBitrateRange(); + } + + /** + * Returns the range of supported video widths. + * <p class=note> + * 32-bit processes will not support resolutions larger than 4096x4096 due to + * the limited address space. + */ + public Range<Integer> getSupportedWidths() { + return mImpl.getSupportedWidths(); + } + + /** + * Returns the range of supported video heights. + * <p class=note> + * 32-bit processes will not support resolutions larger than 4096x4096 due to + * the limited address space. + */ + public Range<Integer> getSupportedHeights() { + return mImpl.getSupportedHeights(); + } + + /** + * Returns the alignment requirement for video width (in pixels). * - * Quality is implementation-specific. As a general rule, a higher quality - * setting results in a better image quality and a lower compression ratio. + * This is a power-of-2 value that video width must be a + * multiple of. */ - public Range<Integer> getQualityRange() { - return mQualityRange; + public int getWidthAlignment() { + return mImpl.getWidthAlignment(); } /** - * Returns the supported range of encoder complexity values. + * Returns the alignment requirement for video height (in pixels). + * + * This is a power-of-2 value that video height must be a + * multiple of. + */ + public int getHeightAlignment() { + return mImpl.getWidthAlignment(); + } + + /** + * Return the upper limit on the smaller dimension of width or height. + * <p></p> + * Some codecs have a limit on the smaller dimension, whether it be + * the width or the height. E.g. a codec may only be able to handle + * up to 1920x1080 both in landscape and portrait mode (1080x1920). + * In this case the maximum width and height are both 1920, but the + * smaller dimension limit will be 1080. For other codecs, this is + * {@code Math.min(getSupportedWidths().getUpper(), + * getSupportedHeights().getUpper())}. + * + * @hide + */ + public int getSmallerDimensionUpperLimit() { + return mImpl.getSmallerDimensionUpperLimit(); + } + + /** + * Returns the range of supported frame rates. * <p> - * Some codecs may support multiple complexity levels, where higher - * complexity values use more encoder tools (e.g. perform more - * intensive calculations) to improve the quality or the compression - * ratio. Use a lower value to save power and/or time. + * This is not a performance indicator. Rather, it expresses the + * limits specified in the coding standard, based on the complexities + * of encoding material for later playback at a certain frame rate, + * or the decoding of such material in non-realtime. */ - public Range<Integer> getComplexityRange() { - return mComplexityRange; + public Range<Integer> getSupportedFrameRates() { + return mImpl.getSupportedFrameRates(); + } + + /** + * Returns the range of supported video widths for a video height. + * @param height the height of the video + */ + public Range<Integer> getSupportedWidthsFor(int height) { + return mImpl.getSupportedWidthsFor(height); + } + + /** + * Returns the range of supported video heights for a video width + * @param width the width of the video + */ + public Range<Integer> getSupportedHeightsFor(int width) { + return mImpl.getSupportedHeightsFor(width); + } + + /** + * Returns the range of supported video frame rates for a video size. + * <p> + * This is not a performance indicator. Rather, it expresses the limits specified in + * the coding standard, based on the complexities of encoding material of a given + * size for later playback at a certain frame rate, or the decoding of such material + * in non-realtime. + + * @param width the width of the video + * @param height the height of the video + */ + public Range<Double> getSupportedFrameRatesFor(int width, int height) { + return mImpl.getSupportedFrameRatesFor(width, height); } + /** + * Returns the range of achievable video frame rates for a video size. + * May return {@code null}, if the codec did not publish any measurement + * data. + * <p> + * This is a performance estimate provided by the device manufacturer based on statistical + * sampling of full-speed decoding and encoding measurements in various configurations + * of common video sizes supported by the codec. As such it should only be used to + * compare individual codecs on the device. The value is not suitable for comparing + * different devices or even different android releases for the same device. + * <p> + * <em>On {@link android.os.Build.VERSION_CODES#M} release</em> the returned range + * corresponds to the fastest frame rates achieved in the tested configurations. As + * such, it should not be used to gauge guaranteed or even average codec performance + * on the device. + * <p> + * <em>On {@link android.os.Build.VERSION_CODES#N} release</em> the returned range + * corresponds closer to sustained performance <em>in tested configurations</em>. + * One can expect to achieve sustained performance higher than the lower limit more than + * 50% of the time, and higher than half of the lower limit at least 90% of the time + * <em>in tested configurations</em>. + * Conversely, one can expect performance lower than twice the upper limit at least + * 90% of the time. + * <p class=note> + * Tested configurations use a single active codec. For use cases where multiple + * codecs are active, applications can expect lower and in most cases significantly lower + * performance. + * <p class=note> + * The returned range value is interpolated from the nearest frame size(s) tested. + * Codec performance is severely impacted by other activity on the device as well + * as environmental factors (such as battery level, temperature or power source), and can + * vary significantly even in a steady environment. + * <p class=note> + * Use this method in cases where only codec performance matters, e.g. to evaluate if + * a codec has any chance of meeting a performance target. Codecs are listed + * in {@link MediaCodecList} in the preferred order as defined by the device + * manufacturer. As such, applications should use the first suitable codec in the + * list to achieve the best balance between power use and performance. + * + * @param width the width of the video + * @param height the height of the video + * + * @throws IllegalArgumentException if the video size is not supported. + */ + @Nullable + public Range<Double> getAchievableFrameRatesFor(int width, int height) { + return mImpl.getAchievableFrameRatesFor(width, height); + } + + /** + * Returns the supported performance points. May return {@code null} if the codec did not + * publish any performance point information (e.g. the vendor codecs have not been updated + * to the latest android release). May return an empty list if the codec published that + * if does not guarantee any performance points. + * <p> + * This is a performance guarantee provided by the device manufacturer for hardware codecs + * based on hardware capabilities of the device. + * <p> + * The returned list is sorted first by decreasing number of pixels, then by decreasing + * width, and finally by decreasing frame rate. + * Performance points assume a single active codec. For use cases where multiple + * codecs are active, should use that highest pixel count, and add the frame rates of + * each individual codec. + * <p class=note> + * 32-bit processes will not support resolutions larger than 4096x4096 due to + * the limited address space, but performance points will be presented as is. + * In other words, even though a component publishes a performance point for + * a resolution higher than 4096x4096, it does not mean that the resolution is supported + * for 32-bit processes. + */ + @Nullable + public List<PerformancePoint> getSupportedPerformancePoints() { + return mImpl.getSupportedPerformancePoints(); + } + + /** + * Returns whether a given video size ({@code width} and + * {@code height}) and {@code frameRate} combination is supported. + */ + public boolean areSizeAndRateSupported(int width, int height, double frameRate) { + return mImpl.areSizeAndRateSupported(width, height, frameRate); + } + + /** + * Returns whether a given video size ({@code width} and + * {@code height}) is supported. + */ + public boolean isSizeSupported(int width, int height) { + return mImpl.isSizeSupported(width, height); + } + + /** + * @hide + * @throws java.lang.ClassCastException + * @throws java.lang.UnsupportedOperationException + */ + public boolean supportsFormat(MediaFormat format) { + return mImpl.supportsFormat(format); + } + } + + /** + * A class that supports querying the encoding capabilities of a codec. + */ + public static final class EncoderCapabilities { + private static final String TAG = "EncoderCapabilities"; + /** Constant quality mode */ public static final int BITRATE_MODE_CQ = 0; /** Variable bitrate mode */ @@ -3874,188 +4563,314 @@ public final class MediaCodecInfo { /** Constant bitrate mode with frame drops */ public static final int BITRATE_MODE_CBR_FD = 3; - private static final Feature[] bitrates = new Feature[] { - new Feature("VBR", BITRATE_MODE_VBR, true), - new Feature("CBR", BITRATE_MODE_CBR, false), - new Feature("CQ", BITRATE_MODE_CQ, false), - new Feature("CBR-FD", BITRATE_MODE_CBR_FD, false) - }; - - private static int parseBitrateMode(String mode) { - for (Feature feat: bitrates) { - if (feat.mName.equalsIgnoreCase(mode)) { - return feat.mValue; - } - } - return 0; - } + /* package private */ interface EncoderCapsIntf { + public Range<Integer> getQualityRange(); - /** - * Query whether a bitrate mode is supported. - */ - public boolean isBitrateModeSupported(int mode) { - for (Feature feat: bitrates) { - if (mode == feat.mValue) { - return (mBitControl & (1 << mode)) != 0; - } - } - return false; - } + public Range<Integer> getComplexityRange(); - private Range<Integer> mQualityRange; - private Range<Integer> mComplexityRange; - private CodecCapabilities mParent; + public boolean isBitrateModeSupported(int mode); - /* no public constructor */ - private EncoderCapabilities() { } + public void getDefaultFormat(MediaFormat format); - /** @hide */ - public static EncoderCapabilities create( - MediaFormat info, CodecCapabilities parent) { - EncoderCapabilities caps = new EncoderCapabilities(); - caps.init(info, parent); - return caps; + public boolean supportsFormat(MediaFormat format); } - private void init(MediaFormat info, CodecCapabilities parent) { - // no support for complexity or quality yet - mParent = parent; - mComplexityRange = Range.create(0, 0); - mQualityRange = Range.create(0, 0); - mBitControl = (1 << BITRATE_MODE_VBR); + /* package private */ static final class EncoderCapsLegacyImpl implements EncoderCapsIntf { + private CodecCapabilities.CodecCapsLegacyImpl mParent; - applyLevelLimits(); - parseFromInfo(info); - } + private Range<Integer> mQualityRange; + private Range<Integer> mComplexityRange; - private void applyLevelLimits() { - String mime = mParent.getMimeType(); - if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_FLAC)) { - mComplexityRange = Range.create(0, 8); - mBitControl = (1 << BITRATE_MODE_CQ); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_NB) - || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_WB) - || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_ALAW) - || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_MLAW) - || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MSGSM)) { - mBitControl = (1 << BITRATE_MODE_CBR); + public Range<Integer> getQualityRange() { + return mQualityRange; } - } - private int mBitControl; - private Integer mDefaultComplexity; - private Integer mDefaultQuality; - private String mQualityScale; + public Range<Integer> getComplexityRange() { + return mComplexityRange; + } - private void parseFromInfo(MediaFormat info) { - Map<String, Object> map = info.getMap(); + private static final Feature[] bitrates = new Feature[] { + new Feature("VBR", BITRATE_MODE_VBR, true), + new Feature("CBR", BITRATE_MODE_CBR, false), + new Feature("CQ", BITRATE_MODE_CQ, false), + new Feature("CBR-FD", BITRATE_MODE_CBR_FD, false) + }; - if (info.containsKey("complexity-range")) { - mComplexityRange = Utils - .parseIntRange(info.getString("complexity-range"), mComplexityRange); - // TODO should we limit this to level limits? + private static int parseBitrateMode(String mode) { + for (Feature feat: bitrates) { + if (feat.mName.equalsIgnoreCase(mode)) { + return feat.mValue; + } + } + return 0; } - if (info.containsKey("quality-range")) { - mQualityRange = Utils - .parseIntRange(info.getString("quality-range"), mQualityRange); + + public boolean isBitrateModeSupported(int mode) { + for (Feature feat: bitrates) { + if (mode == feat.mValue) { + return (mBitControl & (1 << mode)) != 0; + } + } + return false; } - if (info.containsKey("feature-bitrate-modes")) { - mBitControl = 0; - for (String mode: info.getString("feature-bitrate-modes").split(",")) { - mBitControl |= (1 << parseBitrateMode(mode)); + + /* no public constructor */ + private EncoderCapsLegacyImpl() { } + + /** @hide */ + public static EncoderCapsLegacyImpl create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + Log.d(TAG, "Legacy implementation is called while native flag is on."); } + + EncoderCapsLegacyImpl caps = new EncoderCapsLegacyImpl(); + caps.init(info, parent); + return caps; } - try { - mDefaultComplexity = Integer.parseInt((String)map.get("complexity-default")); - } catch (NumberFormatException e) { } + private void init(MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + // no support for complexity or quality yet + mParent = parent; + mComplexityRange = Range.create(0, 0); + mQualityRange = Range.create(0, 0); + mBitControl = (1 << BITRATE_MODE_VBR); - try { - mDefaultQuality = Integer.parseInt((String)map.get("quality-default")); - } catch (NumberFormatException e) { } + applyLevelLimits(); + parseFromInfo(info); + } - mQualityScale = (String)map.get("quality-scale"); - } + private void applyLevelLimits() { + String mime = mParent.getMimeType(); + if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_FLAC)) { + mComplexityRange = Range.create(0, 8); + mBitControl = (1 << BITRATE_MODE_CQ); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_NB) + || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_WB) + || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_ALAW) + || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_MLAW) + || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MSGSM)) { + mBitControl = (1 << BITRATE_MODE_CBR); + } + } + + private int mBitControl; + private Integer mDefaultComplexity; + private Integer mDefaultQuality; + private String mQualityScale; - private boolean supports( - Integer complexity, Integer quality, Integer profile) { - boolean ok = true; - if (ok && complexity != null) { - ok = mComplexityRange.contains(complexity); + private void parseFromInfo(MediaFormat info) { + Map<String, Object> map = info.getMap(); + + if (info.containsKey("complexity-range")) { + mComplexityRange = Utils + .parseIntRange(info.getString("complexity-range"), mComplexityRange); + // TODO should we limit this to level limits? + } + if (info.containsKey("quality-range")) { + mQualityRange = Utils + .parseIntRange(info.getString("quality-range"), mQualityRange); + } + if (info.containsKey("feature-bitrate-modes")) { + mBitControl = 0; + for (String mode: info.getString("feature-bitrate-modes").split(",")) { + mBitControl |= (1 << parseBitrateMode(mode)); + } + } + + try { + mDefaultComplexity = Integer.parseInt((String)map.get("complexity-default")); + } catch (NumberFormatException e) { } + + try { + mDefaultQuality = Integer.parseInt((String)map.get("quality-default")); + } catch (NumberFormatException e) { } + + mQualityScale = (String)map.get("quality-scale"); } - if (ok && quality != null) { - ok = mQualityRange.contains(quality); + + private boolean supports( + Integer complexity, Integer quality, Integer profile) { + boolean ok = true; + if (ok && complexity != null) { + ok = mComplexityRange.contains(complexity); + } + if (ok && quality != null) { + ok = mQualityRange.contains(quality); + } + if (ok && profile != null) { + for (CodecProfileLevel pl: mParent.getProfileLevels()) { + if (pl.profile == profile) { + profile = null; + break; + } + } + ok = profile == null; + } + return ok; } - if (ok && profile != null) { - for (CodecProfileLevel pl: mParent.profileLevels) { - if (pl.profile == profile) { - profile = null; + + /** @hide */ + public void getDefaultFormat(MediaFormat format) { + // don't list trivial quality/complexity as default for now + if (!mQualityRange.getUpper().equals(mQualityRange.getLower()) + && mDefaultQuality != null) { + format.setInteger(MediaFormat.KEY_QUALITY, mDefaultQuality); + } + if (!mComplexityRange.getUpper().equals(mComplexityRange.getLower()) + && mDefaultComplexity != null) { + format.setInteger(MediaFormat.KEY_COMPLEXITY, mDefaultComplexity); + } + // bitrates are listed in order of preference + for (Feature feat: bitrates) { + if ((mBitControl & (1 << feat.mValue)) != 0) { + format.setInteger(MediaFormat.KEY_BITRATE_MODE, feat.mValue); break; } } - ok = profile == null; } - return ok; - } - /** @hide */ - public void getDefaultFormat(MediaFormat format) { - // don't list trivial quality/complexity as default for now - if (!mQualityRange.getUpper().equals(mQualityRange.getLower()) - && mDefaultQuality != null) { - format.setInteger(MediaFormat.KEY_QUALITY, mDefaultQuality); - } - if (!mComplexityRange.getUpper().equals(mComplexityRange.getLower()) - && mDefaultComplexity != null) { - format.setInteger(MediaFormat.KEY_COMPLEXITY, mDefaultComplexity); - } - // bitrates are listed in order of preference - for (Feature feat: bitrates) { - if ((mBitControl & (1 << feat.mValue)) != 0) { - format.setInteger(MediaFormat.KEY_BITRATE_MODE, feat.mValue); - break; + /** @hide */ + public boolean supportsFormat(MediaFormat format) { + final Map<String, Object> map = format.getMap(); + final String mime = mParent.getMimeType(); + + Integer mode = (Integer)map.get(MediaFormat.KEY_BITRATE_MODE); + if (mode != null && !isBitrateModeSupported(mode)) { + return false; + } + + Integer complexity = (Integer)map.get(MediaFormat.KEY_COMPLEXITY); + if (MediaFormat.MIMETYPE_AUDIO_FLAC.equalsIgnoreCase(mime)) { + Integer flacComplexity = + (Integer)map.get(MediaFormat.KEY_FLAC_COMPRESSION_LEVEL); + if (complexity == null) { + complexity = flacComplexity; + } else if (flacComplexity != null && !complexity.equals(flacComplexity)) { + throw new IllegalArgumentException( + "conflicting values for complexity and " + + "flac-compression-level"); + } } + + // other audio parameters + Integer profile = (Integer)map.get(MediaFormat.KEY_PROFILE); + if (MediaFormat.MIMETYPE_AUDIO_AAC.equalsIgnoreCase(mime)) { + Integer aacProfile = (Integer)map.get(MediaFormat.KEY_AAC_PROFILE); + if (profile == null) { + profile = aacProfile; + } else if (aacProfile != null && !aacProfile.equals(profile)) { + throw new IllegalArgumentException( + "conflicting values for profile and aac-profile"); + } + } + + Integer quality = (Integer)map.get(MediaFormat.KEY_QUALITY); + + return supports(complexity, quality, profile); } } - /** @hide */ - public boolean supportsFormat(MediaFormat format) { - final Map<String, Object> map = format.getMap(); - final String mime = mParent.getMimeType(); + /* package private */ static final class EncoderCapsNativeImpl implements EncoderCapsIntf { + private long mNativeContext; // accessed by native methods - Integer mode = (Integer)map.get(MediaFormat.KEY_BITRATE_MODE); - if (mode != null && !isBitrateModeSupported(mode)) { - return false; + private Range<Integer> mQualityRange; + private Range<Integer> mComplexityRange; + + /* no public constructor */ + private EncoderCapsNativeImpl() { } + + // Constructor called from native + /* package private */ EncoderCapsNativeImpl(Range<Integer> qualityRange, + Range<Integer> complexityRange) { + mQualityRange = qualityRange; + mComplexityRange = complexityRange; } - Integer complexity = (Integer)map.get(MediaFormat.KEY_COMPLEXITY); - if (MediaFormat.MIMETYPE_AUDIO_FLAC.equalsIgnoreCase(mime)) { - Integer flacComplexity = - (Integer)map.get(MediaFormat.KEY_FLAC_COMPRESSION_LEVEL); - if (complexity == null) { - complexity = flacComplexity; - } else if (flacComplexity != null && !complexity.equals(flacComplexity)) { - throw new IllegalArgumentException( - "conflicting values for complexity and " + - "flac-compression-level"); - } + public Range<Integer> getQualityRange() { + return mQualityRange; } - // other audio parameters - Integer profile = (Integer)map.get(MediaFormat.KEY_PROFILE); - if (MediaFormat.MIMETYPE_AUDIO_AAC.equalsIgnoreCase(mime)) { - Integer aacProfile = (Integer)map.get(MediaFormat.KEY_AAC_PROFILE); - if (profile == null) { - profile = aacProfile; - } else if (aacProfile != null && !aacProfile.equals(profile)) { - throw new IllegalArgumentException( - "conflicting values for profile and aac-profile"); - } + public Range<Integer> getComplexityRange() { + return mComplexityRange; + } + + public boolean isBitrateModeSupported(int mode) { + return native_isBitrateModeSupported(mode); } - Integer quality = (Integer)map.get(MediaFormat.KEY_QUALITY); + // This API is for internal Java implementation only. Should not be called. + public void getDefaultFormat(MediaFormat format) { + throw new UnsupportedOperationException( + "Java Implementation should not call native implemenatation"); + } + + // This API is for internal Java implementation only. Should not be called. + public boolean supportsFormat(MediaFormat format) { + throw new UnsupportedOperationException( + "Java Implementation should not call native implemenatation"); + } - return supports(complexity, quality, profile); + private native boolean native_isBitrateModeSupported(int mode); + private static native void native_init(); + + static { + System.loadLibrary("media_jni"); + native_init(); + } + } + + private EncoderCapsIntf mImpl; + + /** @hide */ + public static EncoderCapabilities create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + EncoderCapsLegacyImpl impl = EncoderCapsLegacyImpl.create(info, parent); + EncoderCapabilities caps = new EncoderCapabilities(impl); + return caps; + } + + /* package private */ EncoderCapabilities(EncoderCapsIntf impl) { + mImpl = impl; + } + + /** + * Returns the supported range of quality values. + * + * Quality is implementation-specific. As a general rule, a higher quality + * setting results in a better image quality and a lower compression ratio. + */ + public Range<Integer> getQualityRange() { + return mImpl.getQualityRange(); + } + + /** + * Returns the supported range of encoder complexity values. + * <p> + * Some codecs may support multiple complexity levels, where higher + * complexity values use more encoder tools (e.g. perform more + * intensive calculations) to improve the quality or the compression + * ratio. Use a lower value to save power and/or time. + */ + public Range<Integer> getComplexityRange() { + return mImpl.getComplexityRange(); + } + + /** + * Query whether a bitrate mode is supported. + */ + public boolean isBitrateModeSupported(int mode) { + return mImpl.isBitrateModeSupported(mode); + } + + /** @hide */ + public void getDefaultFormat(MediaFormat format) { + mImpl.getDefaultFormat(format); + } + + /** @hide */ + public boolean supportsFormat(MediaFormat format) { + return mImpl.supportsFormat(format); } }; @@ -4960,4 +5775,19 @@ public final class MediaCodecInfo { mName, mCanonicalName, mFlags, caps.toArray(new CodecCapabilities[caps.size()])); } + + /* package private */ class GenericHelper { + private static Range<Integer> constructIntegerRange(int lower, int upper) { + return Range.create(Integer.valueOf(lower), Integer.valueOf(upper)); + } + + private static Range<Double> constructDoubleRange(double lower, double upper) { + return Range.create(Double.valueOf(lower), Double.valueOf(upper)); + } + + private static List<VideoCapabilities.PerformancePoint> + constructPerformancePointList(VideoCapabilities.PerformancePoint[] array) { + return Arrays.asList(array); + } + } } diff --git a/media/jni/Android.bp b/media/jni/Android.bp index f09dc7218d7d..af545d5a4bc4 100644 --- a/media/jni/Android.bp +++ b/media/jni/Android.bp @@ -25,6 +25,7 @@ cc_library_shared { min_sdk_version: "", srcs: [ + "android_media_CodecCapabilities.cpp", "android_media_ImageWriter.cpp", "android_media_ImageReader.cpp", "android_media_JetPlayer.cpp", @@ -64,6 +65,7 @@ cc_library_shared { "libbinder", "libmedia", "libmedia_codeclist", + "libmedia_codeclist_capabilities", "libmedia_jni_utils", "libmedia_omx", "libmediametrics", diff --git a/media/jni/android_media_CodecCapabilities.cpp b/media/jni/android_media_CodecCapabilities.cpp new file mode 100644 index 000000000000..df0c826d8d87 --- /dev/null +++ b/media/jni/android_media_CodecCapabilities.cpp @@ -0,0 +1,1015 @@ +/* + * Copyright 2024, 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. + */ + +//#define LOG_NDEBUG 0 +#define LOG_TAG "MediaCodec-JNI" + +#include "android_media_CodecCapabilities.h" +#include "android_media_Streams.h" +#include "android_runtime/AndroidRuntime.h" +#include "jni.h" + +#include <media/AudioCapabilities.h> +#include <media/CodecCapabilities.h> +#include <media/EncoderCapabilities.h> +#include <media/VideoCapabilities.h> +#include <media/stagefright/foundation/ADebug.h> +#include <media/stagefright/foundation/AMessage.h> +#include <nativehelper/JNIHelp.h> +#include <nativehelper/ScopedLocalRef.h> +#include <utils/Log.h> + +namespace android { + +struct fields_t { + jfieldID audioCapsContext; + jfieldID videoCapsContext; + jfieldID encoderCapsContext; + jfieldID codecCapsContext; +}; +static fields_t fields; + +// JCodecCapabilities + +JCodecCapabilities::JCodecCapabilities(std::shared_ptr<CodecCapabilities> codecCaps) + : mCodecCaps(codecCaps) {} + +std::shared_ptr<CodecCapabilities> JCodecCapabilities::getCodecCaps() const { + return mCodecCaps; +} + +int32_t JCodecCapabilities::getMaxSupportedInstances() const { + return mCodecCaps->getMaxSupportedInstances(); +} + +std::string JCodecCapabilities::getMediaType() const { + return mCodecCaps->getMediaType(); +} + +bool JCodecCapabilities::isFeatureRequired(const std::string& name) const { + return mCodecCaps->isFeatureRequired(name); +} + +bool JCodecCapabilities::isFeatureSupported(const std::string& name) const { + return mCodecCaps->isFeatureSupported(name); +} + +bool JCodecCapabilities::isFormatSupported(const sp<AMessage> &format) const { + return mCodecCaps->isFormatSupported(format); +} + +bool JCodecCapabilities::isRegular() const { + return mCodecCaps->isRegular(); +} + +// Setter + +static sp<JCodecCapabilities> setCodecCapabilities(JNIEnv *env, jobject thiz, + const sp<JCodecCapabilities>& jCodecCaps) { + sp<JCodecCapabilities> old + = (JCodecCapabilities*)env->GetLongField(thiz, fields.codecCapsContext); + if (jCodecCaps != NULL) { + jCodecCaps->incStrong(thiz); + } + if (old != NULL) { + old->decStrong(thiz); + } + env->SetLongField(thiz, fields.codecCapsContext, (jlong)jCodecCaps.get()); + return old; +} + +// Getters + +static AudioCapabilities* getAudioCapabilities(JNIEnv *env, jobject thiz) { + AudioCapabilities* const p = (AudioCapabilities*)env->GetLongField( + thiz, fields.audioCapsContext); + return p; +} + +static VideoCapabilities* getVideoCapabilities(JNIEnv *env, jobject thiz) { + VideoCapabilities* const p = (VideoCapabilities*)env->GetLongField( + thiz, fields.videoCapsContext); + return p; +} + +static EncoderCapabilities* getEncoderCapabilities(JNIEnv *env, jobject thiz) { + EncoderCapabilities* const p = (EncoderCapabilities*)env->GetLongField( + thiz, fields.encoderCapsContext); + return p; +} + +static sp<JCodecCapabilities> getCodecCapabilities(JNIEnv *env, jobject thiz) { + JCodecCapabilities* const p = (JCodecCapabilities*)env->GetLongField( + thiz, fields.codecCapsContext); + return sp<JCodecCapabilities>(p); +} + +// Utils + +static jobject convertToJavaIntRange(JNIEnv *env, const Range<int32_t>& range) { + jclass helperClazz = env->FindClass("android/media/MediaCodecInfo$GenericHelper"); + jmethodID constructIntegerRangeID = env->GetStaticMethodID(helperClazz, "constructIntegerRange", + "(II)Landroid/util/Range;"); + jobject jRange = env->CallStaticObjectMethod(helperClazz, constructIntegerRangeID, + range.lower(), range.upper()); + + return jRange; +} + +static jobject convertToJavaDoubleRange(JNIEnv *env, const Range<double>& range) { + jclass helperClazz = env->FindClass("android/media/MediaCodecInfo$GenericHelper"); + jmethodID constructDoubleRangeID = env->GetStaticMethodID(helperClazz, "constructDoubleRange", + "(DD)Landroid/util/Range;"); + jobject jRange = env->CallStaticObjectMethod(helperClazz, constructDoubleRangeID, + range.lower(), range.upper()); + return jRange; +} + +static jobjectArray convertToJavaIntRangeArray(JNIEnv *env, + const std::vector<Range<int32_t>>& ranges) { + jclass rangeClazz = env->FindClass("android/util/Range"); + CHECK(rangeClazz != NULL); + jobjectArray jRanges = env->NewObjectArray(ranges.size(), rangeClazz, NULL); + for (int i = 0; i < ranges.size(); i++) { + Range<int32_t> range = ranges.at(i); + jobject jRange = convertToJavaIntRange(env, range); + env->SetObjectArrayElement(jRanges, i, jRange); + env->DeleteLocalRef(jRange); + jRange = NULL; + } + return jRanges; +} + +// Converters between Java objects and native instances + +// The Java AudioCapabilities object keep bitrateRange, sampleRates, sampleRateRanges +// and inputChannelRanges in it to prevent reconstruction when called the getters functions. +static jobject convertToJavaAudioCapabilities( + JNIEnv *env, std::shared_ptr<AudioCapabilities> audioCaps) { + if (audioCaps == nullptr) { + return NULL; + } + + // construct Java bitrateRange + const Range<int32_t>& bitrateRange = audioCaps->getBitrateRange(); + jobject jBitrateRange = convertToJavaIntRange(env, bitrateRange); + + // construct Java sampleRates array + const std::vector<int32_t>& sampleRates = audioCaps->getSupportedSampleRates(); + jintArray jSampleRates = env->NewIntArray(sampleRates.size()); + for (size_t i = 0; i < sampleRates.size(); ++i) { + jint val = sampleRates.at(i); + env->SetIntArrayRegion(jSampleRates, i, 1, &val); + } + + // construct Java sampleRateRanges + const std::vector<Range<int32_t>>& sampleRateRanges = audioCaps->getSupportedSampleRateRanges(); + jobjectArray jSampleRateRanges = convertToJavaIntRangeArray(env, sampleRateRanges); + + // construct Java inputChannelRanges + const std::vector<Range<int32_t>>& inputChannelRanges = audioCaps->getInputChannelCountRanges(); + jobjectArray jInputChannelRanges = convertToJavaIntRangeArray(env, inputChannelRanges); + + // construct Java AudioCapsNativeImpl + jclass audioCapsImplClazz + = env->FindClass("android/media/MediaCodecInfo$AudioCapabilities$AudioCapsNativeImpl"); + CHECK(audioCapsImplClazz != NULL); + jmethodID audioCapsImplConstructID = env->GetMethodID(audioCapsImplClazz, "<init>", + "(Landroid/util/Range;" + "[I" + "[Landroid/util/Range;" + "[Landroid/util/Range;)V"); + jobject jAudioCapsImpl = env->NewObject(audioCapsImplClazz, audioCapsImplConstructID, + jBitrateRange, jSampleRates, jSampleRateRanges, jInputChannelRanges); + // The native AudioCapabilities won't be destructed until process ends. + env->SetLongField(jAudioCapsImpl, fields.audioCapsContext, (jlong)audioCaps.get()); + + // construct Java AudioCapabilities + jclass audioCapsClazz + = env->FindClass("android/media/MediaCodecInfo$AudioCapabilities"); + CHECK(audioCapsClazz != NULL); + jmethodID audioCapsConstructID = env->GetMethodID(audioCapsClazz, "<init>", + "(Landroid/media/MediaCodecInfo$AudioCapabilities$AudioCapsIntf;)V"); + jobject jAudioCaps = env->NewObject(audioCapsClazz, audioCapsConstructID, jAudioCapsImpl); + + env->DeleteLocalRef(jBitrateRange); + jBitrateRange = NULL; + + env->DeleteLocalRef(jSampleRates); + jSampleRates = NULL; + + env->DeleteLocalRef(jSampleRateRanges); + jSampleRateRanges = NULL; + + env->DeleteLocalRef(jInputChannelRanges); + jInputChannelRanges = NULL; + + env->DeleteLocalRef(jAudioCapsImpl); + jAudioCapsImpl = NULL; + + return jAudioCaps; +} + +// convert native PerformancePoints to Java objects +static jobject convertToJavaPerformancePoints(JNIEnv *env, + const std::vector<VideoCapabilities::PerformancePoint>& performancePoints) { + jclass performancePointClazz = env->FindClass( + "android/media/MediaCodecInfo$VideoCapabilities$PerformancePoint"); + CHECK(performancePointClazz != NULL); + jmethodID performancePointConstructID = env->GetMethodID(performancePointClazz, "<init>", + "(IIIJII)V"); + + jobjectArray jPerformancePoints = env->NewObjectArray(performancePoints.size(), + performancePointClazz, NULL); + int i = 0; + for (auto it = performancePoints.begin(); it != performancePoints.end(); ++it, ++i) { + jobject jPerformancePoint = env->NewObject(performancePointClazz, + performancePointConstructID, it->getWidth(), + it->getHeight(), it->getMaxFrameRate(), + it->getMaxMacroBlockRate(), it->getBlockSize().getWidth(), + it->getBlockSize().getHeight()); + + env->SetObjectArrayElement(jPerformancePoints, i, jPerformancePoint); + + env->DeleteLocalRef(jPerformancePoint); + } + + jclass helperClazz = env->FindClass("android/media/MediaCodecInfo$GenericHelper"); + CHECK(helperClazz != NULL); + jmethodID asListID = env->GetStaticMethodID(helperClazz, "constructPerformancePointList", + "([Landroid/media/MediaCodecInfo$VideoCapabilities$PerformancePoint;)Ljava/util/List;"); + CHECK(asListID != NULL); + jobject jList = env->CallStaticObjectMethod(helperClazz, asListID, jPerformancePoints); + + return jList; +} + +static VideoCapabilities::PerformancePoint convertToNativePerformancePoint( + JNIEnv *env, jobject pp) { + if (pp == NULL) { + jniThrowException(env, "java/lang/NullPointerException", NULL); + } + + jclass clazz = env->FindClass( + "android/media/MediaCodecInfo$VideoCapabilities$PerformancePoint"); + CHECK(clazz != NULL); + CHECK(env->IsInstanceOf(pp, clazz)); + + jmethodID getWidthID = env->GetMethodID(clazz, "getWidth", "()I"); + CHECK(getWidthID != NULL); + jint width = env->CallIntMethod(pp, getWidthID); + + jmethodID getHeightID = env->GetMethodID(clazz, "getHeight", "()I"); + CHECK(getHeightID != NULL); + jint height = env->CallIntMethod(pp, getHeightID); + + jmethodID getMaxFrameRateID = env->GetMethodID(clazz, "getMaxFrameRate", "()I"); + CHECK(getMaxFrameRateID != NULL); + jint maxFrameRate = env->CallIntMethod(pp, getMaxFrameRateID); + + jmethodID getMaxMacroBlockRateID = env->GetMethodID(clazz, "getMaxMacroBlockRate", "()J"); + CHECK(getMaxMacroBlockRateID != NULL); + jlong maxMacroBlockRate = env->CallLongMethod(pp, getMaxMacroBlockRateID); + + jmethodID getBlockWidthID = env->GetMethodID(clazz, "getBlockWidth", "()I"); + CHECK(getBlockWidthID != NULL); + jint blockWidth = env->CallIntMethod(pp, getBlockWidthID); + + jmethodID getBlockHeightID = env->GetMethodID(clazz, "getBlockHeight", "()I"); + CHECK(getBlockHeightID != NULL); + jint blockHeight = env->CallIntMethod(pp, getBlockHeightID); + + return VideoCapabilities::PerformancePoint(VideoSize(blockWidth, blockHeight), + width, height, maxFrameRate, maxMacroBlockRate); +} + +static jobject convertToJavaVideoCapabilities(JNIEnv *env, + std::shared_ptr<VideoCapabilities> videoCaps) { + if (videoCaps == nullptr) { + return NULL; + } + + // get Java bitrateRange + const Range<int32_t>& bitrateRange = videoCaps->getBitrateRange(); + jobject jBitrateRange = convertToJavaIntRange(env, bitrateRange); + + // get Java widthRange + const Range<int32_t>& widthRange = videoCaps->getSupportedWidths(); + jobject jWidthRange = convertToJavaIntRange(env, widthRange); + + // get Java heightRange + const Range<int32_t>& heightRange = videoCaps->getSupportedHeights(); + jobject jHeightRange = convertToJavaIntRange(env, heightRange); + + // get Java frameRateRange + const Range<int32_t>& frameRateRange = videoCaps->getSupportedFrameRates(); + jobject jFrameRateRange = convertToJavaIntRange(env, frameRateRange); + + // get Java performancePoints + const std::vector<VideoCapabilities::PerformancePoint>& performancePoints + = videoCaps->getSupportedPerformancePoints(); + jobject jPerformancePoints = convertToJavaPerformancePoints(env, performancePoints); + + // get width alignment + int32_t widthAlignment = videoCaps->getWidthAlignment(); + + // get height alignment + int32_t heightAlignment = videoCaps->getHeightAlignment(); + + // get Java VideoCapsNativeImpl + jclass videoCapsImplClazz = env->FindClass( + "android/media/MediaCodecInfo$VideoCapabilities$VideoCapsNativeImpl"); + CHECK(videoCapsImplClazz != NULL); + jmethodID videoCapsImplConstructID = env->GetMethodID(videoCapsImplClazz, "<init>", + "(Landroid/util/Range;" + "Landroid/util/Range;" + "Landroid/util/Range;" + "Landroid/util/Range;" + "Ljava/util/List;II)V"); + jobject jVideoCapsImpl = env->NewObject(videoCapsImplClazz, videoCapsImplConstructID, + jBitrateRange, jWidthRange, jHeightRange, jFrameRateRange, jPerformancePoints, + widthAlignment, heightAlignment); + // The native VideoCapabilities won't be destructed until process ends. + env->SetLongField(jVideoCapsImpl, fields.videoCapsContext, (jlong)videoCaps.get()); + + // get Java VideoCapabilities + jclass videoCapsClazz + = env->FindClass("android/media/MediaCodecInfo$VideoCapabilities"); + CHECK(videoCapsClazz != NULL); + jmethodID videoCapsConstructID = env->GetMethodID(videoCapsClazz, "<init>", + "(Landroid/media/MediaCodecInfo$VideoCapabilities$VideoCapsIntf;)V"); + jobject jVideoCaps = env->NewObject(videoCapsClazz, videoCapsConstructID, jVideoCapsImpl); + + env->DeleteLocalRef(jBitrateRange); + jBitrateRange = NULL; + + env->DeleteLocalRef(jWidthRange); + jWidthRange = NULL; + + env->DeleteLocalRef(jHeightRange); + jHeightRange = NULL; + + env->DeleteLocalRef(jFrameRateRange); + jFrameRateRange = NULL; + + env->DeleteLocalRef(jPerformancePoints); + jPerformancePoints = NULL; + + env->DeleteLocalRef(jVideoCapsImpl); + jVideoCapsImpl = NULL; + + return jVideoCaps; +} + +static jobject convertToJavaEncoderCapabilities(JNIEnv *env, + std::shared_ptr<EncoderCapabilities> encoderCaps) { + if (encoderCaps == nullptr) { + return NULL; + } + + // get quality range + const Range<int>& qualityRange = encoderCaps->getQualityRange(); + jobject jQualityRange = convertToJavaIntRange(env, qualityRange); + + // get complexity range + const Range<int>& complexityRange = encoderCaps->getComplexityRange(); + jobject jComplexityRange = convertToJavaIntRange(env, complexityRange); + + // construct java EncoderCapsNativeImpl + jclass encoderCapsImplClazz = env->FindClass( + "android/media/MediaCodecInfo$EncoderCapabilities$EncoderCapsNativeImpl"); + CHECK(encoderCapsImplClazz != NULL); + jmethodID encoderCapsImplConstructID = env->GetMethodID(encoderCapsImplClazz, "<init>", + "(Landroid/util/Range;Landroid/util/Range;)V"); + jobject jEncoderCapsImpl = env->NewObject(encoderCapsImplClazz, encoderCapsImplConstructID, + jQualityRange, jComplexityRange); + // The native EncoderCapabilities won't be destructed until process ends. + env->SetLongField(jEncoderCapsImpl, fields.encoderCapsContext, (jlong)encoderCaps.get()); + + // construct java EncoderCapabilities object + jclass encoderCapsClazz + = env->FindClass("android/media/MediaCodecInfo$EncoderCapabilities"); + CHECK(encoderCapsClazz != NULL); + jmethodID encoderCapsConstructID = env->GetMethodID(encoderCapsClazz, "<init>", + "(Landroid/media/MediaCodecInfo$EncoderCapabilities$EncoderCapsIntf;)V"); + jobject jEncoderCaps = env->NewObject(encoderCapsClazz, encoderCapsConstructID, + jEncoderCapsImpl); + + env->DeleteLocalRef(jQualityRange); + jQualityRange = NULL; + + env->DeleteLocalRef(jComplexityRange); + jComplexityRange = NULL; + + env->DeleteLocalRef(jEncoderCapsImpl); + jEncoderCapsImpl = NULL; + + return jEncoderCaps; +} + +// Java CodecCapsNativeImpl keeps the defaultFormat, profileLevels, colorFormats, audioCapabilities, +// videoCapabilities and encoderCapabilities in it to prevent reconsturction when called by getter. +static jobject convertToJavaCodecCapsNativeImpl( + JNIEnv *env, std::shared_ptr<CodecCapabilities> codecCaps) { + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return NULL; + } + + // Construct defaultFormat + sp<AMessage> defaultFormat = codecCaps->getDefaultFormat(); + + jobject formatMap = NULL; + if (ConvertMessageToMap(env, defaultFormat, &formatMap)) { + return NULL; + } + + ScopedLocalRef<jclass> mediaFormatClass{env, env->FindClass("android/media/MediaFormat")}; + ScopedLocalRef<jobject> jDefaultFormat{env, env->NewObject( + mediaFormatClass.get(), + env->GetMethodID(mediaFormatClass.get(), "<init>", "(Ljava/util/Map;)V"), + formatMap)}; + + env->DeleteLocalRef(formatMap); + formatMap = NULL; + + // Construct Java ProfileLevelArray + std::vector<ProfileLevel> profileLevels = codecCaps->getProfileLevels(); + + jclass profileLevelClazz = + env->FindClass("android/media/MediaCodecInfo$CodecProfileLevel"); + CHECK(profileLevelClazz != NULL); + + jobjectArray profileLevelArray = + env->NewObjectArray(profileLevels.size(), profileLevelClazz, NULL); + + jfieldID profileField = + env->GetFieldID(profileLevelClazz, "profile", "I"); + jfieldID levelField = + env->GetFieldID(profileLevelClazz, "level", "I"); + + for (size_t i = 0; i < profileLevels.size(); ++i) { + const ProfileLevel &src = profileLevels.at(i); + + jobject profileLevelObj = env->AllocObject(profileLevelClazz); + + env->SetIntField(profileLevelObj, profileField, src.mProfile); + env->SetIntField(profileLevelObj, levelField, src.mLevel); + + env->SetObjectArrayElement(profileLevelArray, i, profileLevelObj); + + env->DeleteLocalRef(profileLevelObj); + profileLevelObj = NULL; + } + + // Construct ColorFormatArray + std::vector<uint32_t> colorFormats = codecCaps->getColorFormats(); + + jintArray colorFormatsArray = env->NewIntArray(colorFormats.size()); + env->SetIntArrayRegion(colorFormatsArray, 0, colorFormats.size(), + reinterpret_cast<jint*>(colorFormats.data())); + + // Construct and set AudioCapabilities + std::shared_ptr<AudioCapabilities> audioCaps = codecCaps->getAudioCapabilities(); + jobject jAudioCaps = convertToJavaAudioCapabilities(env, audioCaps); + + // Set VideoCapabilities + std::shared_ptr<VideoCapabilities> videoCaps = codecCaps->getVideoCapabilities(); + jobject jVideoCaps = convertToJavaVideoCapabilities(env, videoCaps); + + // Set EncoderCapabilities + std::shared_ptr<EncoderCapabilities> encoderCaps = codecCaps->getEncoderCapabilities(); + jobject jEncoderCaps = convertToJavaEncoderCapabilities(env, encoderCaps); + + // Construct CodecCapsNativeImpl + jclass codecCapsImplClazz = + env->FindClass("android/media/MediaCodecInfo$CodecCapabilities$CodecCapsNativeImpl"); + CHECK(codecCapsImplClazz != NULL); + jmethodID codecCapsImplConstructID = env->GetMethodID(codecCapsImplClazz, "<init>", + "([Landroid/media/MediaCodecInfo$CodecProfileLevel;[I" + "Landroid/media/MediaFormat;" + "Landroid/media/MediaCodecInfo$AudioCapabilities;" + "Landroid/media/MediaCodecInfo$VideoCapabilities;" + "Landroid/media/MediaCodecInfo$EncoderCapabilities;)V"); + jobject javaCodecCapsImpl = env->NewObject(codecCapsImplClazz, codecCapsImplConstructID, + profileLevelArray, colorFormatsArray, jDefaultFormat.get(), + jAudioCaps, jVideoCaps, jEncoderCaps); + + // Construct JCodecCapabilities and hold the codecCaps in it + sp<JCodecCapabilities> jCodecCaps = sp<JCodecCapabilities>::make(codecCaps); + setCodecCapabilities(env, javaCodecCapsImpl, jCodecCaps); + + env->DeleteLocalRef(profileLevelArray); + profileLevelArray = NULL; + + env->DeleteLocalRef(colorFormatsArray); + colorFormatsArray = NULL; + + env->DeleteLocalRef(jAudioCaps); + jAudioCaps = NULL; + + env->DeleteLocalRef(jVideoCaps); + jVideoCaps = NULL; + + env->DeleteLocalRef(jEncoderCaps); + jEncoderCaps = NULL; + + return javaCodecCapsImpl; +} + +jobject convertToJavaCodecCapabiliites( + JNIEnv *env, std::shared_ptr<CodecCapabilities> codecCaps) { + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return NULL; + } + + jobject javaCodecCapsImpl = convertToJavaCodecCapsNativeImpl(env, codecCaps); + + // Construct CodecCapabilities + jclass codecCapsClazz = env->FindClass("android/media/MediaCodecInfo$CodecCapabilities"); + CHECK(codecCapsClazz != NULL); + + jmethodID codecCapsConstructID = env->GetMethodID(codecCapsClazz, "<init>", + "(Landroid/media/MediaCodecInfo$CodecCapabilities$CodecCapsIntf;)V"); + jobject javaCodecCaps = env->NewObject(codecCapsClazz, codecCapsConstructID, javaCodecCapsImpl); + + return javaCodecCaps; +} + +} // namespace android + +// ---------------------------------------------------------------------------- + +using namespace android; + +// AudioCapabilities + +static void android_media_AudioCapabilities_native_init(JNIEnv *env, jobject /* thiz */) { + jclass audioCapsImplClazz + = env->FindClass("android/media/MediaCodecInfo$AudioCapabilities$AudioCapsNativeImpl"); + if (audioCapsImplClazz == NULL) { + return; + } + + fields.audioCapsContext = env->GetFieldID(audioCapsImplClazz, "mNativeContext", "J"); + if (fields.audioCapsContext == NULL) { + return; + } + + env->DeleteLocalRef(audioCapsImplClazz); +} + +static jint android_media_AudioCapabilities_getMaxInputChannelCount(JNIEnv *env, jobject thiz) { + AudioCapabilities* const audioCaps = getAudioCapabilities(env, thiz); + if (audioCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + int32_t maxInputChannelCount = audioCaps->getMaxInputChannelCount(); + return maxInputChannelCount; +} + +static jint android_media_AudioCapabilities_getMinInputChannelCount(JNIEnv *env, jobject thiz) { + AudioCapabilities* const audioCaps = getAudioCapabilities(env, thiz); + if (audioCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + int32_t minInputChannelCount = audioCaps->getMinInputChannelCount(); + return minInputChannelCount; +} + +static jboolean android_media_AudioCapabilities_isSampleRateSupported(JNIEnv *env, jobject thiz, + int sampleRate) { + AudioCapabilities* const audioCaps = getAudioCapabilities(env, thiz); + if (audioCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + bool res = audioCaps->isSampleRateSupported(sampleRate); + return res; +} + +// PerformancePoint + +static jboolean android_media_VideoCapabilities_PerformancePoint_covers(JNIEnv *env, jobject thiz, + jobject other) { + VideoCapabilities::PerformancePoint pp0 = convertToNativePerformancePoint(env, thiz); + VideoCapabilities::PerformancePoint pp1 = convertToNativePerformancePoint(env, other); + + bool res = pp0.covers(pp1); + return res; +} + +static jboolean android_media_VideoCapabilities_PerformancePoint_equals(JNIEnv *env, jobject thiz, + jobject other) { + VideoCapabilities::PerformancePoint pp0 = convertToNativePerformancePoint(env, thiz); + VideoCapabilities::PerformancePoint pp1 = convertToNativePerformancePoint(env, other); + + bool res = pp0.equals(pp1); + return res; +} + +// VideoCapabilities + +static void android_media_VideoCapabilities_native_init(JNIEnv *env, jobject /* thiz */) { + jclass clazz + = env->FindClass("android/media/MediaCodecInfo$VideoCapabilities$VideoCapsNativeImpl"); + if (clazz == NULL) { + return; + } + + fields.videoCapsContext = env->GetFieldID(clazz, "mNativeContext", "J"); + if (fields.videoCapsContext == NULL) { + return; + } + + env->DeleteLocalRef(clazz); +} + +static jboolean android_media_VideoCapabilities_areSizeAndRateSupported(JNIEnv *env, jobject thiz, + int32_t width, int32_t height, double frameRate) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + bool res = videoCaps->areSizeAndRateSupported(width, height, frameRate); + return res; +} + +static jboolean android_media_VideoCapabilities_isSizeSupported(JNIEnv *env, jobject thiz, + int32_t width, int32_t height) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + bool res = videoCaps->isSizeSupported(width, height); + return res; +} + +static jobject android_media_VideoCapabilities_getAchievableFrameRatesFor(JNIEnv *env, jobject thiz, + int32_t width, int32_t height) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return NULL; + } + + std::optional<Range<double>> frameRates = videoCaps->getAchievableFrameRatesFor(width, height); + if (!frameRates) { + return NULL; + } + jobject jFrameRates = convertToJavaDoubleRange(env, frameRates.value()); + return jFrameRates; +} + +static jobject android_media_VideoCapabilities_getSupportedFrameRatesFor(JNIEnv *env, jobject thiz, + int32_t width, int32_t height) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return NULL; + } + + std::optional<Range<double>> frameRates = videoCaps->getSupportedFrameRatesFor(width, height); + if (!frameRates) { + return NULL; + } + jobject jFrameRates = convertToJavaDoubleRange(env, frameRates.value()); + return jFrameRates; +} + +static jobject android_media_VideoCapabilities_getSupportedWidthsFor(JNIEnv *env, jobject thiz, + int32_t height) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return NULL; + } + + std::optional<Range<int32_t>> supportedWidths = videoCaps->getSupportedWidthsFor(height); + if (!supportedWidths) { + return NULL; + } + jobject jSupportedWidths = convertToJavaIntRange(env, supportedWidths.value()); + + return jSupportedWidths; +} + +static jobject android_media_VideoCapabilities_getSupportedHeightsFor(JNIEnv *env, jobject thiz, + int32_t width) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return NULL; + } + + std::optional<Range<int32_t>> supportedHeights = videoCaps->getSupportedHeightsFor(width); + if (!supportedHeights) { + return NULL; + } + jobject jSupportedHeights = convertToJavaIntRange(env, supportedHeights.value()); + + return jSupportedHeights; +} + +static jint android_media_VideoCapabilities_getSmallerDimensionUpperLimit(JNIEnv *env, + jobject thiz) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + int smallerDimensionUpperLimit = videoCaps->getSmallerDimensionUpperLimit(); + return smallerDimensionUpperLimit; +} + +// EncoderCapabilities + +static void android_media_EncoderCapabilities_native_init(JNIEnv *env, jobject /* thiz */) { + jclass clazz = env->FindClass( + "android/media/MediaCodecInfo$EncoderCapabilities$EncoderCapsNativeImpl"); + if (clazz == NULL) { + return; + } + + fields.encoderCapsContext = env->GetFieldID(clazz, "mNativeContext", "J"); + if (fields.encoderCapsContext == NULL) { + return; + } + + env->DeleteLocalRef(clazz); +} + +static jboolean android_media_EncoderCapabilities_isBitrateModeSupported(JNIEnv *env, jobject thiz, + int mode) { + EncoderCapabilities* const encoderCaps = getEncoderCapabilities(env, thiz); + if (encoderCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + bool res = encoderCaps->isBitrateModeSupported(mode); + return res; +} + +// CodecCapabilities + +static void android_media_CodecCapabilities_native_init(JNIEnv *env, jobject /* thiz */) { + jclass codecCapsClazz + = env->FindClass("android/media/MediaCodecInfo$CodecCapabilities$CodecCapsNativeImpl"); + if (codecCapsClazz == NULL) { + return; + } + + fields.codecCapsContext = env->GetFieldID(codecCapsClazz, "mNativeContext", "J"); + if (fields.codecCapsContext == NULL) { + return; + } + + env->DeleteLocalRef(codecCapsClazz); +} + +static jobject android_media_CodecCapabilities_createFromProfileLevel(JNIEnv *env, + jobject /* thiz */, jstring mediaType, jint profile, jint level) { + if (mediaType == NULL) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return NULL; + } + + const char *mediaTypeStr = env->GetStringUTFChars(mediaType, nullptr); + if (mediaTypeStr == nullptr) { + return NULL; + } + + std::shared_ptr<CodecCapabilities> codecCaps = CodecCapabilities::CreateFromProfileLevel( + mediaTypeStr, profile, level); + + jobject javaCodecCapsImpl = convertToJavaCodecCapsNativeImpl(env, codecCaps); + + env->ReleaseStringUTFChars(mediaType, mediaTypeStr); + + return javaCodecCapsImpl; +} + +static jobject android_media_CodecCapabilities_native_dup(JNIEnv *env, jobject thiz) { + sp<JCodecCapabilities> jCodecCaps = getCodecCapabilities(env, thiz); + + // As the CodecCaps objects are ready ony, it is ok to use the default copy constructor. + // The duplicate CodecCaps will share the same subobjects with the existing one. + // The lifetime of subobjects are managed by the shared pointer and sp. + std::shared_ptr<CodecCapabilities> duplicate + = std::make_shared<CodecCapabilities>(*(jCodecCaps->getCodecCaps())); + + jobject javaCodecCapsImpl = convertToJavaCodecCapsNativeImpl(env, duplicate); + + return javaCodecCapsImpl; +} + +static void android_media_CodecCapabilities_native_finalize(JNIEnv *env, jobject thiz) { + ALOGV("native_finalize"); + setCodecCapabilities(env, thiz, NULL); +} + +static jint android_media_CodecCapabilities_getMaxSupportedInstances(JNIEnv *env, jobject thiz) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + int maxSupportedInstances = codecCaps->getMaxSupportedInstances(); + return maxSupportedInstances; +} + +static jstring android_media_CodecCapabilities_getMimeType(JNIEnv *env, jobject thiz) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return NULL; + } + + std::string mediaType = codecCaps->getMediaType(); + return env->NewStringUTF(mediaType.c_str()); +} + +static jboolean android_media_CodecCapabilities_isFeatureRequired( + JNIEnv *env, jobject thiz, jstring name) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return false; + } + + if (name == NULL) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return -ENOENT; + } + + const char *nameStr = env->GetStringUTFChars(name, NULL); + if (nameStr == NULL) { + // Out of memory exception already pending. + return -ENOENT; + } + + bool isFeatureRequired = codecCaps->isFeatureRequired(nameStr); + + env->ReleaseStringUTFChars(name, nameStr); + + return isFeatureRequired; +} + +static jboolean android_media_CodecCapabilities_isFeatureSupported( + JNIEnv *env, jobject thiz, jstring name) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return false; + } + + if (name == NULL) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return -ENOENT; + } + + const char *nameStr = env->GetStringUTFChars(name, NULL); + if (nameStr == NULL) { + // Out of memory exception already pending. + return -ENOENT; + } + + bool isFeatureSupported = codecCaps->isFeatureSupported(nameStr); + + env->ReleaseStringUTFChars(name, nameStr); + + return isFeatureSupported; +} + +static jboolean android_media_CodecCapabilities_isFormatSupported(JNIEnv *env, jobject thiz, + jobjectArray keys, jobjectArray values) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return false; + } + + sp<AMessage> format; + status_t err = ConvertKeyValueArraysToMessage(env, keys, values, &format); + if (err != OK) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return -ENOENT;; + } + + return codecCaps->isFormatSupported(format); +} + +static jboolean android_media_CodecCapabilities_isRegular(JNIEnv *env, jobject thiz) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return false; + } + + bool res = codecCaps->isRegular(); + return res; +} + +// ---------------------------------------------------------------------------- + +static const JNINativeMethod gAudioCapsMethods[] = { + {"native_init", "()V", (void *)android_media_AudioCapabilities_native_init}, + {"native_getMaxInputChannelCount", "()I", (void *)android_media_AudioCapabilities_getMaxInputChannelCount}, + {"native_getMinInputChannelCount", "()I", (void *)android_media_AudioCapabilities_getMinInputChannelCount}, + {"native_isSampleRateSupported", "(I)Z", (void *)android_media_AudioCapabilities_isSampleRateSupported} +}; + +static const JNINativeMethod gPerformancePointMethods[] = { + {"native_covers", "(Landroid/media/MediaCodecInfo$VideoCapabilities$PerformancePoint;)Z", (void *)android_media_VideoCapabilities_PerformancePoint_covers}, + {"native_equals", "(Landroid/media/MediaCodecInfo$VideoCapabilities$PerformancePoint;)Z", (void *)android_media_VideoCapabilities_PerformancePoint_equals}, +}; + +static const JNINativeMethod gVideoCapsMethods[] = { + {"native_init", "()V", (void *)android_media_VideoCapabilities_native_init}, + {"native_areSizeAndRateSupported", "(IID)Z", (void *)android_media_VideoCapabilities_areSizeAndRateSupported}, + {"native_isSizeSupported", "(II)Z", (void *)android_media_VideoCapabilities_isSizeSupported}, + {"native_getAchievableFrameRatesFor", "(II)Landroid/util/Range;", (void *)android_media_VideoCapabilities_getAchievableFrameRatesFor}, + {"native_getSupportedFrameRatesFor", "(II)Landroid/util/Range;", (void *)android_media_VideoCapabilities_getSupportedFrameRatesFor}, + {"native_getSupportedWidthsFor", "(I)Landroid/util/Range;", (void *)android_media_VideoCapabilities_getSupportedWidthsFor}, + {"native_getSupportedHeightsFor", "(I)Landroid/util/Range;", (void *)android_media_VideoCapabilities_getSupportedHeightsFor}, + {"native_getSmallerDimensionUpperLimit", "()I", (void *)android_media_VideoCapabilities_getSmallerDimensionUpperLimit} +}; + +static const JNINativeMethod gEncoderCapsMethods[] = { + {"native_init", "()V", (void *)android_media_EncoderCapabilities_native_init}, + {"native_isBitrateModeSupported", "(I)Z", (void *)android_media_EncoderCapabilities_isBitrateModeSupported} +}; + +static const JNINativeMethod gCodecCapsMethods[] = { + { "native_init", "()V", (void *)android_media_CodecCapabilities_native_init }, + { "native_createFromProfileLevel", "(Ljava/lang/String;II)Landroid/media/MediaCodecInfo$CodecCapabilities$CodecCapsNativeImpl;", (void *)android_media_CodecCapabilities_createFromProfileLevel }, + { "native_dup", "()Landroid/media/MediaCodecInfo$CodecCapabilities$CodecCapsNativeImpl;", (void *)android_media_CodecCapabilities_native_dup }, + { "native_finalize", "()V", (void *)android_media_CodecCapabilities_native_finalize }, + { "native_getMaxSupportedInstances", "()I", (void *)android_media_CodecCapabilities_getMaxSupportedInstances }, + { "native_getMimeType", "()Ljava/lang/String;", (void *)android_media_CodecCapabilities_getMimeType }, + { "native_isFeatureRequired", "(Ljava/lang/String;)Z", (void *)android_media_CodecCapabilities_isFeatureRequired }, + { "native_isFeatureSupported", "(Ljava/lang/String;)Z", (void *)android_media_CodecCapabilities_isFeatureSupported }, + { "native_isFormatSupported", "([Ljava/lang/String;[Ljava/lang/Object;)Z", (void *)android_media_CodecCapabilities_isFormatSupported }, + { "native_isRegular", "()Z", (void *)android_media_CodecCapabilities_isRegular }, +}; + +int register_android_media_CodecCapabilities(JNIEnv *env) { + int result = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaCodecInfo$AudioCapabilities$AudioCapsNativeImpl", + gAudioCapsMethods, NELEM(gAudioCapsMethods)); + if (result != JNI_OK) { + return result; + } + + result = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaCodecInfo$VideoCapabilities$PerformancePoint", + gPerformancePointMethods, NELEM(gPerformancePointMethods)); + if (result != JNI_OK) { + return result; + } + + result = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaCodecInfo$VideoCapabilities$VideoCapsNativeImpl", + gVideoCapsMethods, NELEM(gVideoCapsMethods)); + if (result != JNI_OK) { + return result; + } + + result = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaCodecInfo$EncoderCapabilities$EncoderCapsNativeImpl", + gEncoderCapsMethods, NELEM(gEncoderCapsMethods)); + if (result != JNI_OK) { + return result; + } + + result = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaCodecInfo$CodecCapabilities$CodecCapsNativeImpl", + gCodecCapsMethods, NELEM(gCodecCapsMethods)); + return result; +}
\ No newline at end of file diff --git a/media/jni/android_media_CodecCapabilities.h b/media/jni/android_media_CodecCapabilities.h new file mode 100644 index 000000000000..5cca0b503740 --- /dev/null +++ b/media/jni/android_media_CodecCapabilities.h @@ -0,0 +1,47 @@ +/* + * Copyright 2024, 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. + */ + +#ifndef _ANDROID_MEDIA_CODECCAPABILITIES_H_ +#define _ANDROID_MEDIA_CODECCAPABILITIES_H_ + +#include "jni.h" + +#include <media/CodecCapabilities.h> + +namespace android { + +struct JCodecCapabilities : public RefBase { + JCodecCapabilities(std::shared_ptr<CodecCapabilities> codecCaps); + + std::shared_ptr<CodecCapabilities> getCodecCaps() const; + + int32_t getMaxSupportedInstances() const; + std::string getMediaType() const; + bool isFeatureRequired(const std::string& name) const; + bool isFeatureSupported(const std::string& name) const; + bool isFormatSupported(const sp<AMessage> &format) const; + bool isRegular() const; + +private: + std::shared_ptr<CodecCapabilities> mCodecCaps; +}; + +jobject convertToJavaCodecCapabiliites( + JNIEnv *env, std::shared_ptr<CodecCapabilities> codecCaps); + +} + +#endif // _ANDROID_MEDIA_CODECCAPABILITIES_H_
\ No newline at end of file diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp index 8419ce761a4a..1790670903a4 100644 --- a/media/jni/android_media_MediaCodec.cpp +++ b/media/jni/android_media_MediaCodec.cpp @@ -16,12 +16,14 @@ //#define LOG_NDEBUG 0 #define LOG_TAG "MediaCodec-JNI" +#include <android_media_codec.h> #include <utils/Log.h> #include <type_traits> #include "android_media_MediaCodec.h" +#include "android_media_CodecCapabilities.h" #include "android_media_MediaCodecLinearBlock.h" #include "android_media_MediaCrypto.h" #include "android_media_MediaDescrambler.h" @@ -138,6 +140,8 @@ static struct { static struct { jclass capsClazz; jmethodID capsCtorId; + jclass cpasImplClazz; + jmethodID capsImplCtorId; jclass profileLevelClazz; jfieldID profileField; jfieldID levelField; @@ -996,10 +1000,12 @@ static jobject getCodecCapabilitiesObject( env->SetIntArrayRegion(colorFormatsArray.get(), i, 1, &val); } - return env->NewObject( - gCodecInfo.capsClazz, gCodecInfo.capsCtorId, + jobject javaCodecCapsImpl = env->NewObject( + gCodecInfo.cpasImplClazz, gCodecInfo.capsImplCtorId, profileLevelArray.get(), colorFormatsArray.get(), isEncoder, defaultFormatRef.get(), detailsRef.get()); + + return env->NewObject(gCodecInfo.capsClazz, gCodecInfo.capsCtorId, javaCodecCapsImpl); } status_t JMediaCodec::getCodecInfo(JNIEnv *env, jobject *codecInfoObject) const { @@ -1027,11 +1033,18 @@ status_t JMediaCodec::getCodecInfo(JNIEnv *env, jobject *codecInfoObject) const env->NewObjectArray(mediaTypes.size(), gCodecInfo.capsClazz, NULL)); for (size_t i = 0; i < mediaTypes.size(); i++) { - const sp<MediaCodecInfo::Capabilities> caps = - codecInfo->getCapabilitiesFor(mediaTypes[i].c_str()); - - ScopedLocalRef<jobject> capsObj(env, getCodecCapabilitiesObject( - env, mediaTypes[i].c_str(), isEncoder, caps)); + jobject jCodecCaps = NULL; + if (android::media::codec::provider_->native_capabilites()) { + const std::shared_ptr<CodecCapabilities> codecCaps + = codecInfo->getCodecCapsFor(mediaTypes[i].c_str()); + jCodecCaps = convertToJavaCodecCapabiliites(env, codecCaps); + } else { + const sp<MediaCodecInfo::Capabilities> caps = + codecInfo->getCapabilitiesFor(mediaTypes[i].c_str()); + jCodecCaps = getCodecCapabilitiesObject( + env, mediaTypes[i].c_str(), isEncoder, caps); + } + ScopedLocalRef<jobject> capsObj(env, jCodecCaps); env->SetObjectArrayElement(capsArrayObj.get(), i, capsObj.get()); } @@ -3877,10 +3890,20 @@ static void android_media_MediaCodec_native_init(JNIEnv *env, jclass) { gCodecInfo.capsClazz = (jclass)env->NewGlobalRef(clazz.get()); method = env->GetMethodID(clazz.get(), "<init>", + "(Landroid/media/MediaCodecInfo$CodecCapabilities$CodecCapsIntf;)V"); + CHECK(method != NULL); + gCodecInfo.capsCtorId = method; + + clazz.reset(env->FindClass( + "android/media/MediaCodecInfo$CodecCapabilities$CodecCapsLegacyImpl")); + CHECK(clazz.get() != NULL); + gCodecInfo.cpasImplClazz = (jclass)env->NewGlobalRef(clazz.get()); + + method = env->GetMethodID(clazz.get(), "<init>", "([Landroid/media/MediaCodecInfo$CodecProfileLevel;[IZ" "Ljava/util/Map;Ljava/util/Map;)V"); CHECK(method != NULL); - gCodecInfo.capsCtorId = method; + gCodecInfo.capsImplCtorId = method; clazz.reset(env->FindClass("android/media/MediaCodecInfo$CodecProfileLevel")); CHECK(clazz.get() != NULL); diff --git a/media/jni/android_media_MediaCodecList.cpp b/media/jni/android_media_MediaCodecList.cpp index 07866ac34e4c..3522b35539ab 100644 --- a/media/jni/android_media_MediaCodecList.cpp +++ b/media/jni/android_media_MediaCodecList.cpp @@ -16,6 +16,9 @@ //#define LOG_NDEBUG 0 #define LOG_TAG "MediaCodec-JNI" + +#include <android_media_codec.h> + #include <utils/Log.h> #include <media/stagefright/foundation/ADebug.h> @@ -32,6 +35,7 @@ #include "android_runtime/AndroidRuntime.h" #include "jni.h" #include <nativehelper/JNIHelp.h> +#include "android_media_CodecCapabilities.h" #include "android_media_Streams.h" using namespace android; @@ -245,95 +249,113 @@ static jobject android_media_MediaCodecList_getCodecCapabilities( return NULL; } - Vector<MediaCodecInfo::ProfileLevel> profileLevels; - Vector<uint32_t> colorFormats; + jobject caps; + if (android::media::codec::provider_->native_capabilites()) { + std::shared_ptr<CodecCapabilities> codecCaps = info.info->getCodecCapsFor(typeStr); + caps = android::convertToJavaCodecCapabiliites(env, codecCaps); + } else { + Vector<MediaCodecInfo::ProfileLevel> profileLevels; + Vector<uint32_t> colorFormats; + + sp<AMessage> defaultFormat = new AMessage(); + defaultFormat->setString("mime", typeStr); + + // TODO query default-format also from codec/codec list + const sp<MediaCodecInfo::Capabilities> &capabilities = + info.info->getCapabilitiesFor(typeStr); + env->ReleaseStringUTFChars(type, typeStr); + typeStr = NULL; + if (capabilities == NULL) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return NULL; + } - sp<AMessage> defaultFormat = new AMessage(); - defaultFormat->setString("mime", typeStr); + capabilities->getSupportedColorFormats(&colorFormats); + capabilities->getSupportedProfileLevels(&profileLevels); + sp<AMessage> details = capabilities->getDetails(); + bool isEncoder = info.info->isEncoder(); - // TODO query default-format also from codec/codec list - const sp<MediaCodecInfo::Capabilities> &capabilities = - info.info->getCapabilitiesFor(typeStr); - env->ReleaseStringUTFChars(type, typeStr); - typeStr = NULL; - if (capabilities == NULL) { - jniThrowException(env, "java/lang/IllegalArgumentException", NULL); - return NULL; - } + jobject defaultFormatObj = NULL; + if (ConvertMessageToMap(env, defaultFormat, &defaultFormatObj)) { + return NULL; + } - capabilities->getSupportedColorFormats(&colorFormats); - capabilities->getSupportedProfileLevels(&profileLevels); - sp<AMessage> details = capabilities->getDetails(); - bool isEncoder = info.info->isEncoder(); + jobject infoObj = NULL; + if (ConvertMessageToMap(env, details, &infoObj)) { + env->DeleteLocalRef(defaultFormatObj); + return NULL; + } - jobject defaultFormatObj = NULL; - if (ConvertMessageToMap(env, defaultFormat, &defaultFormatObj)) { - return NULL; - } + jclass capsImplClazz = env->FindClass( + "android/media/MediaCodecInfo$CodecCapabilities$CodecCapsLegacyImpl"); + CHECK(capsImplClazz != NULL); - jobject infoObj = NULL; - if (ConvertMessageToMap(env, details, &infoObj)) { - env->DeleteLocalRef(defaultFormatObj); - return NULL; - } + jclass profileLevelClazz = + env->FindClass("android/media/MediaCodecInfo$CodecProfileLevel"); + CHECK(profileLevelClazz != NULL); - jclass capsClazz = - env->FindClass("android/media/MediaCodecInfo$CodecCapabilities"); - CHECK(capsClazz != NULL); + jobjectArray profileLevelArray = + env->NewObjectArray(profileLevels.size(), profileLevelClazz, NULL); - jclass profileLevelClazz = - env->FindClass("android/media/MediaCodecInfo$CodecProfileLevel"); - CHECK(profileLevelClazz != NULL); + jfieldID profileField = + env->GetFieldID(profileLevelClazz, "profile", "I"); - jobjectArray profileLevelArray = - env->NewObjectArray(profileLevels.size(), profileLevelClazz, NULL); + jfieldID levelField = + env->GetFieldID(profileLevelClazz, "level", "I"); - jfieldID profileField = - env->GetFieldID(profileLevelClazz, "profile", "I"); + for (size_t i = 0; i < profileLevels.size(); ++i) { + const MediaCodecInfo::ProfileLevel &src = profileLevels.itemAt(i); - jfieldID levelField = - env->GetFieldID(profileLevelClazz, "level", "I"); + jobject profileLevelObj = env->AllocObject(profileLevelClazz); - for (size_t i = 0; i < profileLevels.size(); ++i) { - const MediaCodecInfo::ProfileLevel &src = profileLevels.itemAt(i); + env->SetIntField(profileLevelObj, profileField, src.mProfile); + env->SetIntField(profileLevelObj, levelField, src.mLevel); - jobject profileLevelObj = env->AllocObject(profileLevelClazz); + env->SetObjectArrayElement(profileLevelArray, i, profileLevelObj); - env->SetIntField(profileLevelObj, profileField, src.mProfile); - env->SetIntField(profileLevelObj, levelField, src.mLevel); + env->DeleteLocalRef(profileLevelObj); + profileLevelObj = NULL; + } - env->SetObjectArrayElement(profileLevelArray, i, profileLevelObj); + jintArray colorFormatsArray = env->NewIntArray(colorFormats.size()); - env->DeleteLocalRef(profileLevelObj); - profileLevelObj = NULL; - } + for (size_t i = 0; i < colorFormats.size(); ++i) { + jint val = colorFormats.itemAt(i); + env->SetIntArrayRegion(colorFormatsArray, i, 1, &val); + } - jintArray colorFormatsArray = env->NewIntArray(colorFormats.size()); + jmethodID capsImplConstructID = env->GetMethodID(capsImplClazz, "<init>", + "([Landroid/media/MediaCodecInfo$CodecProfileLevel;[IZ" + "Ljava/util/Map;Ljava/util/Map;)V"); - for (size_t i = 0; i < colorFormats.size(); ++i) { - jint val = colorFormats.itemAt(i); - env->SetIntArrayRegion(colorFormatsArray, i, 1, &val); - } + jobject capsImpl = env->NewObject(capsImplClazz, capsImplConstructID, + profileLevelArray, colorFormatsArray, isEncoder, + defaultFormatObj, infoObj); + + jclass capsClazz = env->FindClass( + "android/media/MediaCodecInfo$CodecCapabilities"); + CHECK(capsClazz != NULL); - jmethodID capsConstructID = env->GetMethodID(capsClazz, "<init>", - "([Landroid/media/MediaCodecInfo$CodecProfileLevel;[IZ" - "Ljava/util/Map;Ljava/util/Map;)V"); + jmethodID capsConstructID = env->GetMethodID(capsClazz, "<init>", + "(Landroid/media/MediaCodecInfo$CodecCapabilities$CodecCapsIntf;)V"); - jobject caps = env->NewObject(capsClazz, capsConstructID, - profileLevelArray, colorFormatsArray, isEncoder, - defaultFormatObj, infoObj); + caps = env->NewObject(capsClazz, capsConstructID, capsImpl); - env->DeleteLocalRef(profileLevelArray); - profileLevelArray = NULL; + env->DeleteLocalRef(profileLevelArray); + profileLevelArray = NULL; - env->DeleteLocalRef(colorFormatsArray); - colorFormatsArray = NULL; + env->DeleteLocalRef(colorFormatsArray); + colorFormatsArray = NULL; - env->DeleteLocalRef(defaultFormatObj); - defaultFormatObj = NULL; + env->DeleteLocalRef(defaultFormatObj); + defaultFormatObj = NULL; + + env->DeleteLocalRef(infoObj); + infoObj = NULL; - env->DeleteLocalRef(infoObj); - infoObj = NULL; + env->DeleteLocalRef(capsImpl); + capsImpl = NULL; + } return caps; } diff --git a/media/jni/android_media_MediaPlayer.cpp b/media/jni/android_media_MediaPlayer.cpp index a94230014437..f159282ed2c2 100644 --- a/media/jni/android_media_MediaPlayer.cpp +++ b/media/jni/android_media_MediaPlayer.cpp @@ -1479,6 +1479,7 @@ static int register_android_media_MediaPlayer(JNIEnv *env) extern int register_android_media_ImageReader(JNIEnv *env); extern int register_android_media_ImageWriter(JNIEnv *env); extern int register_android_media_JetPlayer(JNIEnv *env); +extern int register_android_media_CodecCapabilities(JNIEnv *env); extern int register_android_media_Crypto(JNIEnv *env); extern int register_android_media_Drm(JNIEnv *env); extern int register_android_media_Descrambler(JNIEnv *env); @@ -1593,6 +1594,11 @@ jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) goto bail; } + if (register_android_media_CodecCapabilities(env) < 0) { + ALOGE("ERROR: CodecCapabilities native registration failed"); + goto bail; + } + if (register_android_media_Crypto(env) < 0) { ALOGE("ERROR: MediaCodec native registration failed"); goto bail; |