• 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                      return false;
898                 }
899             }
900         }
901         return false;
902     }
903 
904     /*
905      *  ------------------ HELPER METHODS FOR STATISTICS AND REPORTING ------------------
906      */
907 
908     // TODO: migrate this into com.android.compatibility.common.util.Stat
909     public static class Stats {
910         /** does not support NaN or Inf in |data| */
Stats(double[] data)911         public Stats(double[] data) {
912             mData = data;
913             if (mData != null) {
914                 mNum = mData.length;
915             }
916         }
917 
getNum()918         public int getNum() {
919             return mNum;
920         }
921 
922         /** calculate mSumX and mSumXX */
analyze()923         private void analyze() {
924             if (mAnalyzed) {
925                 return;
926             }
927 
928             if (mData != null) {
929                 for (double x : mData) {
930                     if (!(x >= mMinX)) { // mMinX may be NaN
931                         mMinX = x;
932                     }
933                     if (!(x <= mMaxX)) { // mMaxX may be NaN
934                         mMaxX = x;
935                     }
936                     mSumX += x;
937                     mSumXX += x * x;
938                 }
939             }
940             mAnalyzed = true;
941         }
942 
943         /** returns the maximum or NaN if it does not exist */
getMin()944         public double getMin() {
945             analyze();
946             return mMinX;
947         }
948 
949         /** returns the minimum or NaN if it does not exist */
getMax()950         public double getMax() {
951             analyze();
952             return mMaxX;
953         }
954 
955         /** returns the average or NaN if it does not exist. */
getAverage()956         public double getAverage() {
957             analyze();
958             if (mNum == 0) {
959                 return Double.NaN;
960             } else {
961                 return mSumX / mNum;
962             }
963         }
964 
965         /** returns the standard deviation or NaN if it does not exist. */
getStdev()966         public double getStdev() {
967             analyze();
968             if (mNum == 0) {
969                 return Double.NaN;
970             } else {
971                 double average = mSumX / mNum;
972                 return Math.sqrt(mSumXX / mNum - average * average);
973             }
974         }
975 
976         /** returns the statistics for the moving average over n values */
movingAverage(int n)977         public Stats movingAverage(int n) {
978             if (n < 1 || mNum < n) {
979                 return new Stats(null);
980             } else if (n == 1) {
981                 return this;
982             }
983 
984             double[] avgs = new double[mNum - n + 1];
985             double sum = 0;
986             for (int i = 0; i < mNum; ++i) {
987                 sum += mData[i];
988                 if (i >= n - 1) {
989                     avgs[i - n + 1] = sum / n;
990                     sum -= mData[i - n + 1];
991                 }
992             }
993             return new Stats(avgs);
994         }
995 
996         /** returns the statistics for the moving average over a window over the
997          *  cumulative sum. Basically, moves a window from: [0, window] to
998          *  [sum - window, sum] over the cumulative sum, over ((sum - window) / average)
999          *  steps, and returns the average value over each window.
1000          *  This method is used to average time-diff data over a window of a constant time.
1001          */
movingAverageOverSum(double window)1002         public Stats movingAverageOverSum(double window) {
1003             if (window <= 0 || mNum < 1) {
1004                 return new Stats(null);
1005             }
1006 
1007             analyze();
1008             double average = mSumX / mNum;
1009             if (window >= mSumX) {
1010                 return new Stats(new double[] { average });
1011             }
1012             int samples = (int)Math.ceil((mSumX - window) / average);
1013             double[] avgs = new double[samples];
1014 
1015             // A somewhat brute force approach to calculating the moving average.
1016             // TODO: add support for weights in Stats, so we can do a more refined approach.
1017             double sum = 0; // sum of elements in the window
1018             int num = 0; // number of elements in the moving window
1019             int bi = 0; // index of the first element in the moving window
1020             int ei = 0; // index of the last element in the moving window
1021             double space = window; // space at the end of the window
1022             double foot = 0; // space at the beginning of the window
1023 
1024             // invariants: foot + sum + space == window
1025             //             bi + num == ei
1026             //
1027             //  window:             |-------------------------------|
1028             //                      |    <-----sum------>           |
1029             //                      <foot>               <---space-->
1030             //                           |               |
1031             //  intervals:   |-----------|-------|-------|--------------------|--------|
1032             //                           ^bi             ^ei
1033 
1034             int ix = 0; // index in the result
1035             while (ix < samples) {
1036                 // add intervals while there is space in the window
1037                 while (ei < mData.length && mData[ei] <= space) {
1038                     space -= mData[ei];
1039                     sum += mData[ei];
1040                     num++;
1041                     ei++;
1042                 }
1043 
1044                 // calculate average over window and deal with odds and ends (e.g. if there are no
1045                 // intervals in the current window: pick whichever element overlaps the window
1046                 // most.
1047                 if (num > 0) {
1048                     avgs[ix++] = sum / num;
1049                 } else if (bi > 0 && foot > space) {
1050                     // consider previous
1051                     avgs[ix++] = mData[bi - 1];
1052                 } else if (ei == mData.length) {
1053                     break;
1054                 } else {
1055                     avgs[ix++] = mData[ei];
1056                 }
1057 
1058                 // move the window to the next position
1059                 foot -= average;
1060                 space += average;
1061 
1062                 // remove intervals that are now partially or wholly outside of the window
1063                 while (bi < ei && foot < 0) {
1064                     foot += mData[bi];
1065                     sum -= mData[bi];
1066                     num--;
1067                     bi++;
1068                 }
1069             }
1070             return new Stats(Arrays.copyOf(avgs, ix));
1071         }
1072 
1073         /** calculate mSortedData */
sort()1074         private void sort() {
1075             if (mSorted || mNum == 0) {
1076                 return;
1077             }
1078             mSortedData = Arrays.copyOf(mData, mNum);
1079             Arrays.sort(mSortedData);
1080             mSorted = true;
1081         }
1082 
1083         /** returns an array of percentiles for the points using nearest rank */
getPercentiles(double... points)1084         public double[] getPercentiles(double... points) {
1085             sort();
1086             double[] res = new double[points.length];
1087             for (int i = 0; i < points.length; ++i) {
1088                 if (mNum < 1 || points[i] < 0 || points[i] > 100) {
1089                     res[i] = Double.NaN;
1090                 } else {
1091                     res[i] = mSortedData[(int)Math.round(points[i] / 100 * (mNum - 1))];
1092                 }
1093             }
1094             return res;
1095         }
1096 
1097         @Override
equals(Object o)1098         public boolean equals(Object o) {
1099             if (o instanceof Stats) {
1100                 Stats other = (Stats)o;
1101                 if (other.mNum != mNum) {
1102                     return false;
1103                 } else if (mNum == 0) {
1104                     return true;
1105                 }
1106                 return Arrays.equals(mData, other.mData);
1107             }
1108             return false;
1109         }
1110 
1111         private double[] mData;
1112         private double mSumX = 0;
1113         private double mSumXX = 0;
1114         private double mMinX = Double.NaN;
1115         private double mMaxX = Double.NaN;
1116         private int mNum = 0;
1117         private boolean mAnalyzed = false;
1118         private double[] mSortedData;
1119         private boolean mSorted = false;
1120     }
1121 
1122     /**
1123      * Convert a forward lock .dm message stream to a .fl file
1124      * @param context Context to use
1125      * @param dmStream The .dm message
1126      * @param flFile The output file to be written
1127      * @return success
1128      */
convertDmToFl( Context context, InputStream dmStream, RandomAccessFile flFile)1129     public static boolean convertDmToFl(
1130             Context context,
1131             InputStream dmStream,
1132             RandomAccessFile flFile) {
1133         final String MIMETYPE_DRM_MESSAGE = "application/vnd.oma.drm.message";
1134         byte[] dmData = new byte[10000];
1135         int totalRead = 0;
1136         int numRead;
1137         while (true) {
1138             try {
1139                 numRead = dmStream.read(dmData, totalRead, dmData.length - totalRead);
1140             } catch (IOException e) {
1141                 Log.w(TAG, "Failed to read from input file");
1142                 return false;
1143             }
1144             if (numRead == -1) {
1145                 break;
1146             }
1147             totalRead += numRead;
1148             if (totalRead == dmData.length) {
1149                 // grow array
1150                 dmData = Arrays.copyOf(dmData, dmData.length + 10000);
1151             }
1152         }
1153         byte[] fileData = Arrays.copyOf(dmData, totalRead);
1154 
1155         DrmManagerClient drmClient = null;
1156         try {
1157             drmClient = new DrmManagerClient(context);
1158         } catch (IllegalArgumentException e) {
1159             Log.w(TAG, "DrmManagerClient instance could not be created, context is Illegal.");
1160             return false;
1161         } catch (IllegalStateException e) {
1162             Log.w(TAG, "DrmManagerClient didn't initialize properly.");
1163             return false;
1164         }
1165 
1166         try {
1167             int convertSessionId = -1;
1168             try {
1169                 convertSessionId = drmClient.openConvertSession(MIMETYPE_DRM_MESSAGE);
1170             } catch (IllegalArgumentException e) {
1171                 Log.w(TAG, "Conversion of Mimetype: " + MIMETYPE_DRM_MESSAGE
1172                         + " is not supported.", e);
1173                 return false;
1174             } catch (IllegalStateException e) {
1175                 Log.w(TAG, "Could not access Open DrmFramework.", e);
1176                 return false;
1177             }
1178 
1179             if (convertSessionId < 0) {
1180                 Log.w(TAG, "Failed to open session.");
1181                 return false;
1182             }
1183 
1184             DrmConvertedStatus convertedStatus = null;
1185             try {
1186                 convertedStatus = drmClient.convertData(convertSessionId, fileData);
1187             } catch (IllegalArgumentException e) {
1188                 Log.w(TAG, "Buffer with data to convert is illegal. Convertsession: "
1189                         + convertSessionId, e);
1190                 return false;
1191             } catch (IllegalStateException e) {
1192                 Log.w(TAG, "Could not convert data. Convertsession: " + convertSessionId, e);
1193                 return false;
1194             }
1195 
1196             if (convertedStatus == null ||
1197                     convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK ||
1198                     convertedStatus.convertedData == null) {
1199                 Log.w(TAG, "Error in converting data. Convertsession: " + convertSessionId);
1200                 try {
1201                     DrmConvertedStatus result = drmClient.closeConvertSession(convertSessionId);
1202                     if (result.statusCode != DrmConvertedStatus.STATUS_OK) {
1203                         Log.w(TAG, "Conversion failed with status: " + result.statusCode);
1204                         return false;
1205                     }
1206                 } catch (IllegalStateException e) {
1207                     Log.w(TAG, "Could not close session. Convertsession: " +
1208                            convertSessionId, e);
1209                 }
1210                 return false;
1211             }
1212 
1213             try {
1214                 flFile.write(convertedStatus.convertedData, 0, convertedStatus.convertedData.length);
1215             } catch (IOException e) {
1216                 Log.w(TAG, "Failed to write to output file: " + e);
1217                 return false;
1218             }
1219 
1220             try {
1221                 convertedStatus = drmClient.closeConvertSession(convertSessionId);
1222             } catch (IllegalStateException e) {
1223                 Log.w(TAG, "Could not close convertsession. Convertsession: " +
1224                         convertSessionId, e);
1225                 return false;
1226             }
1227 
1228             if (convertedStatus == null ||
1229                     convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK ||
1230                     convertedStatus.convertedData == null) {
1231                 Log.w(TAG, "Error in closing session. Convertsession: " + convertSessionId);
1232                 return false;
1233             }
1234 
1235             try {
1236                 flFile.seek(convertedStatus.offset);
1237                 flFile.write(convertedStatus.convertedData);
1238             } catch (IOException e) {
1239                 Log.w(TAG, "Could not update file.", e);
1240                 return false;
1241             }
1242 
1243             return true;
1244         } finally {
1245             drmClient.close();
1246         }
1247     }
1248 
1249     /**
1250      * @param decoder new MediaCodec object
1251      * @param ex MediaExtractor after setDataSource and selectTrack
1252      * @param frameMD5Sums reference MD5 checksum for decoded frames
1253      * @return true if decoded frames checksums matches reference checksums
1254      * @throws IOException
1255      */
verifyDecoder( MediaCodec decoder, MediaExtractor ex, List<String> frameMD5Sums)1256     public static boolean verifyDecoder(
1257             MediaCodec decoder, MediaExtractor ex, List<String> frameMD5Sums)
1258             throws IOException {
1259 
1260         int trackIndex = ex.getSampleTrackIndex();
1261         MediaFormat format = ex.getTrackFormat(trackIndex);
1262         decoder.configure(format, null /* surface */, null /* crypto */, 0 /* flags */);
1263         decoder.start();
1264 
1265         boolean sawInputEOS = false;
1266         boolean sawOutputEOS = false;
1267         final long kTimeOutUs = 5000; // 5ms timeout
1268         int decodedFrameCount = 0;
1269         int expectedFrameCount = frameMD5Sums.size();
1270         MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
1271 
1272         while (!sawOutputEOS) {
1273             // handle input
1274             if (!sawInputEOS) {
1275                 int inIdx = decoder.dequeueInputBuffer(kTimeOutUs);
1276                 if (inIdx >= 0) {
1277                     ByteBuffer buffer = decoder.getInputBuffer(inIdx);
1278                     int sampleSize = ex.readSampleData(buffer, 0);
1279                     if (sampleSize < 0) {
1280                         final int flagEOS = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
1281                         decoder.queueInputBuffer(inIdx, 0, 0, 0, flagEOS);
1282                         sawInputEOS = true;
1283                     } else {
1284                         decoder.queueInputBuffer(inIdx, 0, sampleSize, ex.getSampleTime(), 0);
1285                         ex.advance();
1286                     }
1287                 }
1288             }
1289 
1290             // handle output
1291             int outputBufIndex = decoder.dequeueOutputBuffer(info, kTimeOutUs);
1292             if (outputBufIndex >= 0) {
1293                 try {
1294                     if (info.size > 0) {
1295                         // Disregard 0-sized buffers at the end.
1296                         String md5CheckSum = "";
1297                         Image image = decoder.getOutputImage(outputBufIndex);
1298                         md5CheckSum = getImageMD5Checksum(image);
1299 
1300                         if (!md5CheckSum.equals(frameMD5Sums.get(decodedFrameCount))) {
1301                             Log.d(TAG,
1302                                     String.format(
1303                                             "Frame %d md5sum mismatch: %s(actual) vs %s(expected)",
1304                                             decodedFrameCount, md5CheckSum,
1305                                             frameMD5Sums.get(decodedFrameCount)));
1306                             return false;
1307                         }
1308 
1309                         decodedFrameCount++;
1310                     }
1311                 } catch (Exception e) {
1312                     Log.e(TAG, "getOutputImage md5CheckSum failed", e);
1313                     return false;
1314                 } finally {
1315                     decoder.releaseOutputBuffer(outputBufIndex, false /* render */);
1316                 }
1317                 if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
1318                     sawOutputEOS = true;
1319                 }
1320             } else if (outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
1321                 MediaFormat decOutputFormat = decoder.getOutputFormat();
1322                 Log.d(TAG, "output format " + decOutputFormat);
1323             } else if (outputBufIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
1324                 Log.i(TAG, "Skip handling MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED");
1325             } else if (outputBufIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
1326                 continue;
1327             } else {
1328                 Log.w(TAG, "decoder.dequeueOutputBuffer() unrecognized index: " + outputBufIndex);
1329                 return false;
1330             }
1331         }
1332 
1333         if (decodedFrameCount != expectedFrameCount) {
1334             return false;
1335         }
1336 
1337         return true;
1338     }
1339 
getImageMD5Checksum(Image image)1340     public static String getImageMD5Checksum(Image image) throws Exception {
1341         int format = image.getFormat();
1342         if (ImageFormat.YUV_420_888 != format) {
1343             Log.w(TAG, "unsupported image format");
1344             return "";
1345         }
1346 
1347         MessageDigest md = MessageDigest.getInstance("MD5");
1348 
1349         Rect crop = image.getCropRect();
1350         int cropLeft = crop.left;
1351         int cropRight = crop.right;
1352         int cropTop = crop.top;
1353         int cropBottom = crop.bottom;
1354 
1355         int imageWidth = cropRight - cropLeft;
1356         int imageHeight = cropBottom - cropTop;
1357 
1358         Image.Plane[] planes = image.getPlanes();
1359         for (int i = 0; i < planes.length; ++i) {
1360             ByteBuffer buf = planes[i].getBuffer();
1361 
1362             int width, height, rowStride, pixelStride, x, y, top, left;
1363             rowStride = planes[i].getRowStride();
1364             pixelStride = planes[i].getPixelStride();
1365             if (i == 0) {
1366                 width = imageWidth;
1367                 height = imageHeight;
1368                 left = cropLeft;
1369                 top = cropTop;
1370             } else {
1371                 width = imageWidth / 2;
1372                 height = imageHeight /2;
1373                 left = cropLeft / 2;
1374                 top = cropTop / 2;
1375             }
1376             // local contiguous pixel buffer
1377             byte[] bb = new byte[width * height];
1378             if (buf.hasArray()) {
1379                 byte b[] = buf.array();
1380                 int offs = buf.arrayOffset() + left * pixelStride;
1381                 if (pixelStride == 1) {
1382                     for (y = 0; y < height; ++y) {
1383                         System.arraycopy(bb, y * width, b, (top + y) * rowStride + offs, width);
1384                     }
1385                 } else {
1386                     // do it pixel-by-pixel
1387                     for (y = 0; y < height; ++y) {
1388                         int lineOffset = offs + (top + y) * rowStride;
1389                         for (x = 0; x < width; ++x) {
1390                             bb[y * width + x] = b[lineOffset + x * pixelStride];
1391                         }
1392                     }
1393                 }
1394             } else { // almost always ends up here due to direct buffers
1395                 int pos = buf.position();
1396                 if (pixelStride == 1) {
1397                     for (y = 0; y < height; ++y) {
1398                         buf.position(pos + left + (top + y) * rowStride);
1399                         buf.get(bb, y * width, width);
1400                     }
1401                 } else {
1402                     // local line buffer
1403                     byte[] lb = new byte[rowStride];
1404                     // do it pixel-by-pixel
1405                     for (y = 0; y < height; ++y) {
1406                         buf.position(pos + left * pixelStride + (top + y) * rowStride);
1407                         // we're only guaranteed to have pixelStride * (width - 1) + 1 bytes
1408                         buf.get(lb, 0, pixelStride * (width - 1) + 1);
1409                         for (x = 0; x < width; ++x) {
1410                             bb[y * width + x] = lb[x * pixelStride];
1411                         }
1412                     }
1413                 }
1414                 buf.position(pos);
1415             }
1416             md.update(bb, 0, width * height);
1417         }
1418 
1419         return convertByteArrayToHEXString(md.digest());
1420     }
1421 
convertByteArrayToHEXString(byte[] ba)1422     private static String convertByteArrayToHEXString(byte[] ba) throws Exception {
1423         StringBuilder result = new StringBuilder();
1424         for (int i = 0; i < ba.length; i++) {
1425             result.append(Integer.toString((ba[i] & 0xff) + 0x100, 16).substring(1));
1426         }
1427         return result.toString();
1428     }
1429 
1430 
1431     /*
1432      *  ------------------- HELPER METHODS FOR DETECTING DEVICE TYPES -------------------
1433      */
1434 
hasDeviceGotBattery()1435     public static boolean hasDeviceGotBattery() {
1436         final Intent batteryInfo = mContext.registerReceiver(null,
1437                 new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
1438         return batteryInfo.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true);
1439     }
1440 
getScreenSizeInInches()1441     public static double getScreenSizeInInches() {
1442         DisplayMetrics dm = mContext.getResources().getDisplayMetrics();
1443         double widthInInchesSquared = Math.pow(dm.widthPixels/dm.xdpi,2);
1444         double heightInInchesSquared = Math.pow(dm.heightPixels/dm.ydpi,2);
1445         double diagonalInInches = Math.sqrt(widthInInchesSquared + heightInInchesSquared);
1446         return diagonalInInches;
1447     }
1448 
isTv()1449     public static boolean isTv() {
1450         return pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
1451                 pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION);
1452     }
1453 
hasMicrophone()1454     public static boolean hasMicrophone() {
1455         return pm.hasSystemFeature(PackageManager.FEATURE_MICROPHONE);
1456     }
1457 
hasCamera()1458     public static boolean hasCamera() {
1459         return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
1460     }
1461 
isWatch()1462     public static boolean isWatch() {
1463         return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
1464     }
1465 
isAutomotive()1466     public static boolean isAutomotive() {
1467         return pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
1468     }
1469 
isPc()1470     public static boolean isPc() {
1471         return pm.hasSystemFeature(PackageManager.FEATURE_PC);
1472     }
1473 
hasAudioOutput()1474     public static boolean hasAudioOutput() {
1475         return pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT);
1476     }
1477 
isHandheld()1478     public static boolean isHandheld() {
1479         double screenSize = getScreenSizeInInches();
1480         if (screenSize < (FIRST_SDK_IS_AT_LEAST_R ? 3.3 : 2.5)) return false;
1481         if (screenSize > 8.0) return false;
1482         if (!hasDeviceGotBattery()) return false;
1483         // handheld nature is not exposed to package manager, so for now,
1484         // in addition to physical screen size, the following checks are
1485         // also required:
1486         if (!pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) return false;
1487         if (isWatch()) return false;
1488         if (isTv()) return false;
1489         if (isAutomotive()) return false;
1490         if (isPc()) return false;
1491         return true;
1492     }
1493 
isTablet()1494     public static boolean isTablet() {
1495         double screenSize = getScreenSizeInInches();
1496         if (screenSize < 7.0) return false;
1497         if (screenSize > 18.0) return false;
1498         if (!hasDeviceGotBattery()) return false;
1499         // tablet nature is not exposed to package manager, so for now,
1500         // in addition to physical screen size, the following checks are
1501         // also required:
1502         if (!pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) return false;
1503         if (isWatch()) return false;
1504         if (isTv()) return false;
1505         if (isAutomotive()) return false;
1506         if (isPc()) return false;
1507         return true;
1508     }
1509 
1510     /*
1511      *  ------------------- HELPER METHODS FOR DETECTING NON-PRODUCTION DEVICES -------------------
1512      */
1513 
1514     /*
1515      *  Some parts of media CTS verifies device characterization that does not make sense for
1516      *  non-production devices (such as GSI and cuttlefish). We call these devices 'frankenDevices'.
1517      *  We may also limit test duration on these devices.
1518      */
onFrankenDevice()1519     public static boolean onFrankenDevice() throws IOException {
1520         String systemBrand = PropertyUtil.getProperty("ro.product.system.brand");
1521         String systemModel = PropertyUtil.getProperty("ro.product.system.model");
1522         String systemProduct = PropertyUtil.getProperty("ro.product.system.name");
1523 
1524         // not all devices may have system_ext partition, but if they do use that
1525         {
1526             String systemExtProduct = PropertyUtil.getProperty("ro.product.system_ext.name");
1527             if (systemExtProduct != null) {
1528                 systemProduct = systemExtProduct;
1529             }
1530             String systemExtModel = PropertyUtil.getProperty("ro.product.system_ext.model");
1531             if (systemExtModel != null) {
1532                 systemModel = systemExtModel;
1533             }
1534         }
1535 
1536         if (("Android".equals(systemBrand) || "generic".equals(systemBrand) ||
1537                 "mainline".equals(systemBrand)) &&
1538             (systemModel.startsWith("AOSP on ") || systemProduct.startsWith("aosp_") ||
1539                 systemModel.startsWith("GSI on ") || systemProduct.startsWith("gsi_"))) {
1540             return true;
1541         }
1542 
1543         // Return true for cuttlefish instances
1544         if ((systemBrand.equals("Android") || systemBrand.equals("google")) &&
1545                 (systemProduct.startsWith("cf_") || systemProduct.startsWith("aosp_cf_") ||
1546                         systemModel.startsWith("Cuttlefish "))) {
1547             return true;
1548         }
1549         return false;
1550     }
1551 
1552     /**
1553      *  Function to identify if the device is a cuttlefish instance
1554      */
onCuttlefish()1555     public static boolean onCuttlefish() throws IOException {
1556         String device = SystemProperties.get("ro.product.device", "");
1557         String model = SystemProperties.get("ro.product.model", "");
1558         String name = SystemProperties.get("ro.product.name", "");
1559 
1560         // Return true for cuttlefish instances
1561         if (!device.startsWith("vsoc_")) {
1562             return false;
1563         }
1564         if (!model.startsWith("Cuttlefish ")) {
1565             return false;
1566         }
1567         if (name.startsWith("cf_") || name.startsWith("aosp_cf_")) {
1568             return true;
1569         }
1570         return false;
1571     }
1572 
1573     /*
1574      *  -------------------------------------- END --------------------------------------
1575      */
1576 }
1577