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