• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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