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