1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.videocodec.cts; 18 19 import static android.media.MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR; 20 import static android.media.MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR; 21 import static android.mediav2.common.cts.CodecTestBase.ComponentClass.HARDWARE; 22 23 import static org.junit.Assert.assertEquals; 24 import static org.junit.Assert.assertTrue; 25 import static org.junit.Assume.assumeNotNull; 26 import static org.junit.Assume.assumeTrue; 27 28 import android.media.MediaCodecInfo; 29 import android.media.MediaFormat; 30 import android.mediav2.common.cts.CompareStreams; 31 import android.mediav2.common.cts.EncoderConfigParams; 32 import android.mediav2.common.cts.RawResource; 33 34 import com.android.compatibility.common.util.ApiTest; 35 36 import org.junit.AfterClass; 37 import org.junit.Before; 38 import org.junit.BeforeClass; 39 import org.junit.Test; 40 import org.junit.runner.RunWith; 41 import org.junit.runners.Parameterized; 42 43 import java.io.File; 44 import java.io.IOException; 45 import java.util.ArrayList; 46 import java.util.Collection; 47 import java.util.HashMap; 48 import java.util.List; 49 50 /** 51 * 1. MinMaxResolutionsTest should query the ranges of supported width and height using 52 * MediaCodecInfo.VideoCapabilities, test the min resolution and the max resolution of the encoder. 53 * <p> Test Params: Input configuration = Resolution: min/max, frame rate: 30, bitrate: choose 54 * basing on resolution </p> 55 * 2. MinMaxBitrateTest should query the range of the supported bitrates, and test min/max of them. 56 * <p> Test Params: Input configuration = Resolution: choose basing on bitrate, frame rate: 30, 57 * bitrate: min/max </p> 58 * 3. MinMaxFrameRatesTest should query the range of the supported frame rates, and test min/max 59 * of them. 60 * <p> Test Params: Input configuration = Resolution: 720p, frame rate: min/max, bitrate: 5mbps 61 * </p> 62 * All tests should run for following combinations: 63 * <p>Bitrate mode = CBR/VBR, MaxBFrames = 0/1, Codec type = AVC/HEVC, Intra frame interval = 0/1 64 * second</p> 65 */ 66 @RunWith(Parameterized.class) 67 public class VideoEncoderMinMaxTest extends VideoEncoderValidationTestBase { 68 private static final String LOG_TAG = VideoEncoderMinMaxTest.class.getSimpleName(); 69 private static final float MIN_ACCEPTABLE_QUALITY = 20.0f; // psnr in dB 70 private static final int FRAME_LIMIT = 300; 71 private static final int TARGET_WIDTH = 1280; 72 private static final int TARGET_HEIGHT = 720; 73 private static final int TARGET_FRAME_RATE = 30; 74 private static final int TARGET_BIT_RATE = 5000000; 75 private static final List<Object[]> exhaustiveArgsList = new ArrayList<>(); 76 private static final HashMap<String, RawResource> RES_YUV_MAP = new HashMap<>(); 77 78 @BeforeClass decodeResourcesToYuv()79 public static void decodeResourcesToYuv() { 80 ArrayList<CompressedResource> resources = new ArrayList<>(); 81 for (Object[] arg : exhaustiveArgsList) { 82 resources.add((CompressedResource) arg[2]); 83 } 84 decodeStreamsToYuv(resources, RES_YUV_MAP, LOG_TAG); 85 } 86 87 @AfterClass cleanUpResources()88 public static void cleanUpResources() { 89 for (RawResource res : RES_YUV_MAP.values()) { 90 new File(res.mFileName).delete(); 91 } 92 } 93 getVideoEncoderCfgParams(String mediaType, int bitRateMode, int maxBFrames, int intraInterval)94 private static EncoderConfigParams getVideoEncoderCfgParams(String mediaType, int bitRateMode, 95 int maxBFrames, int intraInterval) { 96 return new EncoderConfigParams.Builder(mediaType) 97 .setWidth(TARGET_WIDTH) 98 .setHeight(TARGET_HEIGHT) 99 .setBitRate(TARGET_BIT_RATE) 100 .setBitRateMode(bitRateMode) 101 .setMaxBFrames(maxBFrames) 102 .setKeyFrameInterval(intraInterval) 103 .setFrameRate(TARGET_FRAME_RATE) 104 .build(); 105 } 106 addParams()107 private static void addParams() { 108 final String[] mediaTypes = new String[]{MediaFormat.MIMETYPE_VIDEO_AVC, 109 MediaFormat.MIMETYPE_VIDEO_HEVC}; 110 final int[] maxBFramesPerSubGop = new int[]{0, 1}; 111 final int[] intraIntervals = new int[]{0, 1}; 112 final int[] bitRateModes = new int[]{BITRATE_MODE_CBR, BITRATE_MODE_VBR}; 113 for (String mediaType : mediaTypes) { 114 for (int maxBFrames : maxBFramesPerSubGop) { 115 for (int intraInterval : intraIntervals) { 116 for (int bitRateMode : bitRateModes) { 117 // mediaType, cfg, resource file 118 exhaustiveArgsList.add( 119 new Object[]{mediaType, getVideoEncoderCfgParams(mediaType, 120 bitRateMode, maxBFrames, 121 intraInterval), BIRTHDAY_FULLHD_LANDSCAPE}); 122 } 123 } 124 } 125 } 126 } 127 applyMinMaxRanges(MediaCodecInfo.VideoCapabilities caps, Object cfgObject)128 private static List<Object> applyMinMaxRanges(MediaCodecInfo.VideoCapabilities caps, 129 Object cfgObject) throws CloneNotSupportedException { 130 List<Object> cfgObjects = new ArrayList<>(); 131 EncoderConfigParams cfgParam = (EncoderConfigParams) cfgObject; 132 int minW = caps.getSupportedWidths().getLower(); 133 int minHForMinW = caps.getSupportedHeightsFor(minW).getLower(); 134 int maxHForMinW = caps.getSupportedHeightsFor(minW).getUpper(); 135 int minH = caps.getSupportedHeights().getLower(); 136 int minWForMinH = caps.getSupportedWidthsFor(minH).getLower(); 137 int maxWForMinH = caps.getSupportedWidthsFor(minH).getUpper(); 138 int maxW = caps.getSupportedWidths().getUpper(); 139 int minHForMaxW = caps.getSupportedHeightsFor(maxW).getLower(); 140 int maxHForMaxW = caps.getSupportedHeightsFor(maxW).getUpper(); 141 int maxH = caps.getSupportedHeights().getUpper(); 142 int minWForMaxH = caps.getSupportedWidthsFor(maxH).getLower(); 143 int maxWForMaxH = caps.getSupportedWidthsFor(maxH).getUpper(); 144 int minBitRate = caps.getBitrateRange().getLower(); 145 int maxBitRate = caps.getBitrateRange().getUpper(); 146 147 // min max res & bitrate tests 148 android.util.Range<Double> rates = caps.getSupportedFrameRatesFor(minW, minHForMinW); 149 int frameRate = rates.clamp((double) TARGET_FRAME_RATE).intValue(); 150 cfgObjects.add(cfgParam.getBuilder().setWidth(minW).setHeight(minHForMinW) 151 .setFrameRate(frameRate).setBitRate(minBitRate).build()); 152 rates = caps.getSupportedFrameRatesFor(maxW, maxHForMaxW); 153 frameRate = rates.clamp((double) TARGET_FRAME_RATE).intValue(); 154 cfgObjects.add(cfgParam.getBuilder().setWidth(maxW).setHeight(maxHForMaxW) 155 .setFrameRate(frameRate).setBitRate(maxBitRate).build()); 156 int bitrate; 157 if (minW != minWForMinH || minH != minHForMinW) { 158 rates = caps.getSupportedFrameRatesFor(minWForMinH, minH); 159 frameRate = rates.clamp((double) TARGET_FRAME_RATE).intValue(); 160 bitrate = caps.getBitrateRange().clamp((int) (maxBitRate / Math.sqrt( 161 (double) maxW * maxHForMaxW / minWForMinH / minH))); 162 cfgObjects.add(cfgParam.getBuilder().setWidth(minWForMinH).setHeight(minH) 163 .setFrameRate(frameRate).setBitRate(bitrate).build()); 164 } 165 if (maxW != maxWForMaxH || maxH != maxHForMaxW) { 166 rates = caps.getSupportedFrameRatesFor(maxWForMaxH, maxH); 167 frameRate = rates.clamp((double) TARGET_FRAME_RATE).intValue(); 168 bitrate = caps.getBitrateRange().clamp((int) (maxBitRate / Math.sqrt( 169 (double) maxW * maxHForMaxW / maxWForMaxH / maxH))); 170 cfgObjects.add(cfgParam.getBuilder().setWidth(maxWForMaxH).setHeight(maxH) 171 .setFrameRate(frameRate).setBitRate(bitrate).build()); 172 } 173 174 rates = caps.getSupportedFrameRatesFor(minW, maxHForMinW); 175 frameRate = rates.clamp((double) TARGET_FRAME_RATE).intValue(); 176 bitrate = caps.getBitrateRange().clamp((int) (maxBitRate / Math.sqrt( 177 (double) maxW * maxHForMaxW / minW / maxHForMinW))); 178 cfgObjects.add(cfgParam.getBuilder().setWidth(minW).setHeight(maxHForMinW) 179 .setFrameRate(frameRate).setBitRate(bitrate).build()); 180 181 rates = caps.getSupportedFrameRatesFor(maxWForMinH, minH); 182 frameRate = rates.clamp((double) TARGET_FRAME_RATE).intValue(); 183 bitrate = caps.getBitrateRange().clamp((int) (maxBitRate / Math.sqrt( 184 (double) maxW * maxHForMaxW / maxWForMinH / minH))); 185 cfgObjects.add(cfgParam.getBuilder().setWidth(maxWForMinH).setHeight(minH) 186 .setFrameRate(frameRate).setBitRate(bitrate).build()); 187 if (maxW != maxWForMinH || minH != minHForMaxW) { 188 rates = caps.getSupportedFrameRatesFor(maxW, minHForMaxW); 189 frameRate = rates.clamp((double) TARGET_FRAME_RATE).intValue(); 190 bitrate = caps.getBitrateRange().clamp((int) (maxBitRate / Math.sqrt( 191 (double) maxW * maxHForMaxW / maxW / minHForMaxW))); 192 cfgObjects.add(cfgParam.getBuilder().setWidth(maxW).setHeight(minHForMaxW) 193 .setFrameRate(frameRate).setBitRate(bitrate).build()); 194 } 195 if (minW != minWForMaxH || maxH != maxHForMinW) { 196 rates = caps.getSupportedFrameRatesFor(minWForMaxH, maxH); 197 frameRate = rates.clamp((double) TARGET_FRAME_RATE).intValue(); 198 bitrate = caps.getBitrateRange().clamp((int) (maxBitRate / Math.sqrt( 199 (double) maxW * maxHForMaxW / minWForMaxH / maxH))); 200 cfgObjects.add(cfgParam.getBuilder().setWidth(minWForMaxH).setHeight(maxH) 201 .setFrameRate(frameRate).setBitRate(bitrate).build()); 202 } 203 204 // min-max frame rate tests 205 try { 206 int minFps = caps.getSupportedFrameRatesFor(TARGET_WIDTH, TARGET_HEIGHT).getLower() 207 .intValue(); 208 cfgObjects.add(cfgParam.getBuilder().setFrameRate(minFps).build()); 209 } catch (IllegalArgumentException ignored) { 210 } 211 try { 212 int maxFps = caps.getSupportedFrameRatesFor(TARGET_WIDTH, TARGET_HEIGHT).getUpper() 213 .intValue(); 214 cfgObjects.add(cfgParam.getBuilder().setFrameRate(maxFps).build()); 215 } catch (IllegalArgumentException ignored) { 216 } 217 218 return cfgObjects; 219 } 220 getCodecInfo(String codecName, String mediaType)221 private static MediaCodecInfo getCodecInfo(String codecName, String mediaType) { 222 for (MediaCodecInfo info : MEDIA_CODEC_LIST_REGULAR.getCodecInfos()) { 223 if (info.getName().equals(codecName)) { 224 for (String type : info.getSupportedTypes()) { 225 if (mediaType.equals(type)) { 226 return info; 227 } 228 } 229 } 230 } 231 return null; 232 } 233 getMinMaxRangeCfgObjects(Object codecName, Object mediaType, Object cfgObject)234 private static List<Object> getMinMaxRangeCfgObjects(Object codecName, Object mediaType, 235 Object cfgObject) throws CloneNotSupportedException { 236 MediaCodecInfo info = getCodecInfo((String) codecName, (String) mediaType); 237 MediaCodecInfo.CodecCapabilities caps = info.getCapabilitiesForType((String) mediaType); 238 return applyMinMaxRanges(caps.getVideoCapabilities(), cfgObject); 239 } 240 updateParamList(Collection<Object[]> paramList)241 private static Collection<Object[]> updateParamList(Collection<Object[]> paramList) 242 throws CloneNotSupportedException { 243 Collection<Object[]> newParamList = new ArrayList<>(); 244 for (Object[] arg : paramList) { 245 List<Object> cfgObjects = getMinMaxRangeCfgObjects(arg[0], arg[1], arg[2]); 246 for (Object obj : cfgObjects) { 247 Object[] argUpdate = new Object[arg.length + 1]; 248 System.arraycopy(arg, 0, argUpdate, 0, arg.length); 249 argUpdate[2] = obj; 250 EncoderConfigParams cfgVar = (EncoderConfigParams) obj; 251 String label = String.format("%.2fmbps_%dx%d_%dfps_maxb-%d_%s_i-dist-%d", 252 cfgVar.mBitRate / 1000000., cfgVar.mWidth, cfgVar.mHeight, 253 cfgVar.mFrameRate, cfgVar.mMaxBFrames, 254 bitRateModeToString(cfgVar.mBitRateMode), (int) cfgVar.mKeyFrameInterval); 255 argUpdate[arg.length - 1] = label; 256 argUpdate[arg.length] = paramToString(argUpdate); 257 newParamList.add(argUpdate); 258 } 259 } 260 return newParamList; 261 } 262 263 @Parameterized.Parameters(name = "{index}_{0}_{1}_{4}") input()264 public static Collection<Object[]> input() throws CloneNotSupportedException { 265 addParams(); 266 return updateParamList(prepareParamList(exhaustiveArgsList, true, false, true, false, 267 HARDWARE)); 268 } 269 VideoEncoderMinMaxTest(String encoder, String mediaType, EncoderConfigParams cfgParams, CompressedResource res, @SuppressWarnings("unused") String testLabel, String allTestParams)270 public VideoEncoderMinMaxTest(String encoder, String mediaType, EncoderConfigParams cfgParams, 271 CompressedResource res, @SuppressWarnings("unused") String testLabel, 272 String allTestParams) { 273 super(encoder, mediaType, cfgParams, res, allTestParams); 274 } 275 276 @Before setUp()277 public void setUp() { 278 mIsLoopBack = true; 279 } 280 281 @ApiTest(apis = {"VideoCapabilities#getSupportedWidths", 282 "VideoCapabilities#getSupportedHeightsFor", 283 "VideoCapabilities#getSupportedWidthsFor", 284 "VideoCapabilities#getSupportedHeights", 285 "VideoCapabilities#getSupportedFrameRatesFor", 286 "VideoCapabilities#getBitrateRange", 287 "android.media.MediaFormat#KEY_WIDTH", 288 "android.media.MediaFormat#KEY_HEIGHT", 289 "android.media.MediaFormat#KEY_BITRATE", 290 "android.media.MediaFormat#KEY_FRAME_RATE"}) 291 @Test testMinMaxSupport()292 public void testMinMaxSupport() throws IOException, InterruptedException { 293 MediaFormat format = mEncCfgParams[0].getFormat(); 294 MediaCodecInfo info = getCodecInfo(mCodecName, mMediaType); 295 assumeTrue(mCodecName + " does not support bitrate mode : " + bitRateModeToString( 296 mEncCfgParams[0].mBitRateMode), 297 info.getCapabilitiesForType(mMediaType).getEncoderCapabilities() 298 .isBitrateModeSupported(mEncCfgParams[0].mBitRateMode)); 299 ArrayList<MediaFormat> formats = new ArrayList<>(); 300 formats.add(format); 301 assertTrue("Encoder: " + mCodecName + " doesn't support format: " + format, 302 areFormatsSupported(mCodecName, mMediaType, formats)); 303 RawResource res = RES_YUV_MAP.getOrDefault(mCRes.uniqueLabel(), null); 304 assumeNotNull("no raw resource found for testing config : " + mEncCfgParams[0] 305 + mTestConfig + mTestEnv + DIAGNOSTICS, res); 306 encodeToMemory(mCodecName, mEncCfgParams[0], res, FRAME_LIMIT, false, true); 307 CompareStreams cs = null; 308 StringBuilder msg = new StringBuilder(); 309 boolean isOk = true; 310 try { 311 cs = new CompareStreams(res, mMediaType, mMuxedOutputFile, true, mIsLoopBack); 312 final double[] minPSNR = cs.getMinimumPSNR(); 313 for (int i = 0; i < minPSNR.length; i++) { 314 if (minPSNR[i] < MIN_ACCEPTABLE_QUALITY) { 315 msg.append(String.format("For %d plane, minPSNR is less than tolerance" 316 + " threshold, Got %f, Threshold %f", i, minPSNR[i], 317 MIN_ACCEPTABLE_QUALITY)); 318 isOk = false; 319 break; 320 } 321 } 322 } finally { 323 if (cs != null) cs.cleanUp(); 324 } 325 assertEquals("encoder did not encode the requested number of frames \n" 326 + mTestConfig + mTestEnv, FRAME_LIMIT, mOutputCount); 327 assertTrue("Encountered frames with PSNR less than configured threshold " 328 + MIN_ACCEPTABLE_QUALITY + "dB \n" + msg + mTestConfig + mTestEnv, isOk); 329 } 330 } 331