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