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 isGoogle(String codecName)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 getCodecNames( boolean isEncoder, Boolean isGoog, MediaFormat... 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 */ getDecoderNames( Boolean isGoog, MediaFormat... formats)237 public static String[] getDecoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) { 238 return getCodecNames(false /* isEncoder */, isGoog, formats); 239 } 240 getDecoderNames(MediaFormat... formats)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 */ getEncoderNames( Boolean isGoog, MediaFormat... formats)246 public static String[] getEncoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) { 247 return getCodecNames(true /* isEncoder */, isGoog, formats); 248 } 249 getEncoderNames(MediaFormat... formats)250 public static String[] getEncoderNames(MediaFormat... formats) { 251 return getCodecNames(true /* isEncoder */, null /* isGoog */, formats); 252 } 253 getDecoderNamesForMime(String mime)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 getEncoderNamesForMime(String mime)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 verifyNumCodecs( int count, boolean isEncoder, Boolean isGoog, MediaFormat... formats)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 getDecoder(MediaFormat format)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 canEncode(MediaFormat format)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 canDecode(MediaFormat format)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 supports(String codecName, String mime, int w, int h)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 supports(String codecName, MediaFormat format)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 hasCodecForTrack(MediaExtractor ex, int track)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 */ hasCodecsForMedia(MediaExtractor ex)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 */ hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix)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 hasCodecsForResourceCombo( Context context, int resourceId, int track, String mimePrefix)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 */ hasCodecsForResource(Context context, int resourceId)421 public static boolean hasCodecsForResource(Context context, int resourceId) { 422 return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, null /* mimePrefix */); 423 } 424 checkCodecsForResource(Context context, int resourceId)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 */ hasCodecForResource(Context context, int resourceId, int track)432 public static boolean hasCodecForResource(Context context, int resourceId, int track) { 433 return hasCodecsForResourceCombo(context, resourceId, track, null /* mimePrefix */); 434 } 435 checkCodecForResource(Context context, int resourceId, int track)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 */ hasCodecForResourceAndDomain( Context context, int resourceId, String mimePrefix)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 */ hasCodecsForPath(Context context, String path)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 getExtractorForPath(Context context, String path)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 checkCodecsForPath(Context context, String path)486 public static boolean checkCodecsForPath(Context context, String path) { 487 return check(hasCodecsForPath(context, path), "no decoder found"); 488 } 489 hasCodecForDomain(boolean encoder, String domain)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 checkCodecForDomain(boolean encoder, String domain)506 public static boolean checkCodecForDomain(boolean encoder, String domain) { 507 return check(hasCodecForDomain(encoder, domain), 508 "no " + domain + (encoder ? " encoder" : " decoder") + " found"); 509 } 510 hasCodecForMime(boolean encoder, String mime)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 hasCodecForMimes(boolean encoder, String[] mimes)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 hasEncoder(String... mimes)538 public static boolean hasEncoder(String... mimes) { 539 return hasCodecForMimes(true /* encoder */, mimes); 540 } 541 hasDecoder(String... mimes)542 public static boolean hasDecoder(String... mimes) { 543 return hasCodecForMimes(false /* encoder */, mimes); 544 } 545 checkDecoder(String... mimes)546 public static boolean checkDecoder(String... mimes) { 547 return check(hasCodecForMimes(false /* encoder */, mimes), "no decoder found"); 548 } 549 checkEncoder(String... mimes)550 public static boolean checkEncoder(String... mimes) { 551 return check(hasCodecForMimes(true /* encoder */, mimes), "no encoder found"); 552 } 553 canDecodeVideo(String mime, int width, int height, float rate)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 canDecodeVideo( String mime, int width, int height, float rate, Integer profile, Integer level, Integer bitrate)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 checkEncoderForFormat(MediaFormat format)577 public static boolean checkEncoderForFormat(MediaFormat format) { 578 return check(canEncode(format), "no encoder for " + format); 579 } 580 checkDecoderForFormat(MediaFormat format)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 getVideoCapabilities(String codecName, String mime)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 getTrackFormatForResource( Context context, int resourceId, String mimeTypePrefix)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 getTrackFormatForPath( Context context, String path, String mimeTypePrefix)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 getTrackFormatForExtractor( MediaExtractor extractor, String mimeTypePrefix)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 createMediaExtractorForMimeType( Context context, int resourceId, String mimeTypePrefix)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 */ setMaxEncoderFrameAndBitrates( MediaCodec encoder, MediaFormat format, int maxFps)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 setMaxEncoderFrameAndBitrates( VideoCapabilities vidCaps, MediaFormat format, int maxFps)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| */ Stats(double[] data)719 public Stats(double[] data) { 720 mData = data; 721 if (mData != null) { 722 mNum = mData.length; 723 } 724 } 725 getNum()726 public int getNum() { 727 return mNum; 728 } 729 730 /** calculate mSumX and mSumXX */ analyze()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 */ getMin()752 public double getMin() { 753 analyze(); 754 return mMinX; 755 } 756 757 /** returns the minimum or NaN if it does not exist */ getMax()758 public double getMax() { 759 analyze(); 760 return mMaxX; 761 } 762 763 /** returns the average or NaN if it does not exist. */ getAverage()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. */ getStdev()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 */ movingAverage(int n)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 */ movingAverageOverSum(double window)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 */ sort()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 */ getPercentiles(double... points)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 equals(Object o)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 */ convertDmToFl( Context context, InputStream dmStream, RandomAccessFile flFile)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 */ verifyDecoder( MediaCodec decoder, MediaExtractor ex, List<String> frameMD5Sums)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 getImageMD5Checksum(Image image)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 convertByteArrayToHEXString(byte[] ba)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