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