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