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