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 */ getTestName()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 */ getTestNameWithClass()84 public static String getTestNameWithClass() { 85 return getTestName(true /* withClass */); 86 } 87 getTestName(boolean withClass)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 */ skipTest(String tag, String reason)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 */ skipTest(String reason)183 public static void skipTest(String reason) { 184 skipTest(TAG, reason); 185 } 186 check(boolean result, String message)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 getCodecNames( boolean isEncoder, Boolean isGoog, MediaFormat... 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 */ getDecoderNames( Boolean isGoog, MediaFormat... formats)231 public static String[] getDecoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) { 232 return getCodecNames(false /* isEncoder */, isGoog, formats); 233 } 234 getDecoderNames(MediaFormat... formats)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 */ getEncoderNames( Boolean isGoog, MediaFormat... formats)240 public static String[] getEncoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) { 241 return getCodecNames(true /* isEncoder */, isGoog, formats); 242 } 243 getEncoderNames(MediaFormat... formats)244 public static String[] getEncoderNames(MediaFormat... formats) { 245 return getCodecNames(true /* isEncoder */, null /* isGoog */, formats); 246 } 247 verifyNumCodecs( int count, boolean isEncoder, Boolean isGoog, MediaFormat... formats)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 getDecoder(MediaFormat format)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 canEncode(MediaFormat format)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 canDecode(MediaFormat format)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 supports(String codecName, String mime, int w, int h)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 supports(String codecName, MediaFormat format)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 hasCodecForTrack(MediaExtractor ex, int track)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 */ hasCodecsForMedia(MediaExtractor ex)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 */ hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix)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 hasCodecsForResourceCombo( Context context, int resourceId, int track, String mimePrefix)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 */ hasCodecsForResource(Context context, int resourceId)403 public static boolean hasCodecsForResource(Context context, int resourceId) { 404 return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, null /* mimePrefix */); 405 } 406 checkCodecsForResource(Context context, int resourceId)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 */ hasCodecForResource(Context context, int resourceId, int track)414 public static boolean hasCodecForResource(Context context, int resourceId, int track) { 415 return hasCodecsForResourceCombo(context, resourceId, track, null /* mimePrefix */); 416 } 417 checkCodecForResource(Context context, int resourceId, int track)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 */ hasCodecForResourceAndDomain( Context context, int resourceId, String mimePrefix)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 */ hasCodecsForPath(Context context, String path)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 getExtractorForPath(Context context, String path)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 checkCodecsForPath(Context context, String path)468 public static boolean checkCodecsForPath(Context context, String path) { 469 return check(hasCodecsForPath(context, path), "no decoder found"); 470 } 471 hasCodecForDomain(boolean encoder, String domain)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 checkCodecForDomain(boolean encoder, String domain)488 public static boolean checkCodecForDomain(boolean encoder, String domain) { 489 return check(hasCodecForDomain(encoder, domain), 490 "no " + domain + (encoder ? " encoder" : " decoder") + " found"); 491 } 492 hasCodecForMime(boolean encoder, String mime)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 hasCodecForMimes(boolean encoder, String[] mimes)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 hasEncoder(String... mimes)520 public static boolean hasEncoder(String... mimes) { 521 return hasCodecForMimes(true /* encoder */, mimes); 522 } 523 hasDecoder(String... mimes)524 public static boolean hasDecoder(String... mimes) { 525 return hasCodecForMimes(false /* encoder */, mimes); 526 } 527 checkDecoder(String... mimes)528 public static boolean checkDecoder(String... mimes) { 529 return check(hasCodecForMimes(false /* encoder */, mimes), "no decoder found"); 530 } 531 checkEncoder(String... mimes)532 public static boolean checkEncoder(String... mimes) { 533 return check(hasCodecForMimes(true /* encoder */, mimes), "no encoder found"); 534 } 535 canDecodeVideo(String mime, int width, int height, float rate)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 canDecodeVideo( String mime, int width, int height, float rate, Integer profile, Integer level, Integer bitrate)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 checkEncoderForFormat(MediaFormat format)559 public static boolean checkEncoderForFormat(MediaFormat format) { 560 return check(canEncode(format), "no encoder for " + format); 561 } 562 checkDecoderForFormat(MediaFormat format)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 getVideoCapabilities(String codecName, String mime)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 getTrackFormatForResource( Context context, int resourceId, String mimeTypePrefix)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 getTrackFormatForPath( Context context, String path, String mimeTypePrefix)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 getTrackFormatForExtractor( MediaExtractor extractor, String mimeTypePrefix)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 createMediaExtractorForMimeType( Context context, int resourceId, String mimeTypePrefix)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 */ setMaxEncoderFrameAndBitrates( MediaCodec encoder, MediaFormat format, int maxFps)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 setMaxEncoderFrameAndBitrates( VideoCapabilities vidCaps, MediaFormat format, int maxFps)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| */ Stats(double[] data)701 public Stats(double[] data) { 702 mData = data; 703 if (mData != null) { 704 mNum = mData.length; 705 } 706 } 707 getNum()708 public int getNum() { 709 return mNum; 710 } 711 712 /** calculate mSumX and mSumXX */ analyze()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 */ getMin()734 public double getMin() { 735 analyze(); 736 return mMinX; 737 } 738 739 /** returns the minimum or NaN if it does not exist */ getMax()740 public double getMax() { 741 analyze(); 742 return mMaxX; 743 } 744 745 /** returns the average or NaN if it does not exist. */ getAverage()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. */ getStdev()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 */ movingAverage(int n)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 */ movingAverageOverSum(double window)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 */ sort()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 */ getPercentiles(double... points)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 equals(Object o)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 */ convertDmToFl( Context context, InputStream dmStream, RandomAccessFile flFile)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 */ verifyDecoder( MediaCodec decoder, MediaExtractor ex, List<String> frameMD5Sums)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 getImageMD5Checksum(Image image)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 convertByteArrayToHEXString(byte[] ba)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