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 android.cts.util;
17 
18 import android.content.Context;
19 import android.content.res.AssetFileDescriptor;
20 import android.media.MediaCodec;
21 import android.media.MediaCodecInfo;
22 import android.media.MediaCodecInfo.CodecCapabilities;
23 import android.media.MediaCodecInfo.VideoCapabilities;
24 import android.media.MediaCodecList;
25 import android.media.MediaExtractor;
26 import android.media.MediaFormat;
27 import android.net.Uri;
28 import android.util.Range;
29 
30 import com.android.cts.util.ReportLog;
31 import com.android.cts.util.ResultType;
32 import com.android.cts.util.ResultUnit;
33 
34 import java.lang.reflect.Method;
35 import static java.lang.reflect.Modifier.isPublic;
36 import static java.lang.reflect.Modifier.isStatic;
37 import java.util.Arrays;
38 import java.util.Map;
39 import android.util.Log;
40 
41 import java.io.IOException;
42 
43 public class MediaUtils {
44     private static final String TAG = "MediaUtils";
45 
46     private static final int ALL_AV_TRACKS = -1;
47 
48     private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
49 
50     /**
51      * Returns the test name (heuristically).
52      *
53      * Since it uses heuristics, this method has only been verified for media
54      * tests. This centralizes the way to signal errors during a test.
55      */
getTestName()56     public static String getTestName() {
57         int bestScore = -1;
58         String testName = "test???";
59         Map<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces();
60         for (Map.Entry<Thread, StackTraceElement[]> entry : traces.entrySet()) {
61             StackTraceElement[] stack = entry.getValue();
62             for (int index = 0; index < stack.length; ++index) {
63                 // method name must start with "test"
64                 String methodName = stack[index].getMethodName();
65                 if (!methodName.startsWith("test")) {
66                     continue;
67                 }
68 
69                 int score = 0;
70                 // see if there is a public non-static void method that takes no argument
71                 Class<?> clazz;
72                 try {
73                     clazz = Class.forName(stack[index].getClassName());
74                     ++score;
75                     for (final Method method : clazz.getDeclaredMethods()) {
76                         if (method.getName().equals(methodName)
77                                 && isPublic(method.getModifiers())
78                                 && !isStatic(method.getModifiers())
79                                 && method.getParameterTypes().length == 0
80                                 && method.getReturnType().equals(Void.TYPE)) {
81                             ++score;
82                             break;
83                         }
84                     }
85                     if (score == 1) {
86                         // if we could read the class, but method is not public void, it is
87                         // not a candidate
88                         continue;
89                     }
90                 } catch (ClassNotFoundException e) {
91                 }
92 
93                 // even if we cannot verify the method signature, there are signals in the stack
94 
95                 // usually test method is invoked by reflection
96                 int depth = 1;
97                 while (index + depth < stack.length
98                         && stack[index + depth].getMethodName().equals("invoke")
99                         && stack[index + depth].getClassName().equals(
100                                 "java.lang.reflect.Method")) {
101                     ++depth;
102                 }
103                 if (depth > 1) {
104                     ++score;
105                     // and usually test method is run by runMethod method in android.test package
106                     if (index + depth < stack.length) {
107                         if (stack[index + depth].getClassName().startsWith("android.test.")) {
108                             ++score;
109                         }
110                         if (stack[index + depth].getMethodName().equals("runMethod")) {
111                             ++score;
112                         }
113                     }
114                 }
115 
116                 if (score > bestScore) {
117                     bestScore = score;
118                     testName = methodName;
119                 }
120             }
121         }
122         return testName;
123     }
124 
125     /**
126      * Finds test name (heuristically) and prints out standard skip message.
127      *
128      * Since it uses heuristics, this method has only been verified for media
129      * tests. This centralizes the way to signal a skipped test.
130      */
skipTest(String tag, String reason)131     public static void skipTest(String tag, String reason) {
132         Log.i(tag, "SKIPPING " + getTestName() + "(): " + reason);
133     }
134 
135     /**
136      * Finds test name (heuristically) and prints out standard skip message.
137      *
138      * Since it uses heuristics, this method has only been verified for media
139      * tests.  This centralizes the way to signal a skipped test.
140      */
skipTest(String reason)141     public static void skipTest(String reason) {
142         skipTest(TAG, reason);
143     }
144 
check(boolean result, String message)145     public static boolean check(boolean result, String message) {
146         if (!result) {
147             skipTest(message);
148         }
149         return result;
150     }
151 
getDecoder(MediaFormat format)152     public static MediaCodec getDecoder(MediaFormat format) {
153         String decoder = sMCL.findDecoderForFormat(format);
154         if (decoder != null) {
155             try {
156                 return MediaCodec.createByCodecName(decoder);
157             } catch (IOException e) {
158             }
159         }
160         return null;
161     }
162 
canDecode(MediaFormat format)163     public static boolean canDecode(MediaFormat format) {
164         if (sMCL.findDecoderForFormat(format) == null) {
165             Log.i(TAG, "no decoder for " + format);
166             return false;
167         }
168         return true;
169     }
170 
supports(String codecName, String mime, int w, int h)171     public static boolean supports(String codecName, String mime, int w, int h) {
172         MediaCodec codec;
173         try {
174             codec = MediaCodec.createByCodecName(codecName);
175         } catch (IOException e) {
176             return false;
177         }
178 
179         CodecCapabilities cap = null;
180         try {
181             cap = codec.getCodecInfo().getCapabilitiesForType(mime);
182         } catch (IllegalArgumentException e) {
183             Log.w(TAG, "not supported mime: " + mime);
184             codec.release();
185             return false;
186         }
187 
188         VideoCapabilities vidCap = cap.getVideoCapabilities();
189         if (vidCap == null) {
190             Log.w(TAG, "not a video codec: " + codecName);
191             codec.release();
192             return false;
193         }
194         try {
195             Range<Double> fps = vidCap.getSupportedFrameRatesFor(w, h);
196         } catch (IllegalArgumentException e) {
197             Log.w(TAG, "unsupported size " + w + "x" + h);
198             codec.release();
199             return false;
200         }
201         codec.release();
202         return true;
203     }
204 
hasCodecForTrack(MediaExtractor ex, int track)205     public static boolean hasCodecForTrack(MediaExtractor ex, int track) {
206         int count = ex.getTrackCount();
207         if (track < 0 || track >= count) {
208             throw new IndexOutOfBoundsException(track + " not in [0.." + (count - 1) + "]");
209         }
210         return canDecode(ex.getTrackFormat(track));
211     }
212 
213     /**
214      * return true iff all audio and video tracks are supported
215      */
hasCodecsForMedia(MediaExtractor ex)216     public static boolean hasCodecsForMedia(MediaExtractor ex) {
217         for (int i = 0; i < ex.getTrackCount(); ++i) {
218             MediaFormat format = ex.getTrackFormat(i);
219             // only check for audio and video codecs
220             String mime = format.getString(MediaFormat.KEY_MIME).toLowerCase();
221             if (!mime.startsWith("audio/") && !mime.startsWith("video/")) {
222                 continue;
223             }
224             if (!canDecode(format)) {
225                 return false;
226             }
227         }
228         return true;
229     }
230 
231     /**
232      * return true iff any track starting with mimePrefix is supported
233      */
hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix)234     public static boolean hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix) {
235         mimePrefix = mimePrefix.toLowerCase();
236         for (int i = 0; i < ex.getTrackCount(); ++i) {
237             MediaFormat format = ex.getTrackFormat(i);
238             String mime = format.getString(MediaFormat.KEY_MIME);
239             if (mime.toLowerCase().startsWith(mimePrefix)) {
240                 if (canDecode(format)) {
241                     return true;
242                 }
243                 Log.i(TAG, "no decoder for " + format);
244             }
245         }
246         return false;
247     }
248 
hasCodecsForResourceCombo( Context context, int resourceId, int track, String mimePrefix)249     private static boolean hasCodecsForResourceCombo(
250             Context context, int resourceId, int track, String mimePrefix) {
251         try {
252             AssetFileDescriptor afd = null;
253             MediaExtractor ex = null;
254             try {
255                 afd = context.getResources().openRawResourceFd(resourceId);
256                 ex = new MediaExtractor();
257                 ex.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
258                 if (mimePrefix != null) {
259                     return hasCodecForMediaAndDomain(ex, mimePrefix);
260                 } else if (track == ALL_AV_TRACKS) {
261                     return hasCodecsForMedia(ex);
262                 } else {
263                     return hasCodecForTrack(ex, track);
264                 }
265             } finally {
266                 if (ex != null) {
267                     ex.release();
268                 }
269                 if (afd != null) {
270                     afd.close();
271                 }
272             }
273         } catch (IOException e) {
274             Log.i(TAG, "could not open resource");
275         }
276         return false;
277     }
278 
279     /**
280      * return true iff all audio and video tracks are supported
281      */
hasCodecsForResource(Context context, int resourceId)282     public static boolean hasCodecsForResource(Context context, int resourceId) {
283         return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, null /* mimePrefix */);
284     }
285 
checkCodecsForResource(Context context, int resourceId)286     public static boolean checkCodecsForResource(Context context, int resourceId) {
287         return check(hasCodecsForResource(context, resourceId), "no decoder found");
288     }
289 
290     /**
291      * return true iff track is supported.
292      */
hasCodecForResource(Context context, int resourceId, int track)293     public static boolean hasCodecForResource(Context context, int resourceId, int track) {
294         return hasCodecsForResourceCombo(context, resourceId, track, null /* mimePrefix */);
295     }
296 
checkCodecForResource(Context context, int resourceId, int track)297     public static boolean checkCodecForResource(Context context, int resourceId, int track) {
298         return check(hasCodecForResource(context, resourceId, track), "no decoder found");
299     }
300 
301     /**
302      * return true iff any track starting with mimePrefix is supported
303      */
hasCodecForResourceAndDomain( Context context, int resourceId, String mimePrefix)304     public static boolean hasCodecForResourceAndDomain(
305             Context context, int resourceId, String mimePrefix) {
306         return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, mimePrefix);
307     }
308 
309     /**
310      * return true iff all audio and video tracks are supported
311      */
hasCodecsForPath(Context context, String path)312     public static boolean hasCodecsForPath(Context context, String path) {
313         MediaExtractor ex = null;
314         try {
315             ex = new MediaExtractor();
316             Uri uri = Uri.parse(path);
317             String scheme = uri.getScheme();
318             if (scheme == null) { // file
319                 ex.setDataSource(path);
320             } else if (scheme.equalsIgnoreCase("file")) {
321                 ex.setDataSource(uri.getPath());
322             } else {
323                 ex.setDataSource(context, uri, null);
324             }
325             return hasCodecsForMedia(ex);
326         } catch (IOException e) {
327             Log.i(TAG, "could not open path " + path);
328         } finally {
329             if (ex != null) {
330                 ex.release();
331             }
332         }
333         return false;
334     }
335 
checkCodecsForPath(Context context, String path)336     public static boolean checkCodecsForPath(Context context, String path) {
337         return check(hasCodecsForPath(context, path), "no decoder found");
338     }
339 
hasCodecForMime(boolean encoder, String mime)340     private static boolean hasCodecForMime(boolean encoder, String mime) {
341         for (MediaCodecInfo info : sMCL.getCodecInfos()) {
342             if (encoder != info.isEncoder()) {
343                 continue;
344             }
345 
346             for (String type : info.getSupportedTypes()) {
347                 if (type.equalsIgnoreCase(mime)) {
348                     Log.i(TAG, "found codec " + info.getName() + " for mime " + mime);
349                     return true;
350                 }
351             }
352         }
353         return false;
354     }
355 
hasCodecForMimes(boolean encoder, String[] mimes)356     private static boolean hasCodecForMimes(boolean encoder, String[] mimes) {
357         for (String mime : mimes) {
358             if (!hasCodecForMime(encoder, mime)) {
359                 Log.i(TAG, "no " + (encoder ? "encoder" : "decoder") + " for mime " + mime);
360                 return false;
361             }
362         }
363         return true;
364     }
365 
366 
hasEncoder(String... mimes)367     public static boolean hasEncoder(String... mimes) {
368         return hasCodecForMimes(true /* encoder */, mimes);
369     }
370 
hasDecoder(String... mimes)371     public static boolean hasDecoder(String... mimes) {
372         return hasCodecForMimes(false /* encoder */, mimes);
373     }
374 
checkDecoder(String... mimes)375     public static boolean checkDecoder(String... mimes) {
376         return check(hasCodecForMimes(false /* encoder */, mimes), "no decoder found");
377     }
378 
checkEncoder(String... mimes)379     public static boolean checkEncoder(String... mimes) {
380         return check(hasCodecForMimes(true /* encoder */, mimes), "no encoder found");
381     }
382 
canDecodeVideo(String mime, int width, int height, float rate)383     public static boolean canDecodeVideo(String mime, int width, int height, float rate) {
384         MediaFormat format = MediaFormat.createVideoFormat(mime, width, height);
385         format.setFloat(MediaFormat.KEY_FRAME_RATE, rate);
386         return canDecode(format);
387     }
388 
checkDecoderForFormat(MediaFormat format)389     public static boolean checkDecoderForFormat(MediaFormat format) {
390         return check(canDecode(format), "no decoder for " + format);
391     }
392 
createMediaExtractorForMimeType( Context context, int resourceId, String mimeTypePrefix)393     public static MediaExtractor createMediaExtractorForMimeType(
394             Context context, int resourceId, String mimeTypePrefix)
395             throws IOException {
396         MediaExtractor extractor = new MediaExtractor();
397         AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId);
398         try {
399             extractor.setDataSource(
400                     afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
401         } finally {
402             afd.close();
403         }
404         int trackIndex;
405         for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) {
406             MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex);
407             if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) {
408                 extractor.selectTrack(trackIndex);
409                 break;
410             }
411         }
412         if (trackIndex == extractor.getTrackCount()) {
413             extractor.release();
414             throw new IllegalStateException("couldn't get a track for " + mimeTypePrefix);
415         }
416 
417         return extractor;
418     }
419 
420     /**
421      * return the average value of the passed array.
422      */
getAverage(double[] data)423     public static double getAverage(double[] data) {
424         int num = data.length;
425         if (num == 0) {
426             return 0;
427         }
428 
429         double sum = data[0];
430         for (int i = 1; i < num; i++) {
431             sum += data[i];
432         }
433         return sum / num;
434     }
435 
436     /**
437      * return the standard deviation value of the passed array
438      */
getStdev(double[] data)439     public static double getStdev(double[] data) {
440         double average = getAverage(data);
441         int num = data.length;
442         if (num == 0) {
443             return 0;
444         }
445         double variance = 0;
446         for (int i = 0; i < num; ++i) {
447             variance += (data[i] - average) * (data[i] - average);
448         }
449         variance /= num;
450         return Math.sqrt(variance);
451     }
452 
calculateMovingAverage(double[] array, int n)453     public static double[] calculateMovingAverage(double[] array, int n) {
454         int num = array.length;
455         if (num < n) {
456             return null;
457         }
458         int avgsNum = num - n + 1;
459         double[] avgs = new double[avgsNum];
460         double sum = array[0];
461         for (int i = 1; i < n; ++i) {
462             sum += array[i];
463         }
464         avgs[0] = sum / n;
465 
466         for (int i = n; i < num; ++i) {
467             sum = sum - array[i - n] + array[i];
468             avgs[i - n + 1] = sum / n;
469         }
470         return avgs;
471     }
472 
logResults(ReportLog log, String prefix, double min, double max, double avg, double stdev)473     public static String logResults(ReportLog log, String prefix,
474             double min, double max, double avg, double stdev) {
475         String msg = prefix;
476         msg += " min=" + Math.round(min / 1000) + " max=" + Math.round(max / 1000) +
477                 " avg=" + Math.round(avg / 1000) + " stdev=" + Math.round(stdev / 1000);
478         log.printValue(msg, 1000000000 / min, ResultType.HIGHER_BETTER, ResultUnit.FPS);
479         return msg;
480     }
481 
getVideoCapabilities(String codecName, String mime)482     public static VideoCapabilities getVideoCapabilities(String codecName, String mime) {
483         for (MediaCodecInfo info : sMCL.getCodecInfos()) {
484             if (!info.getName().equalsIgnoreCase(codecName)) {
485                 continue;
486             }
487             CodecCapabilities caps;
488             try {
489                 caps = info.getCapabilitiesForType(mime);
490             } catch (IllegalArgumentException e) {
491                 // mime is not supported
492                 continue;
493             }
494             return caps.getVideoCapabilities();
495         }
496         return null;
497     }
498 
getAchievableFrameRatesFor( String codecName, String mimeType, int width, int height)499     public static Range<Double> getAchievableFrameRatesFor(
500             String codecName, String mimeType, int width, int height) {
501         VideoCapabilities cap = getVideoCapabilities(codecName, mimeType);
502         if (cap == null) {
503             return null;
504         }
505         return cap.getAchievableFrameRatesFor(width, height);
506     }
507 
508     private static final double FRAMERATE_TOLERANCE = Math.sqrt(12.1);
verifyResults(String name, String mime, int w, int h, double measured)509     public static boolean verifyResults(String name, String mime, int w, int h, double measured) {
510         Range<Double> reported = getAchievableFrameRatesFor(name, mime, w, h);
511         if (reported == null) {
512             Log.d(TAG, "Failed to getAchievableFrameRatesFor " +
513                     name + " " + mime + " " + w + "x" + h);
514             return false;
515         }
516         double lowerBoundary1 = reported.getLower() / FRAMERATE_TOLERANCE;
517         double upperBoundary1 = reported.getUpper() * FRAMERATE_TOLERANCE;
518         double lowerBoundary2 = reported.getUpper() / Math.pow(FRAMERATE_TOLERANCE, 2);
519         double upperBoundary2 = reported.getLower() * Math.pow(FRAMERATE_TOLERANCE, 2);
520         Log.d(TAG, name + " " + mime + " " + w + "x" + h + " " +
521                 "lowerBoundary1 " + lowerBoundary1 + " upperBoundary1 " + upperBoundary1 +
522                 " lowerBoundary2 " + lowerBoundary2 + " upperBoundary2 " + upperBoundary2 +
523                 " measured " + measured);
524         return (measured >= lowerBoundary1 && measured <= upperBoundary1 &&
525                 measured >= lowerBoundary2 && measured <= upperBoundary2);
526     }
527 
getErrorMessage( Range<Double> reportedRange, double[] measuredFps, String[] rawData)528     public static String getErrorMessage(
529             Range<Double> reportedRange, double[] measuredFps, String[] rawData) {
530         String msg = "";
531         if (reportedRange == null) {
532             msg += "Failed to get achievable frame rate.\n";
533         } else {
534             msg += "Expected achievable frame rate range: " + reportedRange + ".\n";
535         }
536         msg += "Measured frame rate: " + Arrays.toString(measuredFps) + ".\n";
537         msg += "Raw data: " + Arrays.toString(rawData) + ".\n";
538         return msg;
539     }
540 }
541