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