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