1 /* 2 * Copyright (C) 2017 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.media.cts.bitstreams.app; 18 19 import android.app.Instrumentation; 20 import android.content.Context; 21 import android.content.SharedPreferences; 22 import android.content.SharedPreferences.Editor; 23 import android.media.MediaCodec; 24 import android.media.MediaCodecInfo.CodecProfileLevel; 25 import android.media.MediaExtractor; 26 import android.media.MediaFormat; 27 import android.media.cts.bitstreams.MediaBitstreams; 28 import android.os.Bundle; 29 import android.os.Debug; 30 import android.os.Environment; 31 import android.util.Xml; 32 33 import androidx.test.InstrumentationRegistry; 34 35 import com.android.compatibility.common.util.DynamicConfigDeviceSide; 36 import com.android.compatibility.common.util.MediaUtils; 37 38 import org.junit.BeforeClass; 39 import org.junit.Test; 40 import org.junit.runner.RunWith; 41 import org.junit.runners.JUnit4; 42 import org.xmlpull.v1.XmlSerializer; 43 44 import java.io.ByteArrayOutputStream; 45 import java.io.File; 46 import java.io.FileNotFoundException; 47 import java.io.FileOutputStream; 48 import java.io.IOException; 49 import java.io.OutputStream; 50 import java.io.PrintStream; 51 import java.nio.file.Files; 52 import java.util.ArrayList; 53 import java.util.List; 54 import java.util.Scanner; 55 import java.util.concurrent.Callable; 56 import java.util.concurrent.ExecutorService; 57 import java.util.concurrent.Executors; 58 import java.util.concurrent.Future; 59 import java.util.concurrent.TimeUnit; 60 61 /** 62 * Test class that uses device-side media APIs to determine up to which resolution MediaPreparer 63 * should copy media files for CtsMediaStressTestCases. 64 */ 65 @RunWith(JUnit4.class) 66 public class MediaBitstreamsDeviceSideTest { 67 68 private static final String KEY_SIZE = "size"; 69 private static final String UTF_8 = "utf-8"; 70 /** Instrumentation status code used to write resolution to metrics */ 71 private static final int INST_STATUS_IN_PROGRESS = 2; 72 73 private static File mAppCache = InstrumentationRegistry.getContext().getExternalCacheDir(); 74 private static String mDeviceBitstreamsPath = InstrumentationRegistry.getArguments().getString( 75 MediaBitstreams.OPT_DEVICE_BITSTREAMS_PATH, 76 MediaBitstreams.DEFAULT_DEVICE_BITSTEAMS_PATH); 77 78 @BeforeClass setUp()79 public static void setUp() { 80 Bundle args = InstrumentationRegistry.getArguments(); 81 String debugStr = args.getString(MediaBitstreams.OPT_DEBUG_TARGET_DEVICE, "false"); 82 boolean debug = Boolean.parseBoolean(debugStr); 83 if (debug && !Debug.isDebuggerConnected()) { 84 Debug.waitForDebugger(); 85 } 86 } 87 fixFormat(MediaFormat format, String path)88 private static void fixFormat(MediaFormat format, String path) { 89 // TODO(b/137684344): Revisit so that we can get this information from 90 // the bitstream or the extractor. 91 if (path.indexOf("/10bit/") < 0) { 92 return; 93 } 94 String mime = format.getString(MediaFormat.KEY_MIME); 95 int profile = -1, level = -1; 96 if (mime.equals(MediaFormat.MIMETYPE_VIDEO_VP9)) { 97 profile = CodecProfileLevel.VP9Profile2; 98 level = CodecProfileLevel.VP9Level1; 99 } else if (mime.equals(MediaFormat.MIMETYPE_VIDEO_HEVC)) { 100 profile = CodecProfileLevel.HEVCProfileMain10; 101 level = CodecProfileLevel.HEVCMainTierLevel1; 102 } else if (mime.equals(MediaFormat.MIMETYPE_VIDEO_AV1)) { 103 profile = CodecProfileLevel.AV1ProfileMain10; 104 level = CodecProfileLevel.AV1Level2; 105 } else { 106 return; 107 } 108 109 if (!format.containsKey(MediaFormat.KEY_PROFILE)) { 110 format.setInteger(MediaFormat.KEY_PROFILE, profile); 111 } 112 if (!format.containsKey(MediaFormat.KEY_LEVEL)) { 113 format.setInteger(MediaFormat.KEY_LEVEL, level); 114 } 115 } 116 117 static interface ReportCallback { run(OutputStream out)118 void run(OutputStream out) throws Exception; 119 } 120 121 static class GenerateBitstreamsFormatsXml implements ReportCallback { 122 @Override run(OutputStream out)123 public void run(OutputStream out) throws Exception { 124 125 String[] keys = new String[] { 126 MediaFormat.KEY_WIDTH, 127 MediaFormat.KEY_HEIGHT, 128 MediaFormat.KEY_FRAME_RATE, 129 MediaFormat.KEY_PROFILE, 130 MediaFormat.KEY_LEVEL, 131 MediaFormat.KEY_BIT_RATE}; 132 133 XmlSerializer formats = Xml.newSerializer(); 134 formats.setOutput(out, UTF_8); 135 formats.startDocument(UTF_8, true); 136 formats.startTag(null, MediaBitstreams.DYNAMIC_CONFIG); 137 138 DynamicConfigDeviceSide config = new DynamicConfigDeviceSide(MediaBitstreams.K_MODULE); 139 for (String path : config.keySet()) { 140 141 formats.startTag(null, MediaBitstreams.DYNAMIC_CONFIG_ENTRY); 142 formats.attribute(null, MediaBitstreams.DYNAMIC_CONFIG_KEY, path); 143 formats.startTag(null, MediaBitstreams.DYNAMIC_CONFIG_VALUE); 144 145 String formatStr = config.getValue(path); 146 if (formatStr != null && !formatStr.isEmpty()) { 147 formats.text(formatStr); 148 } else { 149 File media = new File(mDeviceBitstreamsPath, path); 150 String fullPath = media.getPath(); 151 MediaFormat format = MediaUtils.getTrackFormatForPath(null, fullPath, "video"); 152 StringBuilder formatStringBuilder = new StringBuilder(MediaFormat.KEY_MIME); 153 formatStringBuilder.append('=').append(format.getString(MediaFormat.KEY_MIME)); 154 formatStringBuilder.append(',').append(KEY_SIZE) 155 .append('=').append(media.length()); 156 for (String key : keys) { 157 formatStringBuilder.append(',').append(key).append('='); 158 if (format.containsKey(key)) { 159 formatStringBuilder.append(format.getInteger(key)); 160 } 161 } 162 formats.text(formatStringBuilder.toString()); 163 } 164 165 formats.endTag(null, MediaBitstreams.DYNAMIC_CONFIG_VALUE); 166 formats.endTag(null, MediaBitstreams.DYNAMIC_CONFIG_ENTRY); 167 168 } 169 170 formats.endTag(null, MediaBitstreams.DYNAMIC_CONFIG); 171 formats.endDocument(); 172 173 } 174 } 175 176 static class GenerateSupportedBitstreamsFormatsTxt implements ReportCallback { 177 178 @Override run(OutputStream out)179 public void run(OutputStream out) throws Exception { 180 181 PrintStream ps = new PrintStream(out); 182 Bundle args = InstrumentationRegistry.getArguments(); 183 String prefix = args.getString(MediaBitstreams.OPT_BITSTREAMS_PREFIX, ""); 184 DynamicConfigDeviceSide config = new DynamicConfigDeviceSide(MediaBitstreams.K_MODULE); 185 186 for (String path : config.keySet()) { 187 188 if (!path.startsWith(prefix)) { 189 continue; 190 } 191 192 String formatStr = config.getValue(path); 193 if (formatStr == null || formatStr.isEmpty()) { 194 continue; 195 } 196 197 MediaFormat format = parseTrackFormat(formatStr); 198 String mime = format.getString(MediaFormat.KEY_MIME); 199 String[] decoders = MediaUtils.getDecoderNamesForMime(mime); 200 fixFormat(format, path); 201 202 ps.println(path); 203 ps.println(decoders.length); 204 for (String name : decoders) { 205 ps.println(name); 206 ps.println(MediaUtils.supports(name, format)); 207 } 208 209 } 210 211 ps.flush(); 212 } 213 } 214 215 static class TestBitstreamsConformance implements ReportCallback { 216 217 ExecutorService mExecutorService; 218 getSettings()219 private SharedPreferences getSettings() { 220 Context ctx = InstrumentationRegistry.getContext(); 221 SharedPreferences settings = ctx.getSharedPreferences(MediaBitstreams.K_MODULE, 0); 222 return settings; 223 } 224 setup()225 private void setup() { 226 Bundle args = InstrumentationRegistry.getArguments(); 227 String lastCrash = args.getString(MediaBitstreams.OPT_LAST_CRASH); 228 if (lastCrash != null) { 229 SharedPreferences settings = getSettings(); 230 int n = settings.getInt(lastCrash, 0); 231 Editor editor = settings.edit(); 232 editor.putInt(lastCrash, n + 1); 233 editor.commit(); 234 } 235 } 236 237 @Override run(OutputStream out)238 public void run(OutputStream out) throws Exception { 239 setup(); 240 mExecutorService = Executors.newFixedThreadPool(3); 241 try ( 242 Scanner sc = new Scanner( 243 new File(mDeviceBitstreamsPath, MediaBitstreams.K_BITSTREAMS_LIST_TXT)); 244 PrintStream ps = new PrintStream(out, true) 245 ) { 246 while (sc.hasNextLine()) { 247 verifyBitstream(ps, sc.nextLine()); 248 } 249 } finally { 250 mExecutorService.shutdown(); 251 } 252 } 253 getDecodersForPath(String path)254 private List<String> getDecodersForPath(String path) throws IOException { 255 List<String> decoders = new ArrayList<>(); 256 MediaExtractor ex = new MediaExtractor(); 257 try { 258 ex.setDataSource(path); 259 MediaFormat format = ex.getTrackFormat(0); 260 fixFormat(format, path); 261 boolean[] vendors = new boolean[] {false, true}; 262 for (boolean v : vendors) { 263 for (String name : MediaUtils.getDecoderNames(v, format)) { 264 decoders.add(name); 265 } 266 } 267 } finally { 268 ex.release(); 269 } 270 return decoders; 271 } 272 getFrameChecksumsForPath(String path)273 private List<String> getFrameChecksumsForPath(String path) throws IOException { 274 String md5Path = MediaBitstreams.getMd5Path(path); 275 List<String> frameMD5Sums = Files.readAllLines( 276 new File(mDeviceBitstreamsPath, md5Path).toPath()); 277 for (int i = 0; i < frameMD5Sums.size(); i++) { 278 String line = frameMD5Sums.get(i); 279 frameMD5Sums.set(i, line.split(" ")[0]); 280 } 281 return frameMD5Sums; 282 } 283 verifyBitstream(PrintStream ps, String relativePath)284 private void verifyBitstream(PrintStream ps, String relativePath) { 285 ps.println(relativePath); 286 287 List<String> decoders = new ArrayList<>(); 288 List<String> frameChecksums = new ArrayList<>(); 289 SharedPreferences settings = getSettings(); 290 String fullPath = new File(mDeviceBitstreamsPath, relativePath).toString(); 291 try { 292 String lastCrash = MediaBitstreams.generateCrashSignature(relativePath, ""); 293 if (settings.getInt(lastCrash, 0) >= 3) { 294 ps.println(MediaBitstreams.K_NATIVE_CRASH); 295 return; 296 } 297 decoders = getDecodersForPath(fullPath); 298 frameChecksums = getFrameChecksumsForPath(relativePath); 299 ps.println(false); 300 } catch (Exception e) { 301 ps.println(true); 302 ps.println(e.toString()); 303 return; 304 } 305 306 ps.println(decoders.size()); 307 for (String name : decoders) { 308 ps.println(name); 309 String lastCrash = MediaBitstreams.generateCrashSignature(relativePath, name); 310 if (settings.getInt(lastCrash, 0) >= 3) { 311 ps.println(MediaBitstreams.K_NATIVE_CRASH); 312 } else { 313 ps.println(verifyBitstream(fullPath, name, frameChecksums)); 314 } 315 } 316 317 } 318 verifyBitstream(String path, String name, List<String> frameChecksums)319 private String verifyBitstream(String path, String name, List<String> frameChecksums) { 320 MediaExtractor ex = new MediaExtractor(); 321 MediaCodec d = null; 322 try { 323 Future<MediaCodec> dec = mExecutorService.submit(new Callable<MediaCodec>() { 324 @Override 325 public MediaCodec call() throws Exception { 326 return MediaCodec.createByCodecName(name); 327 } 328 }); 329 MediaCodec decoder = d = dec.get(1, TimeUnit.SECONDS); 330 Future<Boolean> conform = mExecutorService.submit(new Callable<Boolean>() { 331 @Override 332 public Boolean call() throws Exception { 333 ex.setDataSource(path); 334 ex.selectTrack(0); 335 ex.seekTo(0, MediaExtractor.SEEK_TO_NEXT_SYNC); 336 return MediaUtils.verifyDecoder(decoder, ex, frameChecksums); 337 } 338 }); 339 return conform.get(15, TimeUnit.SECONDS).toString(); 340 } catch (Exception e) { 341 return e.toString().replaceAll("\\R", " "); 342 } finally { 343 ex.release(); 344 if (d != null) { 345 d.release(); 346 } 347 } 348 } 349 350 } 351 generateReportFile(String suffix, String reportKey, ReportCallback callback)352 private void generateReportFile(String suffix, String reportKey, ReportCallback callback) 353 throws IOException, FileNotFoundException, Exception { 354 355 OutputStream out = new ByteArrayOutputStream(0); 356 357 try { 358 359 File tmpf = File.createTempFile(getClass().getSimpleName(), suffix, Environment.getExternalStorageDirectory()); 360 Instrumentation inst = InstrumentationRegistry.getInstrumentation(); 361 Bundle bundle = new Bundle(); 362 bundle.putString(MediaBitstreams.KEY_APP_CACHE_DIR, mAppCache.getCanonicalPath()); 363 bundle.putString(reportKey, tmpf.getCanonicalPath()); 364 inst.sendStatus(INST_STATUS_IN_PROGRESS, bundle); 365 366 out = new FileOutputStream(tmpf); 367 callback.run(out); 368 out.flush(); 369 370 } finally { 371 372 out.close(); 373 374 } 375 } 376 377 @Test testGetBitstreamsFormats()378 public void testGetBitstreamsFormats() throws Exception { 379 generateReportFile(".xml", 380 MediaBitstreams.KEY_BITSTREAMS_FORMATS_XML, 381 new GenerateBitstreamsFormatsXml()); 382 } 383 384 @Test testGetSupportedBitstreams()385 public void testGetSupportedBitstreams() throws Exception { 386 generateReportFile(".txt", 387 MediaBitstreams.KEY_SUPPORTED_BITSTREAMS_TXT, 388 new GenerateSupportedBitstreamsFormatsTxt()); 389 } 390 391 @Test testBitstreamsConformance()392 public void testBitstreamsConformance() throws Exception { 393 generateReportFile(".txt", 394 MediaBitstreams.KEY_BITSTREAMS_VALIDATION_TXT, 395 new TestBitstreamsConformance()); 396 } 397 398 /** 399 * Converts a single media track format string into a MediaFormat object 400 * 401 * @param trackFormatString a string representation of the format of one media track 402 * @return a MediaFormat 403 */ parseTrackFormat(String trackFormatString)404 private static MediaFormat parseTrackFormat(String trackFormatString) { 405 MediaFormat format = new MediaFormat(); 406 format.setString(MediaFormat.KEY_MIME, ""); 407 for (String entry : trackFormatString.split(",")) { 408 String[] kv = entry.split("="); 409 if (kv.length < 2 || kv[1].isEmpty()) { 410 continue; 411 } 412 String k = kv[0]; 413 String v = kv[1]; 414 try { 415 format.setInteger(k, Integer.parseInt(v)); 416 } catch (NumberFormatException e) { 417 format.setString(k, v); 418 } 419 } 420 return format; 421 } 422 } 423