1 /* 2 * Copyright 2014 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 package android.cts.util; 17 18 import android.content.Context; 19 import android.content.res.AssetFileDescriptor; 20 import android.media.MediaCodec; 21 import android.media.MediaCodecInfo; 22 import android.media.MediaCodecInfo.CodecCapabilities; 23 import android.media.MediaCodecInfo.VideoCapabilities; 24 import android.media.MediaCodecList; 25 import android.media.MediaExtractor; 26 import android.media.MediaFormat; 27 import android.net.Uri; 28 import android.util.Range; 29 30 import com.android.cts.util.ReportLog; 31 import com.android.cts.util.ResultType; 32 import com.android.cts.util.ResultUnit; 33 34 import java.lang.reflect.Method; 35 import static java.lang.reflect.Modifier.isPublic; 36 import static java.lang.reflect.Modifier.isStatic; 37 import java.util.Arrays; 38 import java.util.Map; 39 import android.util.Log; 40 41 import java.io.IOException; 42 43 public class MediaUtils { 44 private static final String TAG = "MediaUtils"; 45 46 private static final int ALL_AV_TRACKS = -1; 47 48 private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS); 49 50 /** 51 * Returns the test name (heuristically). 52 * 53 * Since it uses heuristics, this method has only been verified for media 54 * tests. This centralizes the way to signal errors during a test. 55 */ getTestName()56 public static String getTestName() { 57 int bestScore = -1; 58 String testName = "test???"; 59 Map<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces(); 60 for (Map.Entry<Thread, StackTraceElement[]> entry : traces.entrySet()) { 61 StackTraceElement[] stack = entry.getValue(); 62 for (int index = 0; index < stack.length; ++index) { 63 // method name must start with "test" 64 String methodName = stack[index].getMethodName(); 65 if (!methodName.startsWith("test")) { 66 continue; 67 } 68 69 int score = 0; 70 // see if there is a public non-static void method that takes no argument 71 Class<?> clazz; 72 try { 73 clazz = Class.forName(stack[index].getClassName()); 74 ++score; 75 for (final Method method : clazz.getDeclaredMethods()) { 76 if (method.getName().equals(methodName) 77 && isPublic(method.getModifiers()) 78 && !isStatic(method.getModifiers()) 79 && method.getParameterTypes().length == 0 80 && method.getReturnType().equals(Void.TYPE)) { 81 ++score; 82 break; 83 } 84 } 85 if (score == 1) { 86 // if we could read the class, but method is not public void, it is 87 // not a candidate 88 continue; 89 } 90 } catch (ClassNotFoundException e) { 91 } 92 93 // even if we cannot verify the method signature, there are signals in the stack 94 95 // usually test method is invoked by reflection 96 int depth = 1; 97 while (index + depth < stack.length 98 && stack[index + depth].getMethodName().equals("invoke") 99 && stack[index + depth].getClassName().equals( 100 "java.lang.reflect.Method")) { 101 ++depth; 102 } 103 if (depth > 1) { 104 ++score; 105 // and usually test method is run by runMethod method in android.test package 106 if (index + depth < stack.length) { 107 if (stack[index + depth].getClassName().startsWith("android.test.")) { 108 ++score; 109 } 110 if (stack[index + depth].getMethodName().equals("runMethod")) { 111 ++score; 112 } 113 } 114 } 115 116 if (score > bestScore) { 117 bestScore = score; 118 testName = methodName; 119 } 120 } 121 } 122 return testName; 123 } 124 125 /** 126 * Finds test name (heuristically) and prints out standard skip message. 127 * 128 * Since it uses heuristics, this method has only been verified for media 129 * tests. This centralizes the way to signal a skipped test. 130 */ skipTest(String tag, String reason)131 public static void skipTest(String tag, String reason) { 132 Log.i(tag, "SKIPPING " + getTestName() + "(): " + reason); 133 } 134 135 /** 136 * Finds test name (heuristically) and prints out standard skip message. 137 * 138 * Since it uses heuristics, this method has only been verified for media 139 * tests. This centralizes the way to signal a skipped test. 140 */ skipTest(String reason)141 public static void skipTest(String reason) { 142 skipTest(TAG, reason); 143 } 144 check(boolean result, String message)145 public static boolean check(boolean result, String message) { 146 if (!result) { 147 skipTest(message); 148 } 149 return result; 150 } 151 getDecoder(MediaFormat format)152 public static MediaCodec getDecoder(MediaFormat format) { 153 String decoder = sMCL.findDecoderForFormat(format); 154 if (decoder != null) { 155 try { 156 return MediaCodec.createByCodecName(decoder); 157 } catch (IOException e) { 158 } 159 } 160 return null; 161 } 162 canDecode(MediaFormat format)163 public static boolean canDecode(MediaFormat format) { 164 if (sMCL.findDecoderForFormat(format) == null) { 165 Log.i(TAG, "no decoder for " + format); 166 return false; 167 } 168 return true; 169 } 170 supports(String codecName, String mime, int w, int h)171 public static boolean supports(String codecName, String mime, int w, int h) { 172 MediaCodec codec; 173 try { 174 codec = MediaCodec.createByCodecName(codecName); 175 } catch (IOException e) { 176 return false; 177 } 178 179 CodecCapabilities cap = null; 180 try { 181 cap = codec.getCodecInfo().getCapabilitiesForType(mime); 182 } catch (IllegalArgumentException e) { 183 Log.w(TAG, "not supported mime: " + mime); 184 codec.release(); 185 return false; 186 } 187 188 VideoCapabilities vidCap = cap.getVideoCapabilities(); 189 if (vidCap == null) { 190 Log.w(TAG, "not a video codec: " + codecName); 191 codec.release(); 192 return false; 193 } 194 try { 195 Range<Double> fps = vidCap.getSupportedFrameRatesFor(w, h); 196 } catch (IllegalArgumentException e) { 197 Log.w(TAG, "unsupported size " + w + "x" + h); 198 codec.release(); 199 return false; 200 } 201 codec.release(); 202 return true; 203 } 204 hasCodecForTrack(MediaExtractor ex, int track)205 public static boolean hasCodecForTrack(MediaExtractor ex, int track) { 206 int count = ex.getTrackCount(); 207 if (track < 0 || track >= count) { 208 throw new IndexOutOfBoundsException(track + " not in [0.." + (count - 1) + "]"); 209 } 210 return canDecode(ex.getTrackFormat(track)); 211 } 212 213 /** 214 * return true iff all audio and video tracks are supported 215 */ hasCodecsForMedia(MediaExtractor ex)216 public static boolean hasCodecsForMedia(MediaExtractor ex) { 217 for (int i = 0; i < ex.getTrackCount(); ++i) { 218 MediaFormat format = ex.getTrackFormat(i); 219 // only check for audio and video codecs 220 String mime = format.getString(MediaFormat.KEY_MIME).toLowerCase(); 221 if (!mime.startsWith("audio/") && !mime.startsWith("video/")) { 222 continue; 223 } 224 if (!canDecode(format)) { 225 return false; 226 } 227 } 228 return true; 229 } 230 231 /** 232 * return true iff any track starting with mimePrefix is supported 233 */ hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix)234 public static boolean hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix) { 235 mimePrefix = mimePrefix.toLowerCase(); 236 for (int i = 0; i < ex.getTrackCount(); ++i) { 237 MediaFormat format = ex.getTrackFormat(i); 238 String mime = format.getString(MediaFormat.KEY_MIME); 239 if (mime.toLowerCase().startsWith(mimePrefix)) { 240 if (canDecode(format)) { 241 return true; 242 } 243 Log.i(TAG, "no decoder for " + format); 244 } 245 } 246 return false; 247 } 248 hasCodecsForResourceCombo( Context context, int resourceId, int track, String mimePrefix)249 private static boolean hasCodecsForResourceCombo( 250 Context context, int resourceId, int track, String mimePrefix) { 251 try { 252 AssetFileDescriptor afd = null; 253 MediaExtractor ex = null; 254 try { 255 afd = context.getResources().openRawResourceFd(resourceId); 256 ex = new MediaExtractor(); 257 ex.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); 258 if (mimePrefix != null) { 259 return hasCodecForMediaAndDomain(ex, mimePrefix); 260 } else if (track == ALL_AV_TRACKS) { 261 return hasCodecsForMedia(ex); 262 } else { 263 return hasCodecForTrack(ex, track); 264 } 265 } finally { 266 if (ex != null) { 267 ex.release(); 268 } 269 if (afd != null) { 270 afd.close(); 271 } 272 } 273 } catch (IOException e) { 274 Log.i(TAG, "could not open resource"); 275 } 276 return false; 277 } 278 279 /** 280 * return true iff all audio and video tracks are supported 281 */ hasCodecsForResource(Context context, int resourceId)282 public static boolean hasCodecsForResource(Context context, int resourceId) { 283 return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, null /* mimePrefix */); 284 } 285 checkCodecsForResource(Context context, int resourceId)286 public static boolean checkCodecsForResource(Context context, int resourceId) { 287 return check(hasCodecsForResource(context, resourceId), "no decoder found"); 288 } 289 290 /** 291 * return true iff track is supported. 292 */ hasCodecForResource(Context context, int resourceId, int track)293 public static boolean hasCodecForResource(Context context, int resourceId, int track) { 294 return hasCodecsForResourceCombo(context, resourceId, track, null /* mimePrefix */); 295 } 296 checkCodecForResource(Context context, int resourceId, int track)297 public static boolean checkCodecForResource(Context context, int resourceId, int track) { 298 return check(hasCodecForResource(context, resourceId, track), "no decoder found"); 299 } 300 301 /** 302 * return true iff any track starting with mimePrefix is supported 303 */ hasCodecForResourceAndDomain( Context context, int resourceId, String mimePrefix)304 public static boolean hasCodecForResourceAndDomain( 305 Context context, int resourceId, String mimePrefix) { 306 return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, mimePrefix); 307 } 308 309 /** 310 * return true iff all audio and video tracks are supported 311 */ hasCodecsForPath(Context context, String path)312 public static boolean hasCodecsForPath(Context context, String path) { 313 MediaExtractor ex = null; 314 try { 315 ex = new MediaExtractor(); 316 Uri uri = Uri.parse(path); 317 String scheme = uri.getScheme(); 318 if (scheme == null) { // file 319 ex.setDataSource(path); 320 } else if (scheme.equalsIgnoreCase("file")) { 321 ex.setDataSource(uri.getPath()); 322 } else { 323 ex.setDataSource(context, uri, null); 324 } 325 return hasCodecsForMedia(ex); 326 } catch (IOException e) { 327 Log.i(TAG, "could not open path " + path); 328 } finally { 329 if (ex != null) { 330 ex.release(); 331 } 332 } 333 return false; 334 } 335 checkCodecsForPath(Context context, String path)336 public static boolean checkCodecsForPath(Context context, String path) { 337 return check(hasCodecsForPath(context, path), "no decoder found"); 338 } 339 hasCodecForMime(boolean encoder, String mime)340 private static boolean hasCodecForMime(boolean encoder, String mime) { 341 for (MediaCodecInfo info : sMCL.getCodecInfos()) { 342 if (encoder != info.isEncoder()) { 343 continue; 344 } 345 346 for (String type : info.getSupportedTypes()) { 347 if (type.equalsIgnoreCase(mime)) { 348 Log.i(TAG, "found codec " + info.getName() + " for mime " + mime); 349 return true; 350 } 351 } 352 } 353 return false; 354 } 355 hasCodecForMimes(boolean encoder, String[] mimes)356 private static boolean hasCodecForMimes(boolean encoder, String[] mimes) { 357 for (String mime : mimes) { 358 if (!hasCodecForMime(encoder, mime)) { 359 Log.i(TAG, "no " + (encoder ? "encoder" : "decoder") + " for mime " + mime); 360 return false; 361 } 362 } 363 return true; 364 } 365 366 hasEncoder(String... mimes)367 public static boolean hasEncoder(String... mimes) { 368 return hasCodecForMimes(true /* encoder */, mimes); 369 } 370 hasDecoder(String... mimes)371 public static boolean hasDecoder(String... mimes) { 372 return hasCodecForMimes(false /* encoder */, mimes); 373 } 374 checkDecoder(String... mimes)375 public static boolean checkDecoder(String... mimes) { 376 return check(hasCodecForMimes(false /* encoder */, mimes), "no decoder found"); 377 } 378 checkEncoder(String... mimes)379 public static boolean checkEncoder(String... mimes) { 380 return check(hasCodecForMimes(true /* encoder */, mimes), "no encoder found"); 381 } 382 canDecodeVideo(String mime, int width, int height, float rate)383 public static boolean canDecodeVideo(String mime, int width, int height, float rate) { 384 MediaFormat format = MediaFormat.createVideoFormat(mime, width, height); 385 format.setFloat(MediaFormat.KEY_FRAME_RATE, rate); 386 return canDecode(format); 387 } 388 checkDecoderForFormat(MediaFormat format)389 public static boolean checkDecoderForFormat(MediaFormat format) { 390 return check(canDecode(format), "no decoder for " + format); 391 } 392 createMediaExtractorForMimeType( Context context, int resourceId, String mimeTypePrefix)393 public static MediaExtractor createMediaExtractorForMimeType( 394 Context context, int resourceId, String mimeTypePrefix) 395 throws IOException { 396 MediaExtractor extractor = new MediaExtractor(); 397 AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId); 398 try { 399 extractor.setDataSource( 400 afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); 401 } finally { 402 afd.close(); 403 } 404 int trackIndex; 405 for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) { 406 MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex); 407 if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) { 408 extractor.selectTrack(trackIndex); 409 break; 410 } 411 } 412 if (trackIndex == extractor.getTrackCount()) { 413 extractor.release(); 414 throw new IllegalStateException("couldn't get a track for " + mimeTypePrefix); 415 } 416 417 return extractor; 418 } 419 420 /** 421 * return the average value of the passed array. 422 */ getAverage(double[] data)423 public static double getAverage(double[] data) { 424 int num = data.length; 425 if (num == 0) { 426 return 0; 427 } 428 429 double sum = data[0]; 430 for (int i = 1; i < num; i++) { 431 sum += data[i]; 432 } 433 return sum / num; 434 } 435 436 /** 437 * return the standard deviation value of the passed array 438 */ getStdev(double[] data)439 public static double getStdev(double[] data) { 440 double average = getAverage(data); 441 int num = data.length; 442 if (num == 0) { 443 return 0; 444 } 445 double variance = 0; 446 for (int i = 0; i < num; ++i) { 447 variance += (data[i] - average) * (data[i] - average); 448 } 449 variance /= num; 450 return Math.sqrt(variance); 451 } 452 calculateMovingAverage(double[] array, int n)453 public static double[] calculateMovingAverage(double[] array, int n) { 454 int num = array.length; 455 if (num < n) { 456 return null; 457 } 458 int avgsNum = num - n + 1; 459 double[] avgs = new double[avgsNum]; 460 double sum = array[0]; 461 for (int i = 1; i < n; ++i) { 462 sum += array[i]; 463 } 464 avgs[0] = sum / n; 465 466 for (int i = n; i < num; ++i) { 467 sum = sum - array[i - n] + array[i]; 468 avgs[i - n + 1] = sum / n; 469 } 470 return avgs; 471 } 472 logResults(ReportLog log, String prefix, double min, double max, double avg, double stdev)473 public static String logResults(ReportLog log, String prefix, 474 double min, double max, double avg, double stdev) { 475 String msg = prefix; 476 msg += " min=" + Math.round(min / 1000) + " max=" + Math.round(max / 1000) + 477 " avg=" + Math.round(avg / 1000) + " stdev=" + Math.round(stdev / 1000); 478 log.printValue(msg, 1000000000 / min, ResultType.HIGHER_BETTER, ResultUnit.FPS); 479 return msg; 480 } 481 getVideoCapabilities(String codecName, String mime)482 public static VideoCapabilities getVideoCapabilities(String codecName, String mime) { 483 for (MediaCodecInfo info : sMCL.getCodecInfos()) { 484 if (!info.getName().equalsIgnoreCase(codecName)) { 485 continue; 486 } 487 CodecCapabilities caps; 488 try { 489 caps = info.getCapabilitiesForType(mime); 490 } catch (IllegalArgumentException e) { 491 // mime is not supported 492 continue; 493 } 494 return caps.getVideoCapabilities(); 495 } 496 return null; 497 } 498 getAchievableFrameRatesFor( String codecName, String mimeType, int width, int height)499 public static Range<Double> getAchievableFrameRatesFor( 500 String codecName, String mimeType, int width, int height) { 501 VideoCapabilities cap = getVideoCapabilities(codecName, mimeType); 502 if (cap == null) { 503 return null; 504 } 505 return cap.getAchievableFrameRatesFor(width, height); 506 } 507 508 private static final double FRAMERATE_TOLERANCE = Math.sqrt(12.1); verifyResults(String name, String mime, int w, int h, double measured)509 public static boolean verifyResults(String name, String mime, int w, int h, double measured) { 510 Range<Double> reported = getAchievableFrameRatesFor(name, mime, w, h); 511 if (reported == null) { 512 Log.d(TAG, "Failed to getAchievableFrameRatesFor " + 513 name + " " + mime + " " + w + "x" + h); 514 return false; 515 } 516 double lowerBoundary1 = reported.getLower() / FRAMERATE_TOLERANCE; 517 double upperBoundary1 = reported.getUpper() * FRAMERATE_TOLERANCE; 518 double lowerBoundary2 = reported.getUpper() / Math.pow(FRAMERATE_TOLERANCE, 2); 519 double upperBoundary2 = reported.getLower() * Math.pow(FRAMERATE_TOLERANCE, 2); 520 Log.d(TAG, name + " " + mime + " " + w + "x" + h + " " + 521 "lowerBoundary1 " + lowerBoundary1 + " upperBoundary1 " + upperBoundary1 + 522 " lowerBoundary2 " + lowerBoundary2 + " upperBoundary2 " + upperBoundary2 + 523 " measured " + measured); 524 return (measured >= lowerBoundary1 && measured <= upperBoundary1 && 525 measured >= lowerBoundary2 && measured <= upperBoundary2); 526 } 527 getErrorMessage( Range<Double> reportedRange, double[] measuredFps, String[] rawData)528 public static String getErrorMessage( 529 Range<Double> reportedRange, double[] measuredFps, String[] rawData) { 530 String msg = ""; 531 if (reportedRange == null) { 532 msg += "Failed to get achievable frame rate.\n"; 533 } else { 534 msg += "Expected achievable frame rate range: " + reportedRange + ".\n"; 535 } 536 msg += "Measured frame rate: " + Arrays.toString(measuredFps) + ".\n"; 537 msg += "Raw data: " + Arrays.toString(rawData) + ".\n"; 538 return msg; 539 } 540 } 541