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 } 898 } 899 } 900 return false; 901 } 902 903 /* 904 * ------------------ HELPER METHODS FOR STATISTICS AND REPORTING ------------------ 905 */ 906 907 // TODO: migrate this into com.android.compatibility.common.util.Stat 908 public static class Stats { 909 /** does not support NaN or Inf in |data| */ Stats(double[] data)910 public Stats(double[] data) { 911 mData = data; 912 if (mData != null) { 913 mNum = mData.length; 914 } 915 } 916 getNum()917 public int getNum() { 918 return mNum; 919 } 920 921 /** calculate mSumX and mSumXX */ analyze()922 private void analyze() { 923 if (mAnalyzed) { 924 return; 925 } 926 927 if (mData != null) { 928 for (double x : mData) { 929 if (!(x >= mMinX)) { // mMinX may be NaN 930 mMinX = x; 931 } 932 if (!(x <= mMaxX)) { // mMaxX may be NaN 933 mMaxX = x; 934 } 935 mSumX += x; 936 mSumXX += x * x; 937 } 938 } 939 mAnalyzed = true; 940 } 941 942 /** returns the maximum or NaN if it does not exist */ getMin()943 public double getMin() { 944 analyze(); 945 return mMinX; 946 } 947 948 /** returns the minimum or NaN if it does not exist */ getMax()949 public double getMax() { 950 analyze(); 951 return mMaxX; 952 } 953 954 /** returns the average or NaN if it does not exist. */ getAverage()955 public double getAverage() { 956 analyze(); 957 if (mNum == 0) { 958 return Double.NaN; 959 } else { 960 return mSumX / mNum; 961 } 962 } 963 964 /** returns the standard deviation or NaN if it does not exist. */ getStdev()965 public double getStdev() { 966 analyze(); 967 if (mNum == 0) { 968 return Double.NaN; 969 } else { 970 double average = mSumX / mNum; 971 return Math.sqrt(mSumXX / mNum - average * average); 972 } 973 } 974 975 /** returns the statistics for the moving average over n values */ movingAverage(int n)976 public Stats movingAverage(int n) { 977 if (n < 1 || mNum < n) { 978 return new Stats(null); 979 } else if (n == 1) { 980 return this; 981 } 982 983 double[] avgs = new double[mNum - n + 1]; 984 double sum = 0; 985 for (int i = 0; i < mNum; ++i) { 986 sum += mData[i]; 987 if (i >= n - 1) { 988 avgs[i - n + 1] = sum / n; 989 sum -= mData[i - n + 1]; 990 } 991 } 992 return new Stats(avgs); 993 } 994 995 /** returns the statistics for the moving average over a window over the 996 * cumulative sum. Basically, moves a window from: [0, window] to 997 * [sum - window, sum] over the cumulative sum, over ((sum - window) / average) 998 * steps, and returns the average value over each window. 999 * This method is used to average time-diff data over a window of a constant time. 1000 */ movingAverageOverSum(double window)1001 public Stats movingAverageOverSum(double window) { 1002 if (window <= 0 || mNum < 1) { 1003 return new Stats(null); 1004 } 1005 1006 analyze(); 1007 double average = mSumX / mNum; 1008 if (window >= mSumX) { 1009 return new Stats(new double[] { average }); 1010 } 1011 int samples = (int)Math.ceil((mSumX - window) / average); 1012 double[] avgs = new double[samples]; 1013 1014 // A somewhat brute force approach to calculating the moving average. 1015 // TODO: add support for weights in Stats, so we can do a more refined approach. 1016 double sum = 0; // sum of elements in the window 1017 int num = 0; // number of elements in the moving window 1018 int bi = 0; // index of the first element in the moving window 1019 int ei = 0; // index of the last element in the moving window 1020 double space = window; // space at the end of the window 1021 double foot = 0; // space at the beginning of the window 1022 1023 // invariants: foot + sum + space == window 1024 // bi + num == ei 1025 // 1026 // window: |-------------------------------| 1027 // | <-----sum------> | 1028 // <foot> <---space--> 1029 // | | 1030 // intervals: |-----------|-------|-------|--------------------|--------| 1031 // ^bi ^ei 1032 1033 int ix = 0; // index in the result 1034 while (ix < samples) { 1035 // add intervals while there is space in the window 1036 while (ei < mData.length && mData[ei] <= space) { 1037 space -= mData[ei]; 1038 sum += mData[ei]; 1039 num++; 1040 ei++; 1041 } 1042 1043 // calculate average over window and deal with odds and ends (e.g. if there are no 1044 // intervals in the current window: pick whichever element overlaps the window 1045 // most. 1046 if (num > 0) { 1047 avgs[ix++] = sum / num; 1048 } else if (bi > 0 && foot > space) { 1049 // consider previous 1050 avgs[ix++] = mData[bi - 1]; 1051 } else if (ei == mData.length) { 1052 break; 1053 } else { 1054 avgs[ix++] = mData[ei]; 1055 } 1056 1057 // move the window to the next position 1058 foot -= average; 1059 space += average; 1060 1061 // remove intervals that are now partially or wholly outside of the window 1062 while (bi < ei && foot < 0) { 1063 foot += mData[bi]; 1064 sum -= mData[bi]; 1065 num--; 1066 bi++; 1067 } 1068 } 1069 return new Stats(Arrays.copyOf(avgs, ix)); 1070 } 1071 1072 /** calculate mSortedData */ sort()1073 private void sort() { 1074 if (mSorted || mNum == 0) { 1075 return; 1076 } 1077 mSortedData = Arrays.copyOf(mData, mNum); 1078 Arrays.sort(mSortedData); 1079 mSorted = true; 1080 } 1081 1082 /** returns an array of percentiles for the points using nearest rank */ getPercentiles(double... points)1083 public double[] getPercentiles(double... points) { 1084 sort(); 1085 double[] res = new double[points.length]; 1086 for (int i = 0; i < points.length; ++i) { 1087 if (mNum < 1 || points[i] < 0 || points[i] > 100) { 1088 res[i] = Double.NaN; 1089 } else { 1090 res[i] = mSortedData[(int)Math.round(points[i] / 100 * (mNum - 1))]; 1091 } 1092 } 1093 return res; 1094 } 1095 1096 @Override equals(Object o)1097 public boolean equals(Object o) { 1098 if (o instanceof Stats) { 1099 Stats other = (Stats)o; 1100 if (other.mNum != mNum) { 1101 return false; 1102 } else if (mNum == 0) { 1103 return true; 1104 } 1105 return Arrays.equals(mData, other.mData); 1106 } 1107 return false; 1108 } 1109 1110 private double[] mData; 1111 private double mSumX = 0; 1112 private double mSumXX = 0; 1113 private double mMinX = Double.NaN; 1114 private double mMaxX = Double.NaN; 1115 private int mNum = 0; 1116 private boolean mAnalyzed = false; 1117 private double[] mSortedData; 1118 private boolean mSorted = false; 1119 } 1120 1121 /** 1122 * Convert a forward lock .dm message stream to a .fl file 1123 * @param context Context to use 1124 * @param dmStream The .dm message 1125 * @param flFile The output file to be written 1126 * @return success 1127 */ convertDmToFl( Context context, InputStream dmStream, RandomAccessFile flFile)1128 public static boolean convertDmToFl( 1129 Context context, 1130 InputStream dmStream, 1131 RandomAccessFile flFile) { 1132 final String MIMETYPE_DRM_MESSAGE = "application/vnd.oma.drm.message"; 1133 byte[] dmData = new byte[10000]; 1134 int totalRead = 0; 1135 int numRead; 1136 while (true) { 1137 try { 1138 numRead = dmStream.read(dmData, totalRead, dmData.length - totalRead); 1139 } catch (IOException e) { 1140 Log.w(TAG, "Failed to read from input file"); 1141 return false; 1142 } 1143 if (numRead == -1) { 1144 break; 1145 } 1146 totalRead += numRead; 1147 if (totalRead == dmData.length) { 1148 // grow array 1149 dmData = Arrays.copyOf(dmData, dmData.length + 10000); 1150 } 1151 } 1152 byte[] fileData = Arrays.copyOf(dmData, totalRead); 1153 1154 DrmManagerClient drmClient = null; 1155 try { 1156 drmClient = new DrmManagerClient(context); 1157 } catch (IllegalArgumentException e) { 1158 Log.w(TAG, "DrmManagerClient instance could not be created, context is Illegal."); 1159 return false; 1160 } catch (IllegalStateException e) { 1161 Log.w(TAG, "DrmManagerClient didn't initialize properly."); 1162 return false; 1163 } 1164 1165 try { 1166 int convertSessionId = -1; 1167 try { 1168 convertSessionId = drmClient.openConvertSession(MIMETYPE_DRM_MESSAGE); 1169 } catch (IllegalArgumentException e) { 1170 Log.w(TAG, "Conversion of Mimetype: " + MIMETYPE_DRM_MESSAGE 1171 + " is not supported.", e); 1172 return false; 1173 } catch (IllegalStateException e) { 1174 Log.w(TAG, "Could not access Open DrmFramework.", e); 1175 return false; 1176 } 1177 1178 if (convertSessionId < 0) { 1179 Log.w(TAG, "Failed to open session."); 1180 return false; 1181 } 1182 1183 DrmConvertedStatus convertedStatus = null; 1184 try { 1185 convertedStatus = drmClient.convertData(convertSessionId, fileData); 1186 } catch (IllegalArgumentException e) { 1187 Log.w(TAG, "Buffer with data to convert is illegal. Convertsession: " 1188 + convertSessionId, e); 1189 return false; 1190 } catch (IllegalStateException e) { 1191 Log.w(TAG, "Could not convert data. Convertsession: " + convertSessionId, e); 1192 return false; 1193 } 1194 1195 if (convertedStatus == null || 1196 convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK || 1197 convertedStatus.convertedData == null) { 1198 Log.w(TAG, "Error in converting data. Convertsession: " + convertSessionId); 1199 try { 1200 DrmConvertedStatus result = drmClient.closeConvertSession(convertSessionId); 1201 if (result.statusCode != DrmConvertedStatus.STATUS_OK) { 1202 Log.w(TAG, "Conversion failed with status: " + result.statusCode); 1203 return false; 1204 } 1205 } catch (IllegalStateException e) { 1206 Log.w(TAG, "Could not close session. Convertsession: " + 1207 convertSessionId, e); 1208 } 1209 return false; 1210 } 1211 1212 try { 1213 flFile.write(convertedStatus.convertedData, 0, convertedStatus.convertedData.length); 1214 } catch (IOException e) { 1215 Log.w(TAG, "Failed to write to output file: " + e); 1216 return false; 1217 } 1218 1219 try { 1220 convertedStatus = drmClient.closeConvertSession(convertSessionId); 1221 } catch (IllegalStateException e) { 1222 Log.w(TAG, "Could not close convertsession. Convertsession: " + 1223 convertSessionId, e); 1224 return false; 1225 } 1226 1227 if (convertedStatus == null || 1228 convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK || 1229 convertedStatus.convertedData == null) { 1230 Log.w(TAG, "Error in closing session. Convertsession: " + convertSessionId); 1231 return false; 1232 } 1233 1234 try { 1235 flFile.seek(convertedStatus.offset); 1236 flFile.write(convertedStatus.convertedData); 1237 } catch (IOException e) { 1238 Log.w(TAG, "Could not update file.", e); 1239 return false; 1240 } 1241 1242 return true; 1243 } finally { 1244 drmClient.close(); 1245 } 1246 } 1247 1248 /** 1249 * @param decoder new MediaCodec object 1250 * @param ex MediaExtractor after setDataSource and selectTrack 1251 * @param frameMD5Sums reference MD5 checksum for decoded frames 1252 * @return true if decoded frames checksums matches reference checksums 1253 * @throws IOException 1254 */ verifyDecoder( MediaCodec decoder, MediaExtractor ex, List<String> frameMD5Sums)1255 public static boolean verifyDecoder( 1256 MediaCodec decoder, MediaExtractor ex, List<String> frameMD5Sums) 1257 throws IOException { 1258 1259 int trackIndex = ex.getSampleTrackIndex(); 1260 MediaFormat format = ex.getTrackFormat(trackIndex); 1261 decoder.configure(format, null /* surface */, null /* crypto */, 0 /* flags */); 1262 decoder.start(); 1263 1264 boolean sawInputEOS = false; 1265 boolean sawOutputEOS = false; 1266 final long kTimeOutUs = 5000; // 5ms timeout 1267 int decodedFrameCount = 0; 1268 int expectedFrameCount = frameMD5Sums.size(); 1269 MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); 1270 1271 while (!sawOutputEOS) { 1272 // handle input 1273 if (!sawInputEOS) { 1274 int inIdx = decoder.dequeueInputBuffer(kTimeOutUs); 1275 if (inIdx >= 0) { 1276 ByteBuffer buffer = decoder.getInputBuffer(inIdx); 1277 int sampleSize = ex.readSampleData(buffer, 0); 1278 if (sampleSize < 0) { 1279 final int flagEOS = MediaCodec.BUFFER_FLAG_END_OF_STREAM; 1280 decoder.queueInputBuffer(inIdx, 0, 0, 0, flagEOS); 1281 sawInputEOS = true; 1282 } else { 1283 decoder.queueInputBuffer(inIdx, 0, sampleSize, ex.getSampleTime(), 0); 1284 ex.advance(); 1285 } 1286 } 1287 } 1288 1289 // handle output 1290 int outputBufIndex = decoder.dequeueOutputBuffer(info, kTimeOutUs); 1291 if (outputBufIndex >= 0) { 1292 try { 1293 if (info.size > 0) { 1294 // Disregard 0-sized buffers at the end. 1295 String md5CheckSum = ""; 1296 Image image = decoder.getOutputImage(outputBufIndex); 1297 md5CheckSum = getImageMD5Checksum(image); 1298 1299 if (!md5CheckSum.equals(frameMD5Sums.get(decodedFrameCount))) { 1300 Log.d(TAG, 1301 String.format( 1302 "Frame %d md5sum mismatch: %s(actual) vs %s(expected)", 1303 decodedFrameCount, md5CheckSum, 1304 frameMD5Sums.get(decodedFrameCount))); 1305 return false; 1306 } 1307 1308 decodedFrameCount++; 1309 } 1310 } catch (Exception e) { 1311 Log.e(TAG, "getOutputImage md5CheckSum failed", e); 1312 return false; 1313 } finally { 1314 decoder.releaseOutputBuffer(outputBufIndex, false /* render */); 1315 } 1316 if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { 1317 sawOutputEOS = true; 1318 } 1319 } else if (outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 1320 MediaFormat decOutputFormat = decoder.getOutputFormat(); 1321 Log.d(TAG, "output format " + decOutputFormat); 1322 } else if (outputBufIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { 1323 Log.i(TAG, "Skip handling MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED"); 1324 } else if (outputBufIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { 1325 continue; 1326 } else { 1327 Log.w(TAG, "decoder.dequeueOutputBuffer() unrecognized index: " + outputBufIndex); 1328 return false; 1329 } 1330 } 1331 1332 if (decodedFrameCount != expectedFrameCount) { 1333 return false; 1334 } 1335 1336 return true; 1337 } 1338 getImageMD5Checksum(Image image)1339 public static String getImageMD5Checksum(Image image) throws Exception { 1340 int format = image.getFormat(); 1341 if (ImageFormat.YUV_420_888 != format) { 1342 Log.w(TAG, "unsupported image format"); 1343 return ""; 1344 } 1345 1346 MessageDigest md = MessageDigest.getInstance("MD5"); 1347 1348 Rect crop = image.getCropRect(); 1349 int cropLeft = crop.left; 1350 int cropRight = crop.right; 1351 int cropTop = crop.top; 1352 int cropBottom = crop.bottom; 1353 1354 int imageWidth = cropRight - cropLeft; 1355 int imageHeight = cropBottom - cropTop; 1356 1357 Image.Plane[] planes = image.getPlanes(); 1358 for (int i = 0; i < planes.length; ++i) { 1359 ByteBuffer buf = planes[i].getBuffer(); 1360 1361 int width, height, rowStride, pixelStride, x, y, top, left; 1362 rowStride = planes[i].getRowStride(); 1363 pixelStride = planes[i].getPixelStride(); 1364 if (i == 0) { 1365 width = imageWidth; 1366 height = imageHeight; 1367 left = cropLeft; 1368 top = cropTop; 1369 } else { 1370 width = imageWidth / 2; 1371 height = imageHeight /2; 1372 left = cropLeft / 2; 1373 top = cropTop / 2; 1374 } 1375 // local contiguous pixel buffer 1376 byte[] bb = new byte[width * height]; 1377 if (buf.hasArray()) { 1378 byte b[] = buf.array(); 1379 int offs = buf.arrayOffset() + left * pixelStride; 1380 if (pixelStride == 1) { 1381 for (y = 0; y < height; ++y) { 1382 System.arraycopy(bb, y * width, b, (top + y) * rowStride + offs, width); 1383 } 1384 } else { 1385 // do it pixel-by-pixel 1386 for (y = 0; y < height; ++y) { 1387 int lineOffset = offs + (top + y) * rowStride; 1388 for (x = 0; x < width; ++x) { 1389 bb[y * width + x] = b[lineOffset + x * pixelStride]; 1390 } 1391 } 1392 } 1393 } else { // almost always ends up here due to direct buffers 1394 int pos = buf.position(); 1395 if (pixelStride == 1) { 1396 for (y = 0; y < height; ++y) { 1397 buf.position(pos + left + (top + y) * rowStride); 1398 buf.get(bb, y * width, width); 1399 } 1400 } else { 1401 // local line buffer 1402 byte[] lb = new byte[rowStride]; 1403 // do it pixel-by-pixel 1404 for (y = 0; y < height; ++y) { 1405 buf.position(pos + left * pixelStride + (top + y) * rowStride); 1406 // we're only guaranteed to have pixelStride * (width - 1) + 1 bytes 1407 buf.get(lb, 0, pixelStride * (width - 1) + 1); 1408 for (x = 0; x < width; ++x) { 1409 bb[y * width + x] = lb[x * pixelStride]; 1410 } 1411 } 1412 } 1413 buf.position(pos); 1414 } 1415 md.update(bb, 0, width * height); 1416 } 1417 1418 return convertByteArrayToHEXString(md.digest()); 1419 } 1420 convertByteArrayToHEXString(byte[] ba)1421 private static String convertByteArrayToHEXString(byte[] ba) throws Exception { 1422 StringBuilder result = new StringBuilder(); 1423 for (int i = 0; i < ba.length; i++) { 1424 result.append(Integer.toString((ba[i] & 0xff) + 0x100, 16).substring(1)); 1425 } 1426 return result.toString(); 1427 } 1428 1429 1430 /* 1431 * ------------------- HELPER METHODS FOR DETECTING DEVICE TYPES ------------------- 1432 */ 1433 hasDeviceGotBattery()1434 public static boolean hasDeviceGotBattery() { 1435 final Intent batteryInfo = mContext.registerReceiver(null, 1436 new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 1437 return batteryInfo.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true); 1438 } 1439 getScreenSizeInInches()1440 public static double getScreenSizeInInches() { 1441 DisplayMetrics dm = mContext.getResources().getDisplayMetrics(); 1442 double widthInInchesSquared = Math.pow(dm.widthPixels/dm.xdpi,2); 1443 double heightInInchesSquared = Math.pow(dm.heightPixels/dm.ydpi,2); 1444 double diagonalInInches = Math.sqrt(widthInInchesSquared + heightInInchesSquared); 1445 return diagonalInInches; 1446 } 1447 isTv()1448 public static boolean isTv() { 1449 return pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) || 1450 pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION); 1451 } 1452 hasMicrophone()1453 public static boolean hasMicrophone() { 1454 return pm.hasSystemFeature(PackageManager.FEATURE_MICROPHONE); 1455 } 1456 hasCamera()1457 public static boolean hasCamera() { 1458 return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); 1459 } 1460 isWatch()1461 public static boolean isWatch() { 1462 return pm.hasSystemFeature(PackageManager.FEATURE_WATCH); 1463 } 1464 isAutomotive()1465 public static boolean isAutomotive() { 1466 return pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); 1467 } 1468 isPc()1469 public static boolean isPc() { 1470 return pm.hasSystemFeature(PackageManager.FEATURE_PC); 1471 } 1472 hasAudioOutput()1473 public static boolean hasAudioOutput() { 1474 return pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT); 1475 } 1476 isHandheld()1477 public static boolean isHandheld() { 1478 double screenSize = getScreenSizeInInches(); 1479 if (screenSize < (FIRST_SDK_IS_AT_LEAST_R ? 3.3 : 2.5)) return false; 1480 if (screenSize > 8.0) return false; 1481 if (!hasDeviceGotBattery()) return false; 1482 // handheld nature is not exposed to package manager, so for now, 1483 // in addition to physical screen size, the following checks are 1484 // also required: 1485 if (!pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) return false; 1486 if (isWatch()) return false; 1487 if (isTv()) return false; 1488 if (isAutomotive()) return false; 1489 if (isPc()) return false; 1490 return true; 1491 } 1492 isTablet()1493 public static boolean isTablet() { 1494 double screenSize = getScreenSizeInInches(); 1495 if (screenSize < 7.0) return false; 1496 if (screenSize > 18.0) return false; 1497 if (!hasDeviceGotBattery()) return false; 1498 // tablet nature is not exposed to package manager, so for now, 1499 // in addition to physical screen size, the following checks are 1500 // also required: 1501 if (!pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) return false; 1502 if (isWatch()) return false; 1503 if (isTv()) return false; 1504 if (isAutomotive()) return false; 1505 if (isPc()) return false; 1506 return true; 1507 } 1508 1509 /* 1510 * ------------------- HELPER METHODS FOR DETECTING NON-PRODUCTION DEVICES ------------------- 1511 */ 1512 1513 /* 1514 * Some parts of media CTS verifies device characterization that does not make sense for 1515 * non-production devices (such as GSI and cuttlefish). We call these devices 'frankenDevices'. 1516 * We may also limit test duration on these devices. 1517 */ onFrankenDevice()1518 public static boolean onFrankenDevice() throws IOException { 1519 String systemBrand = PropertyUtil.getProperty("ro.product.system.brand"); 1520 String systemModel = PropertyUtil.getProperty("ro.product.system.model"); 1521 String systemProduct = PropertyUtil.getProperty("ro.product.system.name"); 1522 1523 // not all devices may have system_ext partition, but if they do use that 1524 { 1525 String systemExtProduct = PropertyUtil.getProperty("ro.product.system_ext.name"); 1526 if (systemExtProduct != null) { 1527 systemProduct = systemExtProduct; 1528 } 1529 String systemExtModel = PropertyUtil.getProperty("ro.product.system_ext.model"); 1530 if (systemExtModel != null) { 1531 systemModel = systemExtModel; 1532 } 1533 } 1534 1535 if (("Android".equals(systemBrand) || "generic".equals(systemBrand) || 1536 "mainline".equals(systemBrand)) && 1537 (systemModel.startsWith("AOSP on ") || systemProduct.startsWith("aosp_") || 1538 systemModel.startsWith("GSI on ") || systemProduct.startsWith("gsi_"))) { 1539 return true; 1540 } 1541 1542 // Return true for cuttlefish instances 1543 if ((systemBrand.equals("Android") || systemBrand.equals("google")) && 1544 (systemProduct.startsWith("cf_") || systemProduct.startsWith("aosp_cf_") || 1545 systemModel.startsWith("Cuttlefish "))) { 1546 return true; 1547 } 1548 return false; 1549 } 1550 1551 /* 1552 * -------------------------------------- END -------------------------------------- 1553 */ 1554 } 1555