| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package android.media.codec.cts; |
| |
| import static android.media.MediaCodecInfo.CodecProfileLevel.*; |
| import static android.media.MediaFormat.MIMETYPE_VIDEO_AVC; |
| import static android.media.MediaFormat.MIMETYPE_VIDEO_H263; |
| import static android.media.MediaFormat.MIMETYPE_VIDEO_HEVC; |
| import static android.media.MediaFormat.MIMETYPE_VIDEO_MPEG4; |
| import static android.media.MediaFormat.MIMETYPE_VIDEO_VP9; |
| |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| import static org.junit.Assume.assumeTrue; |
| |
| import android.app.ActivityManager; |
| import android.content.Context; |
| import android.hardware.display.DisplayManager; |
| import android.media.MediaCodec; |
| import android.media.MediaCodecInfo; |
| import android.media.MediaCodecInfo.AudioCapabilities; |
| import android.media.MediaCodecInfo.CodecCapabilities; |
| import android.media.MediaCodecInfo.CodecProfileLevel; |
| import android.media.MediaCodecInfo.VideoCapabilities; |
| import android.media.MediaCodecList; |
| import android.media.MediaFormat; |
| import android.media.cts.MediaPlayerTestBase; |
| import android.net.Uri; |
| import android.platform.test.annotations.AppModeFull; |
| import android.util.Log; |
| import android.util.Range; |
| import android.util.Size; |
| import android.view.Display; |
| |
| import androidx.test.ext.junit.runners.AndroidJUnit4; |
| |
| import com.android.compatibility.common.util.ApiLevelUtil; |
| import com.android.compatibility.common.util.MediaUtils; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.Arrays; |
| import java.util.Comparator; |
| import java.util.HashSet; |
| import java.util.Set; |
| import java.util.Vector; |
| import java.util.stream.Stream; |
| |
| /** |
| * Basic validation test of data returned by MediaCodeCapabilities. |
| */ |
| @AppModeFull(reason = "Instant apps cannot access the SD card") |
| @RunWith(AndroidJUnit4.class) |
| public class MediaCodecCapabilitiesTest extends MediaPlayerTestBase { |
| |
| private static final String TAG = "MediaCodecCapabilitiesTest"; |
| private static final String MEDIA_DIR = WorkDir.getMediaDirString(); |
| private static final int PLAY_TIME_MS = 20000; |
| private static final int TIMEOUT_US = 1000000; // 1 sec |
| private static final int IFRAME_INTERVAL = 10; // 10 seconds between I-frames |
| |
| private final MediaCodecList mAllCodecs = |
| new MediaCodecList(MediaCodecList.ALL_CODECS); |
| private final MediaCodecInfo[] mAllInfos = |
| mAllCodecs.getCodecInfos(); |
| |
| private static final String MODULE_NAME = "CtsMediaCodecTestCases"; |
| |
| @Before |
| @Override |
| public void setUp() throws Throwable { |
| super.setUp(); |
| } |
| |
| @After |
| @Override |
| public void tearDown() { |
| super.tearDown(); |
| } |
| |
| // Android device implementations with H.264 encoders, MUST support Baseline Profile Level 3. |
| // SHOULD support Main Profile/ Level 4, if supported the device must also support Main |
| // Profile/Level 4 decoding. |
| @Test |
| public void testH264EncoderProfileAndLevel() throws Exception { |
| if (!MediaUtils.checkEncoder(MIMETYPE_VIDEO_AVC)) { |
| return; // skip |
| } |
| |
| assertTrue( |
| "H.264 must support Baseline Profile Level 3", |
| hasEncoder(MIMETYPE_VIDEO_AVC, AVCProfileBaseline, AVCLevel3)); |
| |
| if (hasEncoder(MIMETYPE_VIDEO_AVC, AVCProfileMain, AVCLevel4)) { |
| assertTrue( |
| "H.264 decoder must support Main Profile Level 4 if it can encode it", |
| hasDecoder(MIMETYPE_VIDEO_AVC, AVCProfileMain, AVCLevel4)); |
| } |
| } |
| |
| // Android device implementations with H.264 decoders, MUST support Baseline Profile Level 3. |
| // Android Television Devices MUST support High Profile Level 4.2. |
| @Test |
| public void testH264DecoderProfileAndLevel() throws Exception { |
| if (!MediaUtils.checkDecoder(MIMETYPE_VIDEO_AVC)) { |
| return; // skip |
| } |
| |
| assertTrue( |
| "H.264 must support Baseline Profile Level 3", |
| hasDecoder(MIMETYPE_VIDEO_AVC, AVCProfileBaseline, AVCLevel3)); |
| |
| if (isTv()) { |
| assertTrue( |
| "H.264 must support High Profile Level 4.2 on TV", |
| checkDecoder(MIMETYPE_VIDEO_AVC, AVCProfileHigh, AVCLevel42)); |
| } |
| } |
| |
| // Android device implementations with H.263 encoders, MUST support Level 45. |
| @Test |
| public void testH263EncoderProfileAndLevel() throws Exception { |
| if (!MediaUtils.checkEncoder(MIMETYPE_VIDEO_H263)) { |
| return; // skip |
| } |
| |
| assertTrue( |
| "H.263 must support Level 45", |
| hasEncoder(MIMETYPE_VIDEO_H263, MPEG4ProfileSimple, H263Level45)); |
| } |
| |
| // Android device implementations with H.263 decoders, MUST support Level 30. |
| @Test |
| public void testH263DecoderProfileAndLevel() throws Exception { |
| if (!MediaUtils.checkDecoder(MIMETYPE_VIDEO_H263)) { |
| return; // skip |
| } |
| |
| assertTrue( |
| "H.263 must support Level 30", |
| hasDecoder(MIMETYPE_VIDEO_H263, MPEG4ProfileSimple, H263Level30)); |
| } |
| |
| // Android device implementations with MPEG-4 decoders, MUST support Simple Profile Level 3. |
| @Test |
| public void testMpeg4DecoderProfileAndLevel() throws Exception { |
| if (!MediaUtils.checkDecoder(MIMETYPE_VIDEO_MPEG4)) { |
| return; // skip |
| } |
| |
| assertTrue( |
| "MPEG-4 must support Simple Profile Level 3", |
| hasDecoder(MIMETYPE_VIDEO_MPEG4, MPEG4ProfileSimple, MPEG4Level3)); |
| } |
| |
| // Android device implementations, when supporting H.265 codec MUST support the Main Profile |
| // Level 3 Main tier. |
| // Android Television Devices MUST support the Main Profile Level 4.1 Main tier. |
| // When the UHD video decoding profile is supported, it MUST support Main10 Level 5 Main |
| // Tier profile. |
| @Test |
| public void testH265DecoderProfileAndLevel() throws Exception { |
| if (!MediaUtils.checkDecoder(MIMETYPE_VIDEO_HEVC)) { |
| return; // skip |
| } |
| |
| assertTrue( |
| "H.265 must support Main Profile Main Tier Level 3", |
| hasDecoder(MIMETYPE_VIDEO_HEVC, HEVCProfileMain, HEVCMainTierLevel3)); |
| |
| if (isTv()) { |
| assertTrue( |
| "H.265 must support Main Profile Main Tier Level 4.1 on TV", |
| hasDecoder(MIMETYPE_VIDEO_HEVC, HEVCProfileMain, HEVCMainTierLevel41)); |
| } |
| |
| if (isTv() && MediaUtils.canDecodeVideo(MIMETYPE_VIDEO_HEVC, 3840, 2160, 30)) { |
| assertTrue( |
| "H.265 must support Main10 Profile Main Tier Level 5 if UHD is supported", |
| hasDecoder(MIMETYPE_VIDEO_HEVC, HEVCProfileMain10, HEVCMainTierLevel5)); |
| } |
| } |
| |
| @Test |
| public void testVp9DecoderCapabilitiesOnTv() throws Exception { |
| if (isTv() && MediaUtils.hasHardwareCodec(MIMETYPE_VIDEO_VP9, /* encode = */ false)) { |
| // CDD [5.3.7.4/T-1-1] |
| assertTrue(MediaUtils.canDecodeVideo(MIMETYPE_VIDEO_VP9, 1920, 1080, 60 /* fps */, |
| VP9Profile0, null, null)); |
| if (MediaUtils.canDecodeVideo(MIMETYPE_VIDEO_VP9, 3840, 2160, 60 /* fps */)) { |
| // CDD [5.3.7.5/T-2-1] |
| assertTrue(MediaUtils.canDecodeVideo(MIMETYPE_VIDEO_VP9, 3840, 2160, 60 /* fps */, |
| VP9Profile0, null, null)); |
| } |
| } |
| } |
| |
| @Test |
| public void testAvcBaseline1() throws Exception { |
| if (!checkDecoder(MIMETYPE_VIDEO_AVC, AVCProfileBaseline, AVCLevel1)) { |
| return; // skip |
| } |
| |
| // TODO: add a test stream |
| MediaUtils.skipTest(TAG, "no test stream"); |
| } |
| |
| @Test |
| public void testAvcBaseline12() throws Exception { |
| if (!checkDecoder(MIMETYPE_VIDEO_AVC, AVCProfileBaseline, AVCLevel12)) { |
| return; // skip |
| } |
| |
| if (checkDecodeWithDefaultPlayer(MIMETYPE_VIDEO_AVC, AVCProfileBaseline, AVCLevel12)) { |
| String urlString = Uri.fromFile(new File(MEDIA_DIR, |
| "media_codec_capabilities_test_avc_baseline12.mp4")).toString(); |
| playVideoWithRetries(urlString, 256, 144, PLAY_TIME_MS); |
| } |
| } |
| |
| @Test |
| public void testAvcBaseline30() throws Exception { |
| if (!checkDecoder(MIMETYPE_VIDEO_AVC, AVCProfileBaseline, AVCLevel3)) { |
| return; // skip |
| } |
| |
| if (checkDecodeWithDefaultPlayer(MIMETYPE_VIDEO_AVC, AVCProfileBaseline, AVCLevel3)) { |
| String urlString = Uri.fromFile(new File(MEDIA_DIR, |
| "media_codec_capabilities_test_avc_baseline30.mp4")).toString(); |
| playVideoWithRetries(urlString, 640, 360, PLAY_TIME_MS); |
| } |
| } |
| |
| private int getMaxDisplayHeight() { |
| return Arrays.stream(mContext.getSystemService(DisplayManager.class).getDisplays()) |
| .map(Display::getSupportedModes) |
| .flatMap(Stream::of) |
| .max(Comparator.comparing(Display.Mode::getPhysicalHeight)) |
| .orElseThrow(() -> new RuntimeException("Failed to determine max height")) |
| .getPhysicalHeight(); |
| } |
| |
| private boolean mustSupportAvcHeight(int videoResolutionHeight) { |
| // https://source.android.com/docs/compatibility/13/android-13-cdd#534_h264 |
| |
| // If the height that is reported by the Display.getSupportedModes() method is equal or |
| // greater than the video resolution, device implementations: |
| // [C-2-1] MUST support the HD 720p video decoding profiles in the following table. |
| // [C-2-2] MUST support the HD 1080p video decoding profiles in the following table. |
| return getMaxDisplayHeight() >= videoResolutionHeight; |
| } |
| |
| @Test |
| public void testAvcHigh31() throws Exception { |
| if (!checkDecoder(MIMETYPE_VIDEO_AVC, AVCProfileHigh, AVCLevel31)) { |
| return; // skip |
| } |
| |
| assumeTrue(mustSupportAvcHeight(720)); |
| |
| if (checkDecodeWithDefaultPlayer(MIMETYPE_VIDEO_AVC, AVCProfileHigh, AVCLevel31)) { |
| String urlString = Uri.fromFile(new File(MEDIA_DIR, |
| "media_codec_capabilities_test_avc_high31.mp4")).toString(); |
| playVideoWithRetries(urlString, 1280, 720, PLAY_TIME_MS); |
| } |
| } |
| |
| @Test |
| public void testAvcHigh40() throws Exception { |
| if (!checkDecoder(MIMETYPE_VIDEO_AVC, AVCProfileHigh, AVCLevel4)) { |
| return; // skip |
| } |
| if (ApiLevelUtil.isBefore(18)) { |
| MediaUtils.skipTest(TAG, "fragmented mp4 not supported"); |
| return; |
| } |
| |
| assumeTrue(mustSupportAvcHeight(1080)); |
| |
| if (checkDecodeWithDefaultPlayer(MIMETYPE_VIDEO_AVC, AVCProfileHigh, AVCLevel4)) { |
| String urlString = Uri.fromFile(new File(MEDIA_DIR, |
| "media_codec_capabilities_test_avc_high40.mp4")).toString(); |
| playVideoWithRetries(urlString, 1920, 1080, PLAY_TIME_MS); |
| } |
| } |
| |
| @Test |
| public void testHevcMain1() throws Exception { |
| if (!checkDecoder(MIMETYPE_VIDEO_HEVC, HEVCProfileMain, HEVCMainTierLevel1)) { |
| return; // skip |
| } |
| |
| // TODO: add a test stream |
| MediaUtils.skipTest(TAG, "no test stream"); |
| } |
| |
| @Test |
| public void testHevcMain2() throws Exception { |
| if (!checkDecoder(MIMETYPE_VIDEO_HEVC, HEVCProfileMain, HEVCMainTierLevel2)) { |
| return; // skip |
| } |
| |
| // TODO: add a test stream |
| MediaUtils.skipTest(TAG, "no test stream"); |
| } |
| |
| @Test |
| public void testHevcMain21() throws Exception { |
| if (!checkDecoder(MIMETYPE_VIDEO_HEVC, HEVCProfileMain, HEVCMainTierLevel21)) { |
| return; // skip |
| } |
| |
| // TODO: add a test stream |
| MediaUtils.skipTest(TAG, "no test stream"); |
| } |
| |
| @Test |
| public void testHevcMain3() throws Exception { |
| if (!checkDecoder(MIMETYPE_VIDEO_HEVC, HEVCProfileMain, HEVCMainTierLevel3)) { |
| return; // skip |
| } |
| |
| // TODO: add a test stream |
| MediaUtils.skipTest(TAG, "no test stream"); |
| } |
| |
| @Test |
| public void testHevcMain31() throws Exception { |
| if (!checkDecoder(MIMETYPE_VIDEO_HEVC, HEVCProfileMain, HEVCMainTierLevel31)) { |
| return; // skip |
| } |
| |
| // TODO: add a test stream |
| MediaUtils.skipTest(TAG, "no test stream"); |
| } |
| |
| @Test |
| public void testHevcMain4() throws Exception { |
| if (!checkDecoder(MIMETYPE_VIDEO_HEVC, HEVCProfileMain, HEVCMainTierLevel4)) { |
| return; // skip |
| } |
| |
| // TODO: add a test stream |
| MediaUtils.skipTest(TAG, "no test stream"); |
| } |
| |
| @Test |
| public void testHevcMain41() throws Exception { |
| if (!checkDecoder(MIMETYPE_VIDEO_HEVC, HEVCProfileMain, HEVCMainTierLevel41)) { |
| return; // skip |
| } |
| |
| // TODO: add a test stream |
| MediaUtils.skipTest(TAG, "no test stream"); |
| } |
| |
| @Test |
| public void testHevcMain5() throws Exception { |
| if (!checkDecoder(MIMETYPE_VIDEO_HEVC, HEVCProfileMain, HEVCMainTierLevel5)) { |
| return; // skip |
| } |
| |
| // TODO: add a test stream |
| MediaUtils.skipTest(TAG, "no test stream"); |
| } |
| |
| @Test |
| public void testHevcMain51() throws Exception { |
| if (!checkDecoder(MIMETYPE_VIDEO_HEVC, HEVCProfileMain, HEVCMainTierLevel51)) { |
| return; // skip |
| } |
| |
| // TODO: add a test stream |
| MediaUtils.skipTest(TAG, "no test stream"); |
| } |
| |
| private boolean checkDecoder(String mime, int profile, int level) { |
| if (!hasDecoder(mime, profile, level)) { |
| MediaUtils.skipTest(TAG, "no " + mime + " decoder for profile " |
| + profile + " and level " + level); |
| return false; |
| } |
| return true; |
| } |
| |
| private boolean hasDecoder(String mime, int profile, int level) { |
| return supports(mime, false /* isEncoder */, profile, level, false /* defaultOnly */); |
| } |
| |
| private boolean hasEncoder(String mime, int profile, int level) { |
| return supports(mime, true /* isEncoder */, profile, level, false /* defaultOnly */); |
| } |
| |
| // Checks whether the default AOSP player can play back a specific profile and level for a |
| // given media type. If it cannot, it automatically logs that the test is skipped. |
| private boolean checkDecodeWithDefaultPlayer(String mime, int profile, int level) { |
| if (!supports(mime, false /* isEncoder */, profile, level, true /* defaultOnly */)) { |
| MediaUtils.skipTest(TAG, "default player cannot test codec"); |
| return false; |
| } |
| return true; |
| } |
| |
| private boolean supports( |
| String mime, boolean isEncoder, int profile, int level, |
| boolean defaultOnly) { |
| MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS); |
| for (MediaCodecInfo info : mcl.getCodecInfos()) { |
| if (isEncoder != info.isEncoder()) { |
| continue; |
| } |
| try { |
| CodecCapabilities caps = info.getCapabilitiesForType(mime); |
| for (CodecProfileLevel pl : caps.profileLevels) { |
| if (pl.profile != profile) { |
| continue; |
| } |
| |
| // H.263 levels are not completely ordered: |
| // Level45 support only implies Level10 support |
| if (mime.equalsIgnoreCase(MIMETYPE_VIDEO_H263)) { |
| if (pl.level != level && pl.level == H263Level45 && level > H263Level10) { |
| continue; |
| } |
| } |
| if (pl.level >= level) { |
| return true; |
| } |
| } |
| // the default AOSP player picks the first codec for a specific mime type, so |
| // we can stop after the first one found |
| if (defaultOnly) { |
| return false; |
| } |
| } catch (IllegalArgumentException e) { |
| } |
| } |
| return false; |
| } |
| |
| private boolean isVideoMime(String mime) { |
| return mime.toLowerCase().startsWith("video/"); |
| } |
| |
| private Set<String> requiredAdaptiveFormats() { |
| Set<String> adaptiveFormats = new HashSet<String>(); |
| adaptiveFormats.add(MediaFormat.MIMETYPE_VIDEO_AVC); |
| adaptiveFormats.add(MediaFormat.MIMETYPE_VIDEO_HEVC); |
| adaptiveFormats.add(MediaFormat.MIMETYPE_VIDEO_VP8); |
| adaptiveFormats.add(MediaFormat.MIMETYPE_VIDEO_VP9); |
| return adaptiveFormats; |
| } |
| |
| @Test |
| public void testHaveAdaptiveVideoDecoderForAllSupportedFormats() { |
| Set<String> supportedFormats = new HashSet<String>(); |
| boolean skipped = true; |
| |
| // gather all supported video formats |
| for (MediaCodecInfo info : mAllInfos) { |
| if (info.isEncoder()) { |
| continue; |
| } |
| for (String mime : info.getSupportedTypes()) { |
| if (isVideoMime(mime)) { |
| supportedFormats.add(mime); |
| } |
| } |
| } |
| |
| // limit to CDD-required formats for now |
| supportedFormats.retainAll(requiredAdaptiveFormats()); |
| |
| // check if there is an adaptive decoder for each |
| for (String mime : supportedFormats) { |
| skipped = false; |
| // implicit assumption that QCIF video is always valid. |
| MediaFormat format = MediaFormat.createVideoFormat(mime, 176, 144); |
| format.setFeatureEnabled(CodecCapabilities.FEATURE_AdaptivePlayback, true); |
| String codec = mAllCodecs.findDecoderForFormat(format); |
| assertTrue( |
| "could not find adaptive decoder for " + mime, codec != null); |
| } |
| if (skipped) { |
| MediaUtils.skipTest("no video decoders that are required to be adaptive found"); |
| } |
| } |
| |
| @Test |
| public void testAllVideoDecodersAreAdaptive() { |
| Set<String> adaptiveFormats = requiredAdaptiveFormats(); |
| boolean skipped = true; |
| for (MediaCodecInfo info : mAllInfos) { |
| if (info.isEncoder()) { |
| continue; |
| } |
| for (String mime : info.getSupportedTypes()) { |
| if (!isVideoMime(mime) |
| // limit to CDD-required formats for now |
| || !adaptiveFormats.contains(mime)) { |
| continue; |
| } |
| skipped = false; |
| CodecCapabilities caps = info.getCapabilitiesForType(mime); |
| assertTrue( |
| info.getName() + " is not adaptive for " + mime, |
| caps.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback)); |
| } |
| } |
| if (skipped) { |
| MediaUtils.skipTest("no video decoders that are required to be adaptive found"); |
| } |
| } |
| |
| private MediaFormat createReasonableVideoFormat( |
| CodecCapabilities caps, String mime, boolean encoder, int width, int height) { |
| VideoCapabilities vidCaps = caps.getVideoCapabilities(); |
| MediaFormat format = MediaFormat.createVideoFormat(mime, width, height); |
| if (encoder) { |
| // bitrate |
| int maxWidth = vidCaps.getSupportedWidths().getUpper(); |
| int maxHeight = vidCaps.getSupportedHeightsFor(width).getUpper(); |
| int maxRate = vidCaps.getSupportedFrameRatesFor(width, height).getUpper().intValue(); |
| int bitrate = vidCaps.getBitrateRange().clamp( |
| (int)(vidCaps.getBitrateRange().getUpper() |
| / Math.sqrt((double)maxWidth * maxHeight / width / height))); |
| Log.i(TAG, "reasonable bitrate for " + width + "x" + height + "@" + maxRate |
| + " " + mime + " = " + bitrate); |
| format.setInteger(format.KEY_BIT_RATE, bitrate); |
| format.setInteger(format.KEY_FRAME_RATE, maxRate); |
| format.setInteger(format.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); |
| } |
| return format; |
| } |
| |
| @Test |
| public void testSecureCodecsAdvertiseSecurePlayback() throws IOException { |
| boolean skipped = true; |
| for (MediaCodecInfo info : mAllInfos) { |
| boolean isEncoder = info.isEncoder(); |
| if (isEncoder || !info.getName().endsWith(".secure")) { |
| continue; |
| } |
| for (String mime : info.getSupportedTypes()) { |
| if (!isVideoMime(mime)) { |
| continue; |
| } |
| skipped = false; |
| CodecCapabilities caps = info.getCapabilitiesForType(mime); |
| assertTrue( |
| info.getName() + " does not advertise secure playback", |
| caps.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback)); |
| } |
| } |
| if (skipped) { |
| MediaUtils.skipTest("no video decoders found ending in .secure"); |
| } |
| } |
| |
| private Size getVideoSizeForTest(VideoCapabilities vidCaps) { |
| Size size = new Size(176, 144); // Use QCIF by default. |
| if (vidCaps != null && !vidCaps.isSizeSupported(size.getWidth(), size.getHeight())) { |
| int minWidth = vidCaps.getSupportedWidths().getLower(); |
| int minHeight = vidCaps.getSupportedHeightsFor(minWidth).getLower(); |
| size = new Size(minWidth, minHeight); |
| } |
| return size; |
| } |
| |
| private MediaFormat createVideoFormatForBitrateMode(String mime, int width, int height, |
| int bitrateMode, CodecCapabilities caps) { |
| MediaCodecInfo.EncoderCapabilities encoderCaps = caps.getEncoderCapabilities(); |
| if (!encoderCaps.isBitrateModeSupported(bitrateMode)) { |
| return null; |
| } |
| |
| VideoCapabilities vidCaps = caps.getVideoCapabilities(); |
| MediaFormat format = MediaFormat.createVideoFormat(mime, width, height); |
| |
| // bitrate |
| int maxWidth = vidCaps.getSupportedWidths().getUpper(); |
| int maxHeight = vidCaps.getSupportedHeightsFor(width).getUpper(); |
| int maxRate = vidCaps.getSupportedFrameRatesFor(width, height).getUpper().intValue(); |
| format.setInteger(MediaFormat.KEY_BITRATE_MODE, bitrateMode); |
| if (bitrateMode == MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ) { |
| int quality = encoderCaps.getQualityRange().getLower(); |
| Log.i(TAG, "reasonable quality for " + width + "x" + height + "@" + maxRate |
| + " " + mime + " = " + quality); |
| format.setInteger(MediaFormat.KEY_QUALITY, quality); |
| } else { |
| int bitrate = vidCaps.getBitrateRange().clamp( |
| (int)(vidCaps.getBitrateRange().getUpper() |
| / Math.sqrt((double)maxWidth * maxHeight / width / height))); |
| Log.i(TAG, "reasonable bitrate for " + width + "x" + height + "@" + maxRate |
| + " " + mime + " = " + bitrate + " mode " + bitrateMode); |
| format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); |
| } |
| format.setInteger(MediaFormat.KEY_FRAME_RATE, maxRate); |
| format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); |
| format.setInteger(MediaFormat.KEY_COLOR_FORMAT, |
| CodecCapabilities.COLOR_FormatYUV420Flexible); |
| |
| return format; |
| } |
| |
| @Test |
| public void testAllAdvertisedVideoEncoderBitrateModes() throws IOException { |
| boolean skipped = true; |
| final int[] modes = { |
| MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ, |
| MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR, |
| MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR |
| }; |
| for (MediaCodecInfo info : mAllInfos) { |
| if (!info.isEncoder()) { |
| continue; |
| } |
| |
| for (String mime: info.getSupportedTypes()) { |
| boolean isVideo = isVideoMime(mime); |
| if (!isVideo) { |
| continue; |
| } |
| CodecCapabilities caps = info.getCapabilitiesForType(mime); |
| Size size = getVideoSizeForTest(caps.getVideoCapabilities()); |
| skipped = false; |
| |
| int numSupportedModes = 0; |
| for (int mode : modes) { |
| MediaFormat format = createVideoFormatForBitrateMode( |
| mime, size.getWidth(), size.getHeight(), mode, caps); |
| if (format == null) { |
| continue; |
| } |
| MediaCodec codec = null; |
| try { |
| codec = MediaCodec.createByCodecName(info.getName()); |
| codec.configure(format, null /* surface */, null /* crypto */, |
| MediaCodec.CONFIGURE_FLAG_ENCODE); |
| } finally { |
| if (codec != null) { |
| codec.release(); |
| } |
| } |
| numSupportedModes++; |
| } |
| assertTrue(info.getName() + " has no supported bitrate mode", |
| numSupportedModes > 0); |
| } |
| } |
| if (skipped) { |
| MediaUtils.skipTest("no video encoders found"); |
| } |
| } |
| |
| @Test |
| public void testAllNonTunneledVideoCodecsSupportFlexibleYUV() throws IOException { |
| boolean skipped = true; |
| for (MediaCodecInfo info : mAllInfos) { |
| boolean isEncoder = info.isEncoder(); |
| for (String mime: info.getSupportedTypes()) { |
| if (!isVideoMime(mime)) { |
| continue; |
| } |
| CodecCapabilities caps = info.getCapabilitiesForType(mime); |
| if (caps.isFeatureRequired(CodecCapabilities.FEATURE_TunneledPlayback) |
| || caps.isFeatureRequired(CodecCapabilities.FEATURE_SecurePlayback)) { |
| continue; |
| } |
| skipped = false; |
| boolean found = false; |
| for (int c : caps.colorFormats) { |
| if (c == caps.COLOR_FormatYUV420Flexible) { |
| found = true; |
| break; |
| } |
| } |
| assertTrue( |
| info.getName() + " does not advertise COLOR_FormatYUV420Flexible", |
| found); |
| |
| MediaCodec codec = null; |
| MediaFormat format = null; |
| try { |
| Size size = getVideoSizeForTest(caps.getVideoCapabilities()); |
| codec = MediaCodec.createByCodecName(info.getName()); |
| format = createReasonableVideoFormat( |
| caps, mime, isEncoder, size.getWidth(), size.getHeight()); |
| format.setInteger( |
| MediaFormat.KEY_COLOR_FORMAT, |
| caps.COLOR_FormatYUV420Flexible); |
| |
| codec.configure(format, null /* surface */, null /* crypto */, |
| isEncoder ? codec.CONFIGURE_FLAG_ENCODE : 0); |
| MediaFormat configuredFormat = |
| isEncoder ? codec.getInputFormat() : codec.getOutputFormat(); |
| Log.d(TAG, "color format is " + configuredFormat.getInteger( |
| MediaFormat.KEY_COLOR_FORMAT)); |
| if (isEncoder) { |
| codec.start(); |
| int ix = codec.dequeueInputBuffer(TIMEOUT_US); |
| assertNotNull( |
| info.getName() + " encoder has non-flexYUV input buffer #" + ix, |
| codec.getInputImage(ix)); |
| } else { |
| // TODO: test these on various decoders (need test streams) |
| } |
| } finally { |
| if (codec != null) { |
| codec.release(); |
| } |
| } |
| } |
| } |
| if (skipped) { |
| MediaUtils.skipTest("no non-tunneled/non-secure video decoders found"); |
| } |
| } |
| |
| private static MediaFormat createMinFormat(String mime, CodecCapabilities caps) { |
| MediaFormat format; |
| if (caps.getVideoCapabilities() != null) { |
| VideoCapabilities vcaps = caps.getVideoCapabilities(); |
| int minWidth = vcaps.getSupportedWidths().getLower(); |
| int minHeight = vcaps.getSupportedHeightsFor(minWidth).getLower(); |
| int minBitrate = vcaps.getBitrateRange().getLower(); |
| int minFrameRate = Math.max(vcaps.getSupportedFrameRatesFor(minWidth, minHeight) |
| .getLower().intValue(), 1); |
| format = MediaFormat.createVideoFormat(mime, minWidth, minHeight); |
| int colorFormat = caps.colorFormats[0]; |
| for (int i = 0; i < caps.colorFormats.length; ++i) { |
| colorFormat = caps.colorFormats[i]; |
| // Avoid COLOR_FormatSurface as we will be configuring the codec without a surface. |
| if (colorFormat != CodecCapabilities.COLOR_FormatSurface) { |
| break; |
| } |
| } |
| format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat); |
| format.setInteger(MediaFormat.KEY_BIT_RATE, minBitrate); |
| format.setInteger(MediaFormat.KEY_FRAME_RATE, minFrameRate); |
| format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); |
| } else { |
| AudioCapabilities acaps = caps.getAudioCapabilities(); |
| int minSampleRate = acaps.getSupportedSampleRateRanges()[0].getLower(); |
| int minChannelCount = 1; |
| int minBitrate = acaps.getBitrateRange().getLower(); |
| format = MediaFormat.createAudioFormat(mime, minSampleRate, minChannelCount); |
| format.setInteger(MediaFormat.KEY_BIT_RATE, minBitrate); |
| } |
| |
| return format; |
| } |
| |
| private int getActualMax( |
| boolean isEncoder, String name, String mime, CodecCapabilities caps, int max) { |
| int flag = isEncoder ? MediaCodec.CONFIGURE_FLAG_ENCODE : 0; |
| boolean memory_limited = false; |
| MediaFormat format = createMinFormat(mime, caps); |
| Log.d(TAG, "Test format " + format); |
| Vector<MediaCodec> codecs = new Vector<MediaCodec>(); |
| MediaCodec codec = null; |
| ActivityManager am = (ActivityManager) |
| mContext.getSystemService(Context.ACTIVITY_SERVICE); |
| ActivityManager.MemoryInfo outInfo = new ActivityManager.MemoryInfo(); |
| for (int i = 0; i < max; ++i) { |
| try { |
| Log.d(TAG, "Create codec " + name + " #" + i); |
| codec = MediaCodec.createByCodecName(name); |
| codec.configure(format, null, null, flag); |
| codec.start(); |
| codecs.add(codec); |
| codec = null; |
| |
| am.getMemoryInfo(outInfo); |
| if (outInfo.lowMemory) { |
| Log.d(TAG, "System is in low memory condition, stopping. max: " + i); |
| memory_limited = true; |
| break; |
| } |
| } catch (IllegalArgumentException e) { |
| fail("Got unexpected IllegalArgumentException " + e.getMessage()); |
| } catch (IOException e) { |
| fail("Got unexpected IOException " + e.getMessage()); |
| } catch (MediaCodec.CodecException e) { |
| // ERROR_INSUFFICIENT_RESOURCE is expected as the test keep creating codecs. |
| // But other exception should be treated as failure. |
| if (e.getErrorCode() == MediaCodec.CodecException.ERROR_INSUFFICIENT_RESOURCE) { |
| Log.d(TAG, "Got CodecException with ERROR_INSUFFICIENT_RESOURCE."); |
| break; |
| } else { |
| fail("Unexpected CodecException " + e.getDiagnosticInfo()); |
| } |
| } finally { |
| if (codec != null) { |
| Log.d(TAG, "release codec"); |
| codec.release(); |
| codec = null; |
| } |
| } |
| } |
| int actualMax = codecs.size(); |
| for (int i = 0; i < codecs.size(); ++i) { |
| Log.d(TAG, "release codec #" + i); |
| codecs.get(i).release(); |
| } |
| codecs.clear(); |
| // encode both actual max and whether we ran out of memory |
| if (memory_limited) { |
| actualMax = -actualMax; |
| } |
| return actualMax; |
| } |
| |
| private boolean knownTypes(String type) { |
| return (type.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AAC ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC3 ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_NB ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_WB ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3 ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_FLAC ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_ALAW) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_MLAW) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MPEG ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MSGSM ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_OPUS ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_RAW ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_VORBIS ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AV1 ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AVC ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263 ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG2 ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG4 ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP8 ) || |
| type.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP9 )); |
| } |
| |
| @Test |
| public void testGetMaxSupportedInstances() { |
| StringBuilder xmlOverrides = new StringBuilder(); |
| MediaCodecList allCodecs = new MediaCodecList(MediaCodecList.ALL_CODECS); |
| final boolean isLowRam = ActivityManager.isLowRamDeviceStatic(); |
| for (MediaCodecInfo info : allCodecs.getCodecInfos()) { |
| Log.d(TAG, "codec: " + info.getName()); |
| Log.d(TAG, " isEncoder = " + info.isEncoder()); |
| |
| // don't bother testing aliases |
| if (info.isAlias()) { |
| Log.d(TAG, "skipping: " + info.getName() + " is an alias for " + |
| info.getCanonicalName()); |
| continue; |
| } |
| |
| String[] types = info.getSupportedTypes(); |
| for (int j = 0; j < types.length; ++j) { |
| if (!knownTypes(types[j])) { |
| Log.d(TAG, "skipping unknown type " + types[j]); |
| continue; |
| } |
| Log.d(TAG, "calling getCapabilitiesForType " + types[j]); |
| CodecCapabilities caps = info.getCapabilitiesForType(types[j]); |
| int advertised = caps.getMaxSupportedInstances(); |
| Log.d(TAG, "getMaxSupportedInstances returns " + advertised); |
| assertTrue(advertised > 0); |
| |
| // see how well the declared max matches against reality |
| |
| int tryMax = isLowRam ? 16 : 32; |
| int tryMin = isLowRam ? 4 : 16; |
| |
| int trials = Math.min(advertised + 2, tryMax); |
| int actualMax = getActualMax( |
| info.isEncoder(), info.getName(), types[j], caps, trials); |
| Log.d(TAG, "actualMax " + actualMax + " vs advertised " + advertised |
| + " tryMin " + tryMin + " tryMax " + tryMax); |
| |
| boolean memory_limited = false; |
| if (actualMax < 0) { |
| memory_limited = true; |
| actualMax = -actualMax; |
| } |
| |
| boolean compliant = true; |
| if (info.isHardwareAccelerated()) { |
| // very specific bounds for HW codecs |
| // so the adv+2 above is to see if the HW codec lets us go beyond adv |
| // (it should not) |
| if (actualMax != Math.min(advertised, tryMax)) { |
| Log.d(TAG, "NO: hwcodec " + actualMax + " != min(" + advertised + |
| "," + tryMax + ")"); |
| compliant = false; |
| } |
| } else { |
| // sw codecs get a little more relaxation due to memory pressure |
| if (actualMax >= Math.min(advertised, tryMax)) { |
| // no memory issues, and we allocated them all |
| Log.d(TAG, "OK: swcodec " + actualMax + " >= min(" + advertised + |
| "," + tryMax + ")"); |
| } else if (actualMax >= Math.min(advertised, tryMin) && |
| memory_limited) { |
| // memory issues, but we hit our floors |
| Log.d(TAG, "OK: swcodec " + actualMax + " >= min(" + advertised + |
| "," + tryMin + ") + memory limited"); |
| } else { |
| Log.d(TAG, "NO: swcodec didn't meet criteria"); |
| compliant = false; |
| } |
| } |
| |
| if (!compliant) { |
| String codec = "<MediaCodec name=\"" + info.getName() + |
| "\" type=\"" + types[j] + "\" >"; |
| String limit = " <Limit name=\"concurrent-instances\" max=\"" + |
| actualMax + "\" />"; |
| xmlOverrides.append(codec); |
| xmlOverrides.append("\n"); |
| xmlOverrides.append(limit); |
| xmlOverrides.append("\n"); |
| xmlOverrides.append("</MediaCodec>\n"); |
| } |
| } |
| } |
| |
| if (xmlOverrides.length() > 0) { |
| String failMessage = "In order to pass the test, please publish following " + |
| "codecs' concurrent instances limit in /etc/media_codecs.xml: \n"; |
| fail(failMessage + xmlOverrides.toString()); |
| } |
| } |
| |
| @Test |
| public void testGetSupportedFrameRates() throws IOException { |
| // Chose MediaFormat.MIMETYPE_VIDEO_H263 randomly |
| CodecCapabilities codecCap = CodecCapabilities.createFromProfileLevel( |
| MediaFormat.MIMETYPE_VIDEO_H263, H263ProfileBaseline, H263Level45); |
| Range<Integer> supportedFrameRates = |
| codecCap.getVideoCapabilities().getSupportedFrameRates(); |
| Log.d(TAG, "Supported Frame Rates : " + supportedFrameRates.toString()); |
| /* |
| ITU-T Rec. H.263/Annex X (03/2004) says that for H263ProfileBaseline and H263Level45, |
| the device has to support 15 fps. |
| */ |
| assertTrue("Invalid framerate range", Range.create(1, 15).equals(supportedFrameRates)); |
| } |
| |
| @Test |
| public void testIsSampleRateSupported() throws IOException { |
| if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_AUDIO_AAC)) { |
| return; // skip |
| } |
| // Chose AAC Decoder/MediaFormat.MIMETYPE_AUDIO_AAC randomly |
| MediaCodec codec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_AUDIO_AAC); |
| MediaCodecInfo.AudioCapabilities audioCap = codec.getCodecInfo() |
| .getCapabilitiesForType(MediaFormat.MIMETYPE_AUDIO_AAC).getAudioCapabilities(); |
| final int[] validSampleRates = {8000, 16000, 22050, 44100}; |
| for(int sampleRate : validSampleRates) { |
| Log.d(TAG, "SampleRate = " + sampleRate); |
| assertTrue("Expected True for isSampleRateSupported", |
| audioCap.isSampleRateSupported(sampleRate)); |
| } |
| final int[] invalidSampleRates = {-1, 0, 1, Integer.MAX_VALUE}; |
| for(int sampleRate : invalidSampleRates) { |
| Log.d(TAG, "SampleRate = " + sampleRate); |
| assertFalse("Expected False for isSampleRateSupported", |
| audioCap.isSampleRateSupported(sampleRate)); |
| } |
| codec.release(); |
| } |
| |
| // API test coverage for MediaCodecInfo.EncoderCapabilities.getComplexityRange() |
| @Test |
| public void testGetComplexityRange() throws IOException { |
| boolean skipTest = true; |
| if (MediaUtils.hasEncoder(MediaFormat.MIMETYPE_AUDIO_AAC)) { |
| // Chose AAC Encoder/MediaFormat.MIMETYPE_AUDIO_AAC randomly |
| MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC); |
| Range<Integer> complexityRange = |
| codec.getCodecInfo() |
| .getCapabilitiesForType(MediaFormat.MIMETYPE_AUDIO_AAC) |
| .getEncoderCapabilities() |
| .getComplexityRange(); |
| Log.d(TAG, "AAC ComplexityRange : " + complexityRange.toString()); |
| assertTrue("AAC ComplexityRange invalid low value", complexityRange.getLower() >= 0); |
| codec.release(); |
| skipTest = false; |
| } |
| if (MediaUtils.hasEncoder(MediaFormat.MIMETYPE_AUDIO_FLAC)) { |
| // Repeat test with FLAC Encoder |
| MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_FLAC); |
| Range<Integer> complexityRange = |
| codec.getCodecInfo() |
| .getCapabilitiesForType(MediaFormat.MIMETYPE_AUDIO_FLAC) |
| .getEncoderCapabilities() |
| .getComplexityRange(); |
| Log.d(TAG, "FLAC ComplexityRange : " + complexityRange.toString()); |
| assertTrue("FLAC ComplexityRange invalid low value", complexityRange.getLower() >= 0); |
| codec.release(); |
| skipTest = false; |
| } |
| if (skipTest) { |
| MediaUtils.skipTest(TAG, "AAC and FLAC encoders not present"); |
| } |
| } |
| |
| @Test |
| public void testLowLatencyFeatureIsSupportedOnly() throws IOException { |
| MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS); |
| for (MediaCodecInfo info : list.getCodecInfos()) { |
| for (String type : info.getSupportedTypes()) { |
| CodecCapabilities caps = info.getCapabilitiesForType(type); |
| if (caps.isFeatureSupported(CodecCapabilities.FEATURE_LowLatency)) { |
| assertFalse( |
| info.getName() + "(" + type + ") " |
| + " supports low latency, but low latency shall not be required", |
| caps.isFeatureRequired(CodecCapabilities.FEATURE_LowLatency)); |
| } |
| } |
| } |
| } |
| } |