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 com.android.compatibility.common.util; 17 18 import android.content.Context; 19 import android.content.Intent; 20 import android.content.IntentFilter; 21 import android.content.pm.PackageManager; 22 import android.content.res.AssetFileDescriptor; 23 import android.drm.DrmConvertedStatus; 24 import android.drm.DrmManagerClient; 25 import android.graphics.ImageFormat; 26 import android.graphics.Rect; 27 import android.media.Image; 28 import android.media.Image.Plane; 29 import android.media.MediaCodec; 30 import android.media.MediaCodec.BufferInfo; 31 import android.media.MediaCodecInfo; 32 import android.media.MediaCodecInfo.CodecCapabilities; 33 import android.media.MediaCodecInfo.VideoCapabilities; 34 import android.media.MediaCodecList; 35 import android.media.MediaExtractor; 36 import android.media.MediaFormat; 37 import android.net.Uri; 38 import android.os.BatteryManager; 39 import android.os.Build; 40 import android.os.SystemProperties; 41 import android.os.ParcelFileDescriptor; 42 import android.util.DisplayMetrics; 43 import android.util.Log; 44 import android.util.Range; 45 import android.view.WindowManager; 46 47 import androidx.test.platform.app.InstrumentationRegistry; 48 49 import com.android.compatibility.common.util.DeviceReportLog; 50 import com.android.compatibility.common.util.ResultType; 51 import com.android.compatibility.common.util.ResultUnit; 52 53 import java.io.File; 54 import java.lang.reflect.Method; 55 import java.nio.ByteBuffer; 56 import java.security.MessageDigest; 57 58 import static java.lang.reflect.Modifier.isPublic; 59 import static java.lang.reflect.Modifier.isStatic; 60 import java.util.ArrayList; 61 import java.util.Arrays; 62 import java.util.List; 63 import java.util.Map; 64 65 import static junit.framework.Assert.assertTrue; 66 67 import java.io.IOException; 68 import java.io.InputStream; 69 import java.io.RandomAccessFile; 70 71 public class MediaUtils { 72 private static final String TAG = "MediaUtils"; 73 private static final Context mContext = 74 InstrumentationRegistry.getInstrumentation().getTargetContext(); 75 private static final PackageManager pm = mContext.getPackageManager(); 76 private static final boolean FIRST_SDK_IS_AT_LEAST_R = 77 ApiLevelUtil.isFirstApiAtLeast(Build.VERSION_CODES.R); 78 79 /* 80 * ----------------------- HELPER METHODS FOR SKIPPING TESTS ----------------------- 81 */ 82 private static final int ALL_AV_TRACKS = -1; 83 84 private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS); 85 86 /** 87 * Returns the test name (heuristically). 88 * 89 * Since it uses heuristics, this method has only been verified for media 90 * tests. This centralizes the way to signal errors during a test. 91 */ getTestName()92 public static String getTestName() { 93 return getTestName(false /* withClass */); 94 } 95 96 /** 97 * Returns the test name with the full class (heuristically). 98 * 99 * Since it uses heuristics, this method has only been verified for media 100 * tests. This centralizes the way to signal errors during a test. 101 */ getTestNameWithClass()102 public static String getTestNameWithClass() { 103 return getTestName(true /* withClass */); 104 } 105 getTestName(boolean withClass)106 private static String getTestName(boolean withClass) { 107 int bestScore = -1; 108 String testName = "test???"; 109 Map<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces(); 110 for (Map.Entry<Thread, StackTraceElement[]> entry : traces.entrySet()) { 111 StackTraceElement[] stack = entry.getValue(); 112 for (int index = 0; index < stack.length; ++index) { 113 // method name must start with "test" 114 String methodName = stack[index].getMethodName(); 115 if (!methodName.startsWith("test")) { 116 continue; 117 } 118 119 int score = 0; 120 // see if there is a public non-static void method that takes no argument 121 Class<?> clazz; 122 try { 123 clazz = Class.forName(stack[index].getClassName()); 124 ++score; 125 for (final Method method : clazz.getDeclaredMethods()) { 126 if (method.getName().equals(methodName) 127 && isPublic(method.getModifiers()) 128 && !isStatic(method.getModifiers()) 129 && method.getParameterTypes().length == 0 130 && method.getReturnType().equals(Void.TYPE)) { 131 ++score; 132 break; 133 } 134 } 135 if (score == 1) { 136 // if we could read the class, but method is not public void, it is 137 // not a candidate 138 continue; 139 } 140 } catch (ClassNotFoundException e) { 141 } 142 143 // even if we cannot verify the method signature, there are signals in the stack 144 145 // usually test method is invoked by reflection 146 int depth = 1; 147 while (index + depth < stack.length 148 && stack[index + depth].getMethodName().equals("invoke") 149 && stack[index + depth].getClassName().equals( 150 "java.lang.reflect.Method")) { 151 ++depth; 152 } 153 if (depth > 1) { 154 ++score; 155 // and usually test method is run by runMethod method in android.test package 156 if (index + depth < stack.length) { 157 if (stack[index + depth].getClassName().startsWith("android.test.")) { 158 ++score; 159 } 160 if (stack[index + depth].getMethodName().equals("runMethod")) { 161 ++score; 162 } 163 } 164 } 165 166 if (score > bestScore) { 167 bestScore = score; 168 testName = methodName; 169 if (withClass) { 170 testName = stack[index].getClassName() + "." + testName; 171 } 172 } 173 } 174 } 175 return testName; 176 } 177 178 /** 179 * Finds test name (heuristically) and prints out standard skip message. 180 * 181 * Since it uses heuristics, this method has only been verified for media 182 * tests. This centralizes the way to signal a skipped test. 183 */ skipTest(String tag, String reason)184 public static void skipTest(String tag, String reason) { 185 Log.i(tag, "SKIPPING " + getTestName() + "(): " + reason); 186 DeviceReportLog log = new DeviceReportLog("CtsMediaSkippedTests", "test_skipped"); 187 try { 188 log.addValue("reason", reason, ResultType.NEUTRAL, ResultUnit.NONE); 189 log.addValue( 190 "test", getTestNameWithClass(), ResultType.NEUTRAL, ResultUnit.NONE); 191 log.submit(); 192 } catch (NullPointerException e) { } 193 } 194 195 /** 196 * Finds test name (heuristically) and prints out standard skip message. 197 * 198 * Since it uses heuristics, this method has only been verified for media 199 * tests. This centralizes the way to signal a skipped test. 200 */ skipTest(String reason)201 public static void skipTest(String reason) { 202 skipTest(TAG, reason); 203 } 204 check(boolean result, String message)205 public static boolean check(boolean result, String message) { 206 if (!result) { 207 skipTest(message); 208 } 209 return result; 210 } 211 212 /* 213 * ------------------- HELPER METHODS FOR CHECKING CODEC SUPPORT ------------------- 214 */ 215 isGoogle(String codecName)216 public static boolean isGoogle(String codecName) { 217 codecName = codecName.toLowerCase(); 218 return codecName.startsWith("omx.google.") 219 || codecName.startsWith("c2.android.") 220 || codecName.startsWith("c2.google."); 221 } 222 223 // returns the list of codecs that support any one of the formats getCodecNames( boolean isEncoder, Boolean isGoog, MediaFormat... formats)224 private static String[] getCodecNames( 225 boolean isEncoder, Boolean isGoog, MediaFormat... formats) { 226 ArrayList<String> result = new ArrayList<>(); 227 for (MediaCodecInfo info : sMCL.getCodecInfos()) { 228 if (info.isAlias()) { 229 // don't consider aliases here 230 continue; 231 } 232 if (info.isEncoder() != isEncoder) { 233 continue; 234 } 235 if (isGoog != null && isGoogle(info.getName()) != isGoog) { 236 continue; 237 } 238 239 for (MediaFormat format : formats) { 240 String mime = format.getString(MediaFormat.KEY_MIME); 241 242 CodecCapabilities caps = null; 243 try { 244 caps = info.getCapabilitiesForType(mime); 245 } catch (IllegalArgumentException e) { // mime is not supported 246 continue; 247 } 248 if (caps.isFormatSupported(format)) { 249 result.add(info.getName()); 250 break; 251 } 252 } 253 } 254 return result.toArray(new String[result.size()]); 255 } 256 257 /* Use isGoog = null to query all decoders */ getDecoderNames( Boolean isGoog, MediaFormat... formats)258 public static String[] getDecoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) { 259 return getCodecNames(false /* isEncoder */, isGoog, formats); 260 } 261 getDecoderNames(MediaFormat... formats)262 public static String[] getDecoderNames(MediaFormat... formats) { 263 return getCodecNames(false /* isEncoder */, null /* isGoog */, formats); 264 } 265 266 /* Use isGoog = null to query all decoders */ getEncoderNames( Boolean isGoog, MediaFormat... formats)267 public static String[] getEncoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) { 268 return getCodecNames(true /* isEncoder */, isGoog, formats); 269 } 270 getEncoderNames(MediaFormat... formats)271 public static String[] getEncoderNames(MediaFormat... formats) { 272 return getCodecNames(true /* isEncoder */, null /* isGoog */, formats); 273 } 274 getDecoderNamesForMime(String mime)275 public static String[] getDecoderNamesForMime(String mime) { 276 MediaFormat format = new MediaFormat(); 277 format.setString(MediaFormat.KEY_MIME, mime); 278 return getCodecNames(false /* isEncoder */, null /* isGoog */, format); 279 } 280 getEncoderNamesForMime(String mime)281 public static String[] getEncoderNamesForMime(String mime) { 282 MediaFormat format = new MediaFormat(); 283 format.setString(MediaFormat.KEY_MIME, mime); 284 return getCodecNames(true /* isEncoder */, null /* isGoog */, format); 285 } 286 verifyNumCodecs( int count, boolean isEncoder, Boolean isGoog, MediaFormat... formats)287 public static void verifyNumCodecs( 288 int count, boolean isEncoder, Boolean isGoog, MediaFormat... formats) { 289 String desc = (isEncoder ? "encoders" : "decoders") + " for " 290 + (formats.length == 1 ? formats[0].toString() : Arrays.toString(formats)); 291 if (isGoog != null) { 292 desc = (isGoog ? "Google " : "non-Google ") + desc; 293 } 294 295 String[] codecs = getCodecNames(isEncoder, isGoog, formats); 296 assertTrue("test can only verify " + count + " " + desc + "; found " + codecs.length + ": " 297 + Arrays.toString(codecs), codecs.length <= count); 298 } 299 getDecoder(MediaFormat format)300 public static MediaCodec getDecoder(MediaFormat format) { 301 String decoder = sMCL.findDecoderForFormat(format); 302 if (decoder != null) { 303 try { 304 return MediaCodec.createByCodecName(decoder); 305 } catch (IOException e) { 306 } 307 } 308 return null; 309 } 310 canEncode(MediaFormat format)311 public static boolean canEncode(MediaFormat format) { 312 if (sMCL.findEncoderForFormat(format) == null) { 313 Log.i(TAG, "no encoder for " + format); 314 return false; 315 } 316 return true; 317 } 318 canDecode(MediaFormat format)319 public static boolean canDecode(MediaFormat format) { 320 return canDecode(format, 0.0); 321 } 322 323 // this is "do we claim to decode"; caller is on the hook to determine 324 // if we actually meet that claim, specifically around speed. canDecode(MediaFormat format, double rate )325 public static boolean canDecode(MediaFormat format, double rate ) { 326 String decoder = sMCL.findDecoderForFormat(format); 327 328 if (decoder == null) { 329 Log.i(TAG, "no decoder for " + format); 330 return false; 331 } 332 333 if (rate == 0.0) { 334 return true; 335 } 336 337 // before Q, we always said yes once we found a decoder for the format. 338 if (ApiLevelUtil.isBefore(Build.VERSION_CODES.Q)) { 339 return true; 340 } 341 342 // we care about speed of decoding 343 Log.d(TAG, "checking for decoding " + format + " at " + 344 rate + " fps with " + decoder); 345 346 String mime = format.getString(MediaFormat.KEY_MIME); 347 int width = format.getInteger(MediaFormat.KEY_WIDTH); 348 int height = format.getInteger(MediaFormat.KEY_HEIGHT); 349 350 MediaCodecInfo[] mciList = sMCL.getCodecInfos(); 351 352 if (mciList == null) { 353 Log.d(TAG, "did not get list of MediaCodecInfo"); 354 return false; 355 } 356 357 MediaCodecInfo mci = null; 358 for (MediaCodecInfo mci2 : mciList) { 359 if (mci2.getName().equals(decoder)) { 360 mci = mci2; 361 break; 362 } 363 } 364 if (mci == null) { 365 return false; 366 } 367 if (!mci.getName().equals(decoder)) { 368 Log.e(TAG, "did not find expected " + decoder); 369 return false; 370 } 371 372 if (ApiLevelUtil.isAtLeast(Build.VERSION_CODES.Q) 373 && PropertyUtil.isVendorApiLevelAtLeast(Build.VERSION_CODES.Q) 374 && mci.isHardwareAccelerated()) { 375 MediaCodecInfo.VideoCapabilities caps = 376 mci.getCapabilitiesForType(mime).getVideoCapabilities(); 377 List<MediaCodecInfo.VideoCapabilities.PerformancePoint> pp = 378 caps.getSupportedPerformancePoints(); 379 VideoCapabilities.PerformancePoint target = 380 new VideoCapabilities.PerformancePoint(width, height, (int) rate); 381 for (MediaCodecInfo.VideoCapabilities.PerformancePoint point : pp) { 382 if (point.covers(target)) { 383 Log.i(TAG, "target " + target.toString() + 384 " covered by point " + point.toString()); 385 return true; 386 } 387 } 388 Log.i(TAG, "NOT covered by any hardware performance point"); 389 return false; 390 } else { 391 String verified = MediaPerfUtils.areAchievableFrameRates( 392 decoder, mime, width, height, rate); 393 if (verified == null) { 394 Log.d(TAG, "claims to decode content at " + rate + " fps"); 395 return true; 396 } 397 Log.d(TAG, "achieveable framerates says: " + verified); 398 return false; 399 } 400 } 401 supports(String codecName, String mime, int w, int h)402 public static boolean supports(String codecName, String mime, int w, int h) { 403 // While this could be simply written as such, give more graceful feedback. 404 // MediaFormat format = MediaFormat.createVideoFormat(mime, w, h); 405 // return supports(codecName, format); 406 407 VideoCapabilities vidCap = getVideoCapabilities(codecName, mime); 408 if (vidCap == null) { 409 return false; 410 } else if (vidCap.isSizeSupported(w, h)) { 411 return true; 412 } 413 414 Log.w(TAG, "unsupported size " + w + "x" + h); 415 return false; 416 } 417 supports(String codecName, MediaFormat format)418 public static boolean supports(String codecName, MediaFormat format) { 419 MediaCodec codec; 420 try { 421 codec = MediaCodec.createByCodecName(codecName); 422 } catch (IOException e) { 423 Log.w(TAG, "codec not found: " + codecName); 424 return false; 425 } catch (NullPointerException e) { 426 Log.w(TAG, "codec name is null"); 427 return false; 428 } 429 430 String mime = format.getString(MediaFormat.KEY_MIME); 431 CodecCapabilities cap = null; 432 try { 433 cap = codec.getCodecInfo().getCapabilitiesForType(mime); 434 return cap.isFormatSupported(format); 435 } catch (IllegalArgumentException e) { 436 Log.w(TAG, "not supported mime: " + mime); 437 return false; 438 } finally { 439 codec.release(); 440 } 441 } 442 hasCodecForTrack(MediaExtractor ex, int track)443 public static boolean hasCodecForTrack(MediaExtractor ex, int track) { 444 int count = ex.getTrackCount(); 445 if (track < 0 || track >= count) { 446 throw new IndexOutOfBoundsException(track + " not in [0.." + (count - 1) + "]"); 447 } 448 return canDecode(ex.getTrackFormat(track)); 449 } 450 451 /** 452 * return true iff all audio and video tracks are supported 453 */ hasCodecsForMedia(MediaExtractor ex)454 public static boolean hasCodecsForMedia(MediaExtractor ex) { 455 for (int i = 0; i < ex.getTrackCount(); ++i) { 456 MediaFormat format = ex.getTrackFormat(i); 457 // only check for audio and video codecs 458 String mime = format.getString(MediaFormat.KEY_MIME).toLowerCase(); 459 if (!mime.startsWith("audio/") && !mime.startsWith("video/")) { 460 continue; 461 } 462 if (!canDecode(format)) { 463 return false; 464 } 465 } 466 return true; 467 } 468 469 /** 470 * return true iff any track starting with mimePrefix is supported 471 */ hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix)472 public static boolean hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix) { 473 mimePrefix = mimePrefix.toLowerCase(); 474 for (int i = 0; i < ex.getTrackCount(); ++i) { 475 MediaFormat format = ex.getTrackFormat(i); 476 String mime = format.getString(MediaFormat.KEY_MIME); 477 if (mime.toLowerCase().startsWith(mimePrefix)) { 478 if (canDecode(format)) { 479 return true; 480 } 481 Log.i(TAG, "no decoder for " + format); 482 } 483 } 484 return false; 485 } 486 hasCodecsForResourceCombo(final String resource, int track, String mimePrefix)487 private static boolean hasCodecsForResourceCombo(final String resource, int track, 488 String mimePrefix) { 489 try { 490 AssetFileDescriptor afd = null; 491 MediaExtractor ex = null; 492 try { 493 ex = new MediaExtractor(); 494 ex.setDataSource(resource); 495 if (mimePrefix != null) { 496 return hasCodecForMediaAndDomain(ex, mimePrefix); 497 } else if (track == ALL_AV_TRACKS) { 498 return hasCodecsForMedia(ex); 499 } else { 500 return hasCodecForTrack(ex, track); 501 } 502 } finally { 503 if (ex != null) { 504 ex.release(); 505 } 506 if (afd != null) { 507 afd.close(); 508 } 509 } 510 } catch (IOException e) { 511 Log.i(TAG, "could not open resource"); 512 } 513 return false; 514 } 515 hasCodecsForResourceCombo( Context context, int resourceId, int track, String mimePrefix)516 private static boolean hasCodecsForResourceCombo( 517 Context context, int resourceId, int track, String mimePrefix) { 518 try { 519 AssetFileDescriptor afd = null; 520 MediaExtractor ex = null; 521 try { 522 afd = context.getResources().openRawResourceFd(resourceId); 523 ex = new MediaExtractor(); 524 ex.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); 525 if (mimePrefix != null) { 526 return hasCodecForMediaAndDomain(ex, mimePrefix); 527 } else if (track == ALL_AV_TRACKS) { 528 return hasCodecsForMedia(ex); 529 } else { 530 return hasCodecForTrack(ex, track); 531 } 532 } finally { 533 if (ex != null) { 534 ex.release(); 535 } 536 if (afd != null) { 537 afd.close(); 538 } 539 } 540 } catch (IOException e) { 541 Log.i(TAG, "could not open resource"); 542 } 543 return false; 544 } 545 546 /** 547 * return true iff all audio and video tracks are supported 548 */ hasCodecsForResource(Context context, int resourceId)549 public static boolean hasCodecsForResource(Context context, int resourceId) { 550 return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, null /* mimePrefix */); 551 } 552 checkCodecsForResource(Context context, int resourceId)553 public static boolean checkCodecsForResource(Context context, int resourceId) { 554 return check(hasCodecsForResource(context, resourceId), "no decoder found"); 555 } 556 hasCodecsForResource(final String resource)557 public static boolean hasCodecsForResource(final String resource) { 558 return hasCodecsForResourceCombo(resource, ALL_AV_TRACKS, null /* mimePrefix */); 559 } 560 checkCodecsForResource(final String resource)561 public static boolean checkCodecsForResource(final String resource) { 562 return check(hasCodecsForResource(resource), "no decoder found"); 563 } 564 565 /** 566 * return true iff track is supported. 567 */ hasCodecForResource(Context context, int resourceId, int track)568 public static boolean hasCodecForResource(Context context, int resourceId, int track) { 569 return hasCodecsForResourceCombo(context, resourceId, track, null /* mimePrefix */); 570 } 571 checkCodecForResource(Context context, int resourceId, int track)572 public static boolean checkCodecForResource(Context context, int resourceId, int track) { 573 return check(hasCodecForResource(context, resourceId, track), "no decoder found"); 574 } 575 hasCodecForResource(final String resource, int track)576 public static boolean hasCodecForResource(final String resource, int track) { 577 return hasCodecsForResourceCombo(resource, track, null /* mimePrefix */); 578 } 579 checkCodecForResource(final String resource, int track)580 public static boolean checkCodecForResource(final String resource, int track) { 581 return check(hasCodecForResource(resource, track), "no decoder found"); 582 } 583 584 /** 585 * return true iff any track starting with mimePrefix is supported 586 */ hasCodecForResourceAndDomain( Context context, int resourceId, String mimePrefix)587 public static boolean hasCodecForResourceAndDomain( 588 Context context, int resourceId, String mimePrefix) { 589 return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, mimePrefix); 590 } 591 hasCodecForResourceAndDomain(String resource, String mimePrefix)592 public static boolean hasCodecForResourceAndDomain(String resource, String mimePrefix) { 593 return hasCodecsForResourceCombo(resource, ALL_AV_TRACKS, mimePrefix); 594 } 595 596 /** 597 * return true iff all audio and video tracks are supported 598 */ hasCodecsForPath(Context context, String path)599 public static boolean hasCodecsForPath(Context context, String path) { 600 MediaExtractor ex = null; 601 try { 602 ex = getExtractorForPath(context, path); 603 return hasCodecsForMedia(ex); 604 } catch (IOException e) { 605 Log.i(TAG, "could not open path " + path); 606 } finally { 607 if (ex != null) { 608 ex.release(); 609 } 610 } 611 return true; 612 } 613 getExtractorForPath(Context context, String path)614 private static MediaExtractor getExtractorForPath(Context context, String path) 615 throws IOException { 616 Uri uri = Uri.parse(path); 617 String scheme = uri.getScheme(); 618 MediaExtractor ex = new MediaExtractor(); 619 try { 620 if (scheme == null) { // file 621 ex.setDataSource(path); 622 } else if (scheme.equalsIgnoreCase("file")) { 623 ex.setDataSource(uri.getPath()); 624 } else { 625 ex.setDataSource(context, uri, null); 626 } 627 } catch (IOException e) { 628 ex.release(); 629 throw e; 630 } 631 return ex; 632 } 633 checkCodecsForPath(Context context, String path)634 public static boolean checkCodecsForPath(Context context, String path) { 635 return check(hasCodecsForPath(context, path), "no decoder found"); 636 } 637 hasCodecForDomain(boolean encoder, String domain)638 public static boolean hasCodecForDomain(boolean encoder, String domain) { 639 for (MediaCodecInfo info : sMCL.getCodecInfos()) { 640 if (encoder != info.isEncoder()) { 641 continue; 642 } 643 644 for (String type : info.getSupportedTypes()) { 645 if (type.toLowerCase().startsWith(domain.toLowerCase() + "/")) { 646 Log.i(TAG, "found codec " + info.getName() + " for mime " + type); 647 return true; 648 } 649 } 650 } 651 return false; 652 } 653 checkCodecForDomain(boolean encoder, String domain)654 public static boolean checkCodecForDomain(boolean encoder, String domain) { 655 return check(hasCodecForDomain(encoder, domain), 656 "no " + domain + (encoder ? " encoder" : " decoder") + " found"); 657 } 658 hasCodecForMime(boolean encoder, String mime)659 private static boolean hasCodecForMime(boolean encoder, String mime) { 660 for (MediaCodecInfo info : sMCL.getCodecInfos()) { 661 if (encoder != info.isEncoder()) { 662 continue; 663 } 664 665 for (String type : info.getSupportedTypes()) { 666 if (type.equalsIgnoreCase(mime)) { 667 Log.i(TAG, "found codec " + info.getName() + " for mime " + mime); 668 return true; 669 } 670 } 671 } 672 return false; 673 } 674 hasCodecForMimes(boolean encoder, String[] mimes)675 private static boolean hasCodecForMimes(boolean encoder, String[] mimes) { 676 for (String mime : mimes) { 677 if (!hasCodecForMime(encoder, mime)) { 678 Log.i(TAG, "no " + (encoder ? "encoder" : "decoder") + " for mime " + mime); 679 return false; 680 } 681 } 682 return true; 683 } 684 685 hasEncoder(String... mimes)686 public static boolean hasEncoder(String... mimes) { 687 return hasCodecForMimes(true /* encoder */, mimes); 688 } 689 hasDecoder(String... mimes)690 public static boolean hasDecoder(String... mimes) { 691 return hasCodecForMimes(false /* encoder */, mimes); 692 } 693 checkDecoder(String... mimes)694 public static boolean checkDecoder(String... mimes) { 695 return check(hasCodecForMimes(false /* encoder */, mimes), "no decoder found"); 696 } 697 checkEncoder(String... mimes)698 public static boolean checkEncoder(String... mimes) { 699 return check(hasCodecForMimes(true /* encoder */, mimes), "no encoder found"); 700 } 701 702 // checks format, does not address actual speed of decoding canDecodeVideo(String mime, int width, int height, float rate)703 public static boolean canDecodeVideo(String mime, int width, int height, float rate) { 704 return canDecodeVideo(mime, width, height, rate, (float)0.0); 705 } 706 707 // format + decode rate canDecodeVideo(String mime, int width, int height, float rate, float decodeRate)708 public static boolean canDecodeVideo(String mime, int width, int height, float rate, float decodeRate) { 709 MediaFormat format = MediaFormat.createVideoFormat(mime, width, height); 710 format.setFloat(MediaFormat.KEY_FRAME_RATE, rate); 711 return canDecode(format, decodeRate); 712 } 713 canDecodeVideo( String mime, int width, int height, float rate, Integer profile, Integer level, Integer bitrate)714 public static boolean canDecodeVideo( 715 String mime, int width, int height, float rate, 716 Integer profile, Integer level, Integer bitrate) { 717 return canDecodeVideo(mime, width, height, rate, profile, level, bitrate, (float)0.0); 718 } 719 canDecodeVideo( String mime, int width, int height, float rate, Integer profile, Integer level, Integer bitrate, float decodeRate)720 public static boolean canDecodeVideo( 721 String mime, int width, int height, float rate, 722 Integer profile, Integer level, Integer bitrate, float decodeRate) { 723 MediaFormat format = MediaFormat.createVideoFormat(mime, width, height); 724 format.setFloat(MediaFormat.KEY_FRAME_RATE, rate); 725 if (profile != null) { 726 format.setInteger(MediaFormat.KEY_PROFILE, profile); 727 if (level != null) { 728 format.setInteger(MediaFormat.KEY_LEVEL, level); 729 } 730 } 731 if (bitrate != null) { 732 format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); 733 } 734 return canDecode(format, decodeRate); 735 } 736 checkEncoderForFormat(MediaFormat format)737 public static boolean checkEncoderForFormat(MediaFormat format) { 738 return check(canEncode(format), "no encoder for " + format); 739 } 740 checkDecoderForFormat(MediaFormat format)741 public static boolean checkDecoderForFormat(MediaFormat format) { 742 return check(canDecode(format), "no decoder for " + format); 743 } 744 745 /* 746 * ----------------------- HELPER METHODS FOR MEDIA HANDLING ----------------------- 747 */ 748 getVideoCapabilities(String codecName, String mime)749 public static VideoCapabilities getVideoCapabilities(String codecName, String mime) { 750 for (MediaCodecInfo info : sMCL.getCodecInfos()) { 751 if (!info.getName().equalsIgnoreCase(codecName)) { 752 continue; 753 } 754 CodecCapabilities caps; 755 try { 756 caps = info.getCapabilitiesForType(mime); 757 } catch (IllegalArgumentException e) { 758 // mime is not supported 759 Log.w(TAG, "not supported mime: " + mime); 760 return null; 761 } 762 VideoCapabilities vidCaps = caps.getVideoCapabilities(); 763 if (vidCaps == null) { 764 Log.w(TAG, "not a video codec: " + codecName); 765 } 766 return vidCaps; 767 } 768 Log.w(TAG, "codec not found: " + codecName); 769 return null; 770 } 771 getTrackFormatForResource( Context context, int resourceId, String mimeTypePrefix)772 public static MediaFormat getTrackFormatForResource( 773 Context context, 774 int resourceId, 775 String mimeTypePrefix) throws IOException { 776 MediaExtractor extractor = new MediaExtractor(); 777 AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId); 778 try { 779 extractor.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); 780 } finally { 781 afd.close(); 782 } 783 return getTrackFormatForExtractor(extractor, mimeTypePrefix); 784 } 785 getTrackFormatForResource( final String resource, String mimeTypePrefix)786 public static MediaFormat getTrackFormatForResource( 787 final String resource, 788 String mimeTypePrefix) throws IOException { 789 MediaExtractor extractor = new MediaExtractor(); 790 try { 791 extractor.setDataSource(resource); 792 } catch (IOException e) { 793 e.printStackTrace(); 794 } 795 return getTrackFormatForExtractor(extractor, mimeTypePrefix); 796 } 797 getTrackFormatForPath( Context context, String path, String mimeTypePrefix)798 public static MediaFormat getTrackFormatForPath( 799 Context context, String path, String mimeTypePrefix) 800 throws IOException { 801 MediaExtractor extractor = getExtractorForPath(context, path); 802 return getTrackFormatForExtractor(extractor, mimeTypePrefix); 803 } 804 getTrackFormatForExtractor( MediaExtractor extractor, String mimeTypePrefix)805 private static MediaFormat getTrackFormatForExtractor( 806 MediaExtractor extractor, 807 String mimeTypePrefix) { 808 int trackIndex; 809 MediaFormat format = null; 810 for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) { 811 MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex); 812 if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) { 813 format = trackMediaFormat; 814 break; 815 } 816 } 817 extractor.release(); 818 if (format == null) { 819 throw new RuntimeException("couldn't get a track for " + mimeTypePrefix); 820 } 821 822 return format; 823 } 824 createMediaExtractorForMimeType( Context context, String resource, String mimeTypePrefix)825 public static MediaExtractor createMediaExtractorForMimeType( 826 Context context, String resource, String mimeTypePrefix) 827 throws IOException { 828 MediaExtractor extractor = new MediaExtractor(); 829 File inpFile = new File(resource); 830 ParcelFileDescriptor parcelFD = 831 ParcelFileDescriptor.open(inpFile, ParcelFileDescriptor.MODE_READ_ONLY); 832 AssetFileDescriptor afd = new AssetFileDescriptor(parcelFD, 0, parcelFD.getStatSize()); 833 try { 834 extractor.setDataSource( 835 afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); 836 } finally { 837 afd.close(); 838 } 839 int trackIndex; 840 for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) { 841 MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex); 842 if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) { 843 extractor.selectTrack(trackIndex); 844 break; 845 } 846 } 847 if (trackIndex == extractor.getTrackCount()) { 848 extractor.release(); 849 throw new IllegalStateException("couldn't get a track for " + mimeTypePrefix); 850 } 851 852 return extractor; 853 } 854 855 /* 856 * ---------------------- HELPER METHODS FOR CODEC CONFIGURATION 857 */ 858 859 /** Format must contain mime, width and height. 860 * Throws Exception if encoder does not support this width and height */ setMaxEncoderFrameAndBitrates( MediaCodec encoder, MediaFormat format, int maxFps)861 public static void setMaxEncoderFrameAndBitrates( 862 MediaCodec encoder, MediaFormat format, int maxFps) { 863 String mime = format.getString(MediaFormat.KEY_MIME); 864 865 VideoCapabilities vidCaps = 866 encoder.getCodecInfo().getCapabilitiesForType(mime).getVideoCapabilities(); 867 setMaxEncoderFrameAndBitrates(vidCaps, format, maxFps); 868 } 869 setMaxEncoderFrameAndBitrates( VideoCapabilities vidCaps, MediaFormat format, int maxFps)870 public static void setMaxEncoderFrameAndBitrates( 871 VideoCapabilities vidCaps, MediaFormat format, int maxFps) { 872 int width = format.getInteger(MediaFormat.KEY_WIDTH); 873 int height = format.getInteger(MediaFormat.KEY_HEIGHT); 874 875 int maxWidth = vidCaps.getSupportedWidths().getUpper(); 876 int maxHeight = vidCaps.getSupportedHeightsFor(maxWidth).getUpper(); 877 int frameRate = Math.min( 878 maxFps, vidCaps.getSupportedFrameRatesFor(width, height).getUpper().intValue()); 879 format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); 880 881 int bitrate = vidCaps.getBitrateRange().clamp( 882 (int)(vidCaps.getBitrateRange().getUpper() / 883 Math.sqrt((double)maxWidth * maxHeight / width / height))); 884 format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); 885 } 886 hasHardwareCodec(String mime, boolean encode)887 public static boolean hasHardwareCodec(String mime, boolean encode) { 888 for (MediaCodecInfo info : sMCL.getCodecInfos()) { 889 if (info.isEncoder() == encode && info.isHardwareAccelerated()) { 890 try { 891 if (info.getCapabilitiesForType(mime) != null) { 892 return true; 893 } 894 } catch (IllegalArgumentException e) { 895 // mime is not supported 896 Log.w(TAG, "not supported mime: " + mime); 897 return false; 898 } 899 } 900 } 901 return false; 902 } 903 904 /* 905 * ------------------ HELPER METHODS FOR STATISTICS AND REPORTING ------------------ 906 */ 907 908 // TODO: migrate this into com.android.compatibility.common.util.Stat 909 public static class Stats { 910 /** does not support NaN or Inf in |data| */ Stats(double[] data)911 public Stats(double[] data) { 912 mData = data; 913 if (mData != null) { 914 mNum = mData.length; 915 } 916 } 917 getNum()918 public int getNum() { 919 return mNum; 920 } 921 922 /** calculate mSumX and mSumXX */ analyze()923 private void analyze() { 924 if (mAnalyzed) { 925 return; 926 } 927 928 if (mData != null) { 929 for (double x : mData) { 930 if (!(x >= mMinX)) { // mMinX may be NaN 931 mMinX = x; 932 } 933 if (!(x <= mMaxX)) { // mMaxX may be NaN 934 mMaxX = x; 935 } 936 mSumX += x; 937 mSumXX += x * x; 938 } 939 } 940 mAnalyzed = true; 941 } 942 943 /** returns the maximum or NaN if it does not exist */ getMin()944 public double getMin() { 945 analyze(); 946 return mMinX; 947 } 948 949 /** returns the minimum or NaN if it does not exist */ getMax()950 public double getMax() { 951 analyze(); 952 return mMaxX; 953 } 954 955 /** returns the average or NaN if it does not exist. */ getAverage()956 public double getAverage() { 957 analyze(); 958 if (mNum == 0) { 959 return Double.NaN; 960 } else { 961 return mSumX / mNum; 962 } 963 } 964 965 /** returns the standard deviation or NaN if it does not exist. */ getStdev()966 public double getStdev() { 967 analyze(); 968 if (mNum == 0) { 969 return Double.NaN; 970 } else { 971 double average = mSumX / mNum; 972 return Math.sqrt(mSumXX / mNum - average * average); 973 } 974 } 975 976 /** returns the statistics for the moving average over n values */ movingAverage(int n)977 public Stats movingAverage(int n) { 978 if (n < 1 || mNum < n) { 979 return new Stats(null); 980 } else if (n == 1) { 981 return this; 982 } 983 984 double[] avgs = new double[mNum - n + 1]; 985 double sum = 0; 986 for (int i = 0; i < mNum; ++i) { 987 sum += mData[i]; 988 if (i >= n - 1) { 989 avgs[i - n + 1] = sum / n; 990 sum -= mData[i - n + 1]; 991 } 992 } 993 return new Stats(avgs); 994 } 995 996 /** returns the statistics for the moving average over a window over the 997 * cumulative sum. Basically, moves a window from: [0, window] to 998 * [sum - window, sum] over the cumulative sum, over ((sum - window) / average) 999 * steps, and returns the average value over each window. 1000 * This method is used to average time-diff data over a window of a constant time. 1001 */ movingAverageOverSum(double window)1002 public Stats movingAverageOverSum(double window) { 1003 if (window <= 0 || mNum < 1) { 1004 return new Stats(null); 1005 } 1006 1007 analyze(); 1008 double average = mSumX / mNum; 1009 if (window >= mSumX) { 1010 return new Stats(new double[] { average }); 1011 } 1012 int samples = (int)Math.ceil((mSumX - window) / average); 1013 double[] avgs = new double[samples]; 1014 1015 // A somewhat brute force approach to calculating the moving average. 1016 // TODO: add support for weights in Stats, so we can do a more refined approach. 1017 double sum = 0; // sum of elements in the window 1018 int num = 0; // number of elements in the moving window 1019 int bi = 0; // index of the first element in the moving window 1020 int ei = 0; // index of the last element in the moving window 1021 double space = window; // space at the end of the window 1022 double foot = 0; // space at the beginning of the window 1023 1024 // invariants: foot + sum + space == window 1025 // bi + num == ei 1026 // 1027 // window: |-------------------------------| 1028 // | <-----sum------> | 1029 // <foot> <---space--> 1030 // | | 1031 // intervals: |-----------|-------|-------|--------------------|--------| 1032 // ^bi ^ei 1033 1034 int ix = 0; // index in the result 1035 while (ix < samples) { 1036 // add intervals while there is space in the window 1037 while (ei < mData.length && mData[ei] <= space) { 1038 space -= mData[ei]; 1039 sum += mData[ei]; 1040 num++; 1041 ei++; 1042 } 1043 1044 // calculate average over window and deal with odds and ends (e.g. if there are no 1045 // intervals in the current window: pick whichever element overlaps the window 1046 // most. 1047 if (num > 0) { 1048 avgs[ix++] = sum / num; 1049 } else if (bi > 0 && foot > space) { 1050 // consider previous 1051 avgs[ix++] = mData[bi - 1]; 1052 } else if (ei == mData.length) { 1053 break; 1054 } else { 1055 avgs[ix++] = mData[ei]; 1056 } 1057 1058 // move the window to the next position 1059 foot -= average; 1060 space += average; 1061 1062 // remove intervals that are now partially or wholly outside of the window 1063 while (bi < ei && foot < 0) { 1064 foot += mData[bi]; 1065 sum -= mData[bi]; 1066 num--; 1067 bi++; 1068 } 1069 } 1070 return new Stats(Arrays.copyOf(avgs, ix)); 1071 } 1072 1073 /** calculate mSortedData */ sort()1074 private void sort() { 1075 if (mSorted || mNum == 0) { 1076 return; 1077 } 1078 mSortedData = Arrays.copyOf(mData, mNum); 1079 Arrays.sort(mSortedData); 1080 mSorted = true; 1081 } 1082 1083 /** returns an array of percentiles for the points using nearest rank */ getPercentiles(double... points)1084 public double[] getPercentiles(double... points) { 1085 sort(); 1086 double[] res = new double[points.length]; 1087 for (int i = 0; i < points.length; ++i) { 1088 if (mNum < 1 || points[i] < 0 || points[i] > 100) { 1089 res[i] = Double.NaN; 1090 } else { 1091 res[i] = mSortedData[(int)Math.round(points[i] / 100 * (mNum - 1))]; 1092 } 1093 } 1094 return res; 1095 } 1096 1097 @Override equals(Object o)1098 public boolean equals(Object o) { 1099 if (o instanceof Stats) { 1100 Stats other = (Stats)o; 1101 if (other.mNum != mNum) { 1102 return false; 1103 } else if (mNum == 0) { 1104 return true; 1105 } 1106 return Arrays.equals(mData, other.mData); 1107 } 1108 return false; 1109 } 1110 1111 private double[] mData; 1112 private double mSumX = 0; 1113 private double mSumXX = 0; 1114 private double mMinX = Double.NaN; 1115 private double mMaxX = Double.NaN; 1116 private int mNum = 0; 1117 private boolean mAnalyzed = false; 1118 private double[] mSortedData; 1119 private boolean mSorted = false; 1120 } 1121 1122 /** 1123 * Convert a forward lock .dm message stream to a .fl file 1124 * @param context Context to use 1125 * @param dmStream The .dm message 1126 * @param flFile The output file to be written 1127 * @return success 1128 */ convertDmToFl( Context context, InputStream dmStream, RandomAccessFile flFile)1129 public static boolean convertDmToFl( 1130 Context context, 1131 InputStream dmStream, 1132 RandomAccessFile flFile) { 1133 final String MIMETYPE_DRM_MESSAGE = "application/vnd.oma.drm.message"; 1134 byte[] dmData = new byte[10000]; 1135 int totalRead = 0; 1136 int numRead; 1137 while (true) { 1138 try { 1139 numRead = dmStream.read(dmData, totalRead, dmData.length - totalRead); 1140 } catch (IOException e) { 1141 Log.w(TAG, "Failed to read from input file"); 1142 return false; 1143 } 1144 if (numRead == -1) { 1145 break; 1146 } 1147 totalRead += numRead; 1148 if (totalRead == dmData.length) { 1149 // grow array 1150 dmData = Arrays.copyOf(dmData, dmData.length + 10000); 1151 } 1152 } 1153 byte[] fileData = Arrays.copyOf(dmData, totalRead); 1154 1155 DrmManagerClient drmClient = null; 1156 try { 1157 drmClient = new DrmManagerClient(context); 1158 } catch (IllegalArgumentException e) { 1159 Log.w(TAG, "DrmManagerClient instance could not be created, context is Illegal."); 1160 return false; 1161 } catch (IllegalStateException e) { 1162 Log.w(TAG, "DrmManagerClient didn't initialize properly."); 1163 return false; 1164 } 1165 1166 try { 1167 int convertSessionId = -1; 1168 try { 1169 convertSessionId = drmClient.openConvertSession(MIMETYPE_DRM_MESSAGE); 1170 } catch (IllegalArgumentException e) { 1171 Log.w(TAG, "Conversion of Mimetype: " + MIMETYPE_DRM_MESSAGE 1172 + " is not supported.", e); 1173 return false; 1174 } catch (IllegalStateException e) { 1175 Log.w(TAG, "Could not access Open DrmFramework.", e); 1176 return false; 1177 } 1178 1179 if (convertSessionId < 0) { 1180 Log.w(TAG, "Failed to open session."); 1181 return false; 1182 } 1183 1184 DrmConvertedStatus convertedStatus = null; 1185 try { 1186 convertedStatus = drmClient.convertData(convertSessionId, fileData); 1187 } catch (IllegalArgumentException e) { 1188 Log.w(TAG, "Buffer with data to convert is illegal. Convertsession: " 1189 + convertSessionId, e); 1190 return false; 1191 } catch (IllegalStateException e) { 1192 Log.w(TAG, "Could not convert data. Convertsession: " + convertSessionId, e); 1193 return false; 1194 } 1195 1196 if (convertedStatus == null || 1197 convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK || 1198 convertedStatus.convertedData == null) { 1199 Log.w(TAG, "Error in converting data. Convertsession: " + convertSessionId); 1200 try { 1201 DrmConvertedStatus result = drmClient.closeConvertSession(convertSessionId); 1202 if (result.statusCode != DrmConvertedStatus.STATUS_OK) { 1203 Log.w(TAG, "Conversion failed with status: " + result.statusCode); 1204 return false; 1205 } 1206 } catch (IllegalStateException e) { 1207 Log.w(TAG, "Could not close session. Convertsession: " + 1208 convertSessionId, e); 1209 } 1210 return false; 1211 } 1212 1213 try { 1214 flFile.write(convertedStatus.convertedData, 0, convertedStatus.convertedData.length); 1215 } catch (IOException e) { 1216 Log.w(TAG, "Failed to write to output file: " + e); 1217 return false; 1218 } 1219 1220 try { 1221 convertedStatus = drmClient.closeConvertSession(convertSessionId); 1222 } catch (IllegalStateException e) { 1223 Log.w(TAG, "Could not close convertsession. Convertsession: " + 1224 convertSessionId, e); 1225 return false; 1226 } 1227 1228 if (convertedStatus == null || 1229 convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK || 1230 convertedStatus.convertedData == null) { 1231 Log.w(TAG, "Error in closing session. Convertsession: " + convertSessionId); 1232 return false; 1233 } 1234 1235 try { 1236 flFile.seek(convertedStatus.offset); 1237 flFile.write(convertedStatus.convertedData); 1238 } catch (IOException e) { 1239 Log.w(TAG, "Could not update file.", e); 1240 return false; 1241 } 1242 1243 return true; 1244 } finally { 1245 drmClient.close(); 1246 } 1247 } 1248 1249 /** 1250 * @param decoder new MediaCodec object 1251 * @param ex MediaExtractor after setDataSource and selectTrack 1252 * @param frameMD5Sums reference MD5 checksum for decoded frames 1253 * @return true if decoded frames checksums matches reference checksums 1254 * @throws IOException 1255 */ verifyDecoder( MediaCodec decoder, MediaExtractor ex, List<String> frameMD5Sums)1256 public static boolean verifyDecoder( 1257 MediaCodec decoder, MediaExtractor ex, List<String> frameMD5Sums) 1258 throws IOException { 1259 1260 int trackIndex = ex.getSampleTrackIndex(); 1261 MediaFormat format = ex.getTrackFormat(trackIndex); 1262 decoder.configure(format, null /* surface */, null /* crypto */, 0 /* flags */); 1263 decoder.start(); 1264 1265 boolean sawInputEOS = false; 1266 boolean sawOutputEOS = false; 1267 final long kTimeOutUs = 5000; // 5ms timeout 1268 int decodedFrameCount = 0; 1269 int expectedFrameCount = frameMD5Sums.size(); 1270 MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); 1271 1272 while (!sawOutputEOS) { 1273 // handle input 1274 if (!sawInputEOS) { 1275 int inIdx = decoder.dequeueInputBuffer(kTimeOutUs); 1276 if (inIdx >= 0) { 1277 ByteBuffer buffer = decoder.getInputBuffer(inIdx); 1278 int sampleSize = ex.readSampleData(buffer, 0); 1279 if (sampleSize < 0) { 1280 final int flagEOS = MediaCodec.BUFFER_FLAG_END_OF_STREAM; 1281 decoder.queueInputBuffer(inIdx, 0, 0, 0, flagEOS); 1282 sawInputEOS = true; 1283 } else { 1284 decoder.queueInputBuffer(inIdx, 0, sampleSize, ex.getSampleTime(), 0); 1285 ex.advance(); 1286 } 1287 } 1288 } 1289 1290 // handle output 1291 int outputBufIndex = decoder.dequeueOutputBuffer(info, kTimeOutUs); 1292 if (outputBufIndex >= 0) { 1293 try { 1294 if (info.size > 0) { 1295 // Disregard 0-sized buffers at the end. 1296 String md5CheckSum = ""; 1297 Image image = decoder.getOutputImage(outputBufIndex); 1298 md5CheckSum = getImageMD5Checksum(image); 1299 1300 if (!md5CheckSum.equals(frameMD5Sums.get(decodedFrameCount))) { 1301 Log.d(TAG, 1302 String.format( 1303 "Frame %d md5sum mismatch: %s(actual) vs %s(expected)", 1304 decodedFrameCount, md5CheckSum, 1305 frameMD5Sums.get(decodedFrameCount))); 1306 return false; 1307 } 1308 1309 decodedFrameCount++; 1310 } 1311 } catch (Exception e) { 1312 Log.e(TAG, "getOutputImage md5CheckSum failed", e); 1313 return false; 1314 } finally { 1315 decoder.releaseOutputBuffer(outputBufIndex, false /* render */); 1316 } 1317 if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { 1318 sawOutputEOS = true; 1319 } 1320 } else if (outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 1321 MediaFormat decOutputFormat = decoder.getOutputFormat(); 1322 Log.d(TAG, "output format " + decOutputFormat); 1323 } else if (outputBufIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { 1324 Log.i(TAG, "Skip handling MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED"); 1325 } else if (outputBufIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { 1326 continue; 1327 } else { 1328 Log.w(TAG, "decoder.dequeueOutputBuffer() unrecognized index: " + outputBufIndex); 1329 return false; 1330 } 1331 } 1332 1333 if (decodedFrameCount != expectedFrameCount) { 1334 return false; 1335 } 1336 1337 return true; 1338 } 1339 getImageMD5Checksum(Image image)1340 public static String getImageMD5Checksum(Image image) throws Exception { 1341 int format = image.getFormat(); 1342 if (ImageFormat.YUV_420_888 != format) { 1343 Log.w(TAG, "unsupported image format"); 1344 return ""; 1345 } 1346 1347 MessageDigest md = MessageDigest.getInstance("MD5"); 1348 1349 Rect crop = image.getCropRect(); 1350 int cropLeft = crop.left; 1351 int cropRight = crop.right; 1352 int cropTop = crop.top; 1353 int cropBottom = crop.bottom; 1354 1355 int imageWidth = cropRight - cropLeft; 1356 int imageHeight = cropBottom - cropTop; 1357 1358 Image.Plane[] planes = image.getPlanes(); 1359 for (int i = 0; i < planes.length; ++i) { 1360 ByteBuffer buf = planes[i].getBuffer(); 1361 1362 int width, height, rowStride, pixelStride, x, y, top, left; 1363 rowStride = planes[i].getRowStride(); 1364 pixelStride = planes[i].getPixelStride(); 1365 if (i == 0) { 1366 width = imageWidth; 1367 height = imageHeight; 1368 left = cropLeft; 1369 top = cropTop; 1370 } else { 1371 width = imageWidth / 2; 1372 height = imageHeight /2; 1373 left = cropLeft / 2; 1374 top = cropTop / 2; 1375 } 1376 // local contiguous pixel buffer 1377 byte[] bb = new byte[width * height]; 1378 if (buf.hasArray()) { 1379 byte b[] = buf.array(); 1380 int offs = buf.arrayOffset() + left * pixelStride; 1381 if (pixelStride == 1) { 1382 for (y = 0; y < height; ++y) { 1383 System.arraycopy(bb, y * width, b, (top + y) * rowStride + offs, width); 1384 } 1385 } else { 1386 // do it pixel-by-pixel 1387 for (y = 0; y < height; ++y) { 1388 int lineOffset = offs + (top + y) * rowStride; 1389 for (x = 0; x < width; ++x) { 1390 bb[y * width + x] = b[lineOffset + x * pixelStride]; 1391 } 1392 } 1393 } 1394 } else { // almost always ends up here due to direct buffers 1395 int pos = buf.position(); 1396 if (pixelStride == 1) { 1397 for (y = 0; y < height; ++y) { 1398 buf.position(pos + left + (top + y) * rowStride); 1399 buf.get(bb, y * width, width); 1400 } 1401 } else { 1402 // local line buffer 1403 byte[] lb = new byte[rowStride]; 1404 // do it pixel-by-pixel 1405 for (y = 0; y < height; ++y) { 1406 buf.position(pos + left * pixelStride + (top + y) * rowStride); 1407 // we're only guaranteed to have pixelStride * (width - 1) + 1 bytes 1408 buf.get(lb, 0, pixelStride * (width - 1) + 1); 1409 for (x = 0; x < width; ++x) { 1410 bb[y * width + x] = lb[x * pixelStride]; 1411 } 1412 } 1413 } 1414 buf.position(pos); 1415 } 1416 md.update(bb, 0, width * height); 1417 } 1418 1419 return convertByteArrayToHEXString(md.digest()); 1420 } 1421 convertByteArrayToHEXString(byte[] ba)1422 private static String convertByteArrayToHEXString(byte[] ba) throws Exception { 1423 StringBuilder result = new StringBuilder(); 1424 for (int i = 0; i < ba.length; i++) { 1425 result.append(Integer.toString((ba[i] & 0xff) + 0x100, 16).substring(1)); 1426 } 1427 return result.toString(); 1428 } 1429 1430 1431 /* 1432 * ------------------- HELPER METHODS FOR DETECTING DEVICE TYPES ------------------- 1433 */ 1434 hasDeviceGotBattery()1435 public static boolean hasDeviceGotBattery() { 1436 final Intent batteryInfo = mContext.registerReceiver(null, 1437 new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 1438 return batteryInfo.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true); 1439 } 1440 getScreenSizeInInches()1441 public static double getScreenSizeInInches() { 1442 DisplayMetrics dm = mContext.getResources().getDisplayMetrics(); 1443 double widthInInchesSquared = Math.pow(dm.widthPixels/dm.xdpi,2); 1444 double heightInInchesSquared = Math.pow(dm.heightPixels/dm.ydpi,2); 1445 double diagonalInInches = Math.sqrt(widthInInchesSquared + heightInInchesSquared); 1446 return diagonalInInches; 1447 } 1448 isTv()1449 public static boolean isTv() { 1450 return pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) || 1451 pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION); 1452 } 1453 hasMicrophone()1454 public static boolean hasMicrophone() { 1455 return pm.hasSystemFeature(PackageManager.FEATURE_MICROPHONE); 1456 } 1457 hasCamera()1458 public static boolean hasCamera() { 1459 return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); 1460 } 1461 isWatch()1462 public static boolean isWatch() { 1463 return pm.hasSystemFeature(PackageManager.FEATURE_WATCH); 1464 } 1465 isAutomotive()1466 public static boolean isAutomotive() { 1467 return pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); 1468 } 1469 isPc()1470 public static boolean isPc() { 1471 return pm.hasSystemFeature(PackageManager.FEATURE_PC); 1472 } 1473 hasAudioOutput()1474 public static boolean hasAudioOutput() { 1475 return pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT); 1476 } 1477 isHandheld()1478 public static boolean isHandheld() { 1479 double screenSize = getScreenSizeInInches(); 1480 if (screenSize < (FIRST_SDK_IS_AT_LEAST_R ? 3.3 : 2.5)) return false; 1481 if (screenSize > 8.0) return false; 1482 if (!hasDeviceGotBattery()) return false; 1483 // handheld nature is not exposed to package manager, so for now, 1484 // in addition to physical screen size, the following checks are 1485 // also required: 1486 if (!pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) return false; 1487 if (isWatch()) return false; 1488 if (isTv()) return false; 1489 if (isAutomotive()) return false; 1490 if (isPc()) return false; 1491 return true; 1492 } 1493 isTablet()1494 public static boolean isTablet() { 1495 double screenSize = getScreenSizeInInches(); 1496 if (screenSize < 7.0) return false; 1497 if (screenSize > 18.0) return false; 1498 if (!hasDeviceGotBattery()) return false; 1499 // tablet nature is not exposed to package manager, so for now, 1500 // in addition to physical screen size, the following checks are 1501 // also required: 1502 if (!pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) return false; 1503 if (isWatch()) return false; 1504 if (isTv()) return false; 1505 if (isAutomotive()) return false; 1506 if (isPc()) return false; 1507 return true; 1508 } 1509 1510 /* 1511 * ------------------- HELPER METHODS FOR DETECTING NON-PRODUCTION DEVICES ------------------- 1512 */ 1513 1514 /* 1515 * Some parts of media CTS verifies device characterization that does not make sense for 1516 * non-production devices (such as GSI and cuttlefish). We call these devices 'frankenDevices'. 1517 * We may also limit test duration on these devices. 1518 */ onFrankenDevice()1519 public static boolean onFrankenDevice() throws IOException { 1520 String systemBrand = PropertyUtil.getProperty("ro.product.system.brand"); 1521 String systemModel = PropertyUtil.getProperty("ro.product.system.model"); 1522 String systemProduct = PropertyUtil.getProperty("ro.product.system.name"); 1523 1524 // not all devices may have system_ext partition, but if they do use that 1525 { 1526 String systemExtProduct = PropertyUtil.getProperty("ro.product.system_ext.name"); 1527 if (systemExtProduct != null) { 1528 systemProduct = systemExtProduct; 1529 } 1530 String systemExtModel = PropertyUtil.getProperty("ro.product.system_ext.model"); 1531 if (systemExtModel != null) { 1532 systemModel = systemExtModel; 1533 } 1534 } 1535 1536 if (("Android".equals(systemBrand) || "generic".equals(systemBrand) || 1537 "mainline".equals(systemBrand)) && 1538 (systemModel.startsWith("AOSP on ") || systemProduct.startsWith("aosp_") || 1539 systemModel.startsWith("GSI on ") || systemProduct.startsWith("gsi_"))) { 1540 return true; 1541 } 1542 1543 // Return true for cuttlefish instances 1544 if ((systemBrand.equals("Android") || systemBrand.equals("google")) && 1545 (systemProduct.startsWith("cf_") || systemProduct.startsWith("aosp_cf_") || 1546 systemModel.startsWith("Cuttlefish "))) { 1547 return true; 1548 } 1549 return false; 1550 } 1551 1552 /** 1553 * Function to identify if the device is a cuttlefish instance 1554 */ onCuttlefish()1555 public static boolean onCuttlefish() throws IOException { 1556 String device = SystemProperties.get("ro.product.device", ""); 1557 String model = SystemProperties.get("ro.product.model", ""); 1558 String name = SystemProperties.get("ro.product.name", ""); 1559 1560 // Return true for cuttlefish instances 1561 if (!device.startsWith("vsoc_")) { 1562 return false; 1563 } 1564 if (!model.startsWith("Cuttlefish ")) { 1565 return false; 1566 } 1567 if (name.startsWith("cf_") || name.startsWith("aosp_cf_")) { 1568 return true; 1569 } 1570 return false; 1571 } 1572 1573 /* 1574 * -------------------------------------- END -------------------------------------- 1575 */ 1576 } 1577