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.Log;
29 import android.util.Range;
30 
31 import com.android.compatibility.common.util.DeviceReportLog;
32 import com.android.compatibility.common.util.ResultType;
33 import com.android.compatibility.common.util.ResultUnit;
34 
35 import java.lang.reflect.Method;
36 import static java.lang.reflect.Modifier.isPublic;
37 import static java.lang.reflect.Modifier.isStatic;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.Map;
41 
42 import static junit.framework.Assert.assertTrue;
43 
44 import java.io.IOException;
45 
46 public class MediaUtils {
47     private static final String TAG = "MediaUtils";
48 
49     /*
50      *  ----------------------- HELPER METHODS FOR SKIPPING TESTS -----------------------
51      */
52     private static final int ALL_AV_TRACKS = -1;
53 
54     private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
55 
56     /**
57      * Returns the test name (heuristically).
58      *
59      * Since it uses heuristics, this method has only been verified for media
60      * tests. This centralizes the way to signal errors during a test.
61      */
getTestName()62     public static String getTestName() {
63         return getTestName(false /* withClass */);
64     }
65 
66     /**
67      * Returns the test name with the full class (heuristically).
68      *
69      * Since it uses heuristics, this method has only been verified for media
70      * tests. This centralizes the way to signal errors during a test.
71      */
getTestNameWithClass()72     public static String getTestNameWithClass() {
73         return getTestName(true /* withClass */);
74     }
75 
getTestName(boolean withClass)76     private static String getTestName(boolean withClass) {
77         int bestScore = -1;
78         String testName = "test???";
79         Map<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces();
80         for (Map.Entry<Thread, StackTraceElement[]> entry : traces.entrySet()) {
81             StackTraceElement[] stack = entry.getValue();
82             for (int index = 0; index < stack.length; ++index) {
83                 // method name must start with "test"
84                 String methodName = stack[index].getMethodName();
85                 if (!methodName.startsWith("test")) {
86                     continue;
87                 }
88 
89                 int score = 0;
90                 // see if there is a public non-static void method that takes no argument
91                 Class<?> clazz;
92                 try {
93                     clazz = Class.forName(stack[index].getClassName());
94                     ++score;
95                     for (final Method method : clazz.getDeclaredMethods()) {
96                         if (method.getName().equals(methodName)
97                                 && isPublic(method.getModifiers())
98                                 && !isStatic(method.getModifiers())
99                                 && method.getParameterTypes().length == 0
100                                 && method.getReturnType().equals(Void.TYPE)) {
101                             ++score;
102                             break;
103                         }
104                     }
105                     if (score == 1) {
106                         // if we could read the class, but method is not public void, it is
107                         // not a candidate
108                         continue;
109                     }
110                 } catch (ClassNotFoundException e) {
111                 }
112 
113                 // even if we cannot verify the method signature, there are signals in the stack
114 
115                 // usually test method is invoked by reflection
116                 int depth = 1;
117                 while (index + depth < stack.length
118                         && stack[index + depth].getMethodName().equals("invoke")
119                         && stack[index + depth].getClassName().equals(
120                                 "java.lang.reflect.Method")) {
121                     ++depth;
122                 }
123                 if (depth > 1) {
124                     ++score;
125                     // and usually test method is run by runMethod method in android.test package
126                     if (index + depth < stack.length) {
127                         if (stack[index + depth].getClassName().startsWith("android.test.")) {
128                             ++score;
129                         }
130                         if (stack[index + depth].getMethodName().equals("runMethod")) {
131                             ++score;
132                         }
133                     }
134                 }
135 
136                 if (score > bestScore) {
137                     bestScore = score;
138                     testName = methodName;
139                     if (withClass) {
140                         testName = stack[index].getClassName() + "." + testName;
141                     }
142                 }
143             }
144         }
145         return testName;
146     }
147 
148     /**
149      * Finds test name (heuristically) and prints out standard skip message.
150      *
151      * Since it uses heuristics, this method has only been verified for media
152      * tests. This centralizes the way to signal a skipped test.
153      */
skipTest(String tag, String reason)154     public static void skipTest(String tag, String reason) {
155         Log.i(tag, "SKIPPING " + getTestName() + "(): " + reason);
156         DeviceReportLog log = new DeviceReportLog("CtsMediaSkippedTests", "test_skipped");
157         log.addValue("reason", reason, ResultType.NEUTRAL, ResultUnit.NONE);
158         log.addValue(
159                 "test", getTestNameWithClass(), ResultType.NEUTRAL, ResultUnit.NONE);
160         // TODO: replace with submit() when it is added to DeviceReportLog
161         try {
162             log.submit(null);
163         } catch (NullPointerException e) { }
164     }
165 
166     /**
167      * Finds test name (heuristically) and prints out standard skip message.
168      *
169      * Since it uses heuristics, this method has only been verified for media
170      * tests.  This centralizes the way to signal a skipped test.
171      */
skipTest(String reason)172     public static void skipTest(String reason) {
173         skipTest(TAG, reason);
174     }
175 
check(boolean result, String message)176     public static boolean check(boolean result, String message) {
177         if (!result) {
178             skipTest(message);
179         }
180         return result;
181     }
182 
183     /*
184      *  ------------------- HELPER METHODS FOR CHECKING CODEC SUPPORT -------------------
185      */
186 
187     // returns the list of codecs that support any one of the formats
getCodecNames( boolean isEncoder, Boolean isGoog, MediaFormat... formats)188     private static String[] getCodecNames(
189             boolean isEncoder, Boolean isGoog, MediaFormat... formats) {
190         MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
191         ArrayList<String> result = new ArrayList<>();
192         for (MediaCodecInfo info : mcl.getCodecInfos()) {
193             if (info.isEncoder() != isEncoder) {
194                 continue;
195             }
196             if (isGoog != null
197                     && info.getName().toLowerCase().startsWith("omx.google.") != isGoog) {
198                 continue;
199             }
200 
201             for (MediaFormat format : formats) {
202                 String mime = format.getString(MediaFormat.KEY_MIME);
203 
204                 CodecCapabilities caps = null;
205                 try {
206                     caps = info.getCapabilitiesForType(mime);
207                 } catch (IllegalArgumentException e) {  // mime is not supported
208                     continue;
209                 }
210                 if (caps.isFormatSupported(format)) {
211                     result.add(info.getName());
212                     break;
213                 }
214             }
215         }
216         return result.toArray(new String[result.size()]);
217     }
218 
219     /* Use isGoog = null to query all decoders */
getDecoderNames( Boolean isGoog, MediaFormat... formats)220     public static String[] getDecoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) {
221         return getCodecNames(false /* isEncoder */, isGoog, formats);
222     }
223 
getDecoderNames(MediaFormat... formats)224     public static String[] getDecoderNames(MediaFormat... formats) {
225         return getCodecNames(false /* isEncoder */, null /* isGoog */, formats);
226     }
227 
228     /* Use isGoog = null to query all decoders */
getEncoderNames( Boolean isGoog, MediaFormat... formats)229     public static String[] getEncoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) {
230         return getCodecNames(true /* isEncoder */, isGoog, formats);
231     }
232 
getEncoderNames(MediaFormat... formats)233     public static String[] getEncoderNames(MediaFormat... formats) {
234         return getCodecNames(true /* isEncoder */, null /* isGoog */, formats);
235     }
236 
verifyNumCodecs( int count, boolean isEncoder, Boolean isGoog, MediaFormat... formats)237     public static void verifyNumCodecs(
238             int count, boolean isEncoder, Boolean isGoog, MediaFormat... formats) {
239         String desc = (isEncoder ? "encoders" : "decoders") + " for "
240                 + (formats.length == 1 ? formats[0].toString() : Arrays.toString(formats));
241         if (isGoog != null) {
242             desc = (isGoog ? "Google " : "non-Google ") + desc;
243         }
244 
245         String[] codecs = getCodecNames(isEncoder, isGoog, formats);
246         assertTrue("test can only verify " + count + " " + desc + "; found " + codecs.length + ": "
247                 + Arrays.toString(codecs), codecs.length <= count);
248     }
249 
getDecoder(MediaFormat format)250     public static MediaCodec getDecoder(MediaFormat format) {
251         String decoder = sMCL.findDecoderForFormat(format);
252         if (decoder != null) {
253             try {
254                 return MediaCodec.createByCodecName(decoder);
255             } catch (IOException e) {
256             }
257         }
258         return null;
259     }
260 
canEncode(MediaFormat format)261     public static boolean canEncode(MediaFormat format) {
262         if (sMCL.findEncoderForFormat(format) == null) {
263             Log.i(TAG, "no encoder for " + format);
264             return false;
265         }
266         return true;
267     }
268 
canDecode(MediaFormat format)269     public static boolean canDecode(MediaFormat format) {
270         if (sMCL.findDecoderForFormat(format) == null) {
271             Log.i(TAG, "no decoder for " + format);
272             return false;
273         }
274         return true;
275     }
276 
supports(String codecName, String mime, int w, int h)277     public static boolean supports(String codecName, String mime, int w, int h) {
278         // While this could be simply written as such, give more graceful feedback.
279         // MediaFormat format = MediaFormat.createVideoFormat(mime, w, h);
280         // return supports(codecName, format);
281 
282         VideoCapabilities vidCap = getVideoCapabilities(codecName, mime);
283         if (vidCap == null) {
284             return false;
285         } else if (vidCap.isSizeSupported(w, h)) {
286             return true;
287         }
288 
289         Log.w(TAG, "unsupported size " + w + "x" + h);
290         return false;
291     }
292 
supports(String codecName, MediaFormat format)293     public static boolean supports(String codecName, MediaFormat format) {
294         MediaCodec codec;
295         try {
296             codec = MediaCodec.createByCodecName(codecName);
297         } catch (IOException e) {
298             Log.w(TAG, "codec not found: " + codecName);
299             return false;
300         }
301 
302         String mime = format.getString(MediaFormat.KEY_MIME);
303         CodecCapabilities cap = null;
304         try {
305             cap = codec.getCodecInfo().getCapabilitiesForType(mime);
306         } catch (IllegalArgumentException e) {
307             Log.w(TAG, "not supported mime: " + mime);
308             codec.release();
309             return false;
310         }
311 
312         return cap.isFormatSupported(format);
313     }
314 
hasCodecForTrack(MediaExtractor ex, int track)315     public static boolean hasCodecForTrack(MediaExtractor ex, int track) {
316         int count = ex.getTrackCount();
317         if (track < 0 || track >= count) {
318             throw new IndexOutOfBoundsException(track + " not in [0.." + (count - 1) + "]");
319         }
320         return canDecode(ex.getTrackFormat(track));
321     }
322 
323     /**
324      * return true iff all audio and video tracks are supported
325      */
hasCodecsForMedia(MediaExtractor ex)326     public static boolean hasCodecsForMedia(MediaExtractor ex) {
327         for (int i = 0; i < ex.getTrackCount(); ++i) {
328             MediaFormat format = ex.getTrackFormat(i);
329             // only check for audio and video codecs
330             String mime = format.getString(MediaFormat.KEY_MIME).toLowerCase();
331             if (!mime.startsWith("audio/") && !mime.startsWith("video/")) {
332                 continue;
333             }
334             if (!canDecode(format)) {
335                 return false;
336             }
337         }
338         return true;
339     }
340 
341     /**
342      * return true iff any track starting with mimePrefix is supported
343      */
hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix)344     public static boolean hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix) {
345         mimePrefix = mimePrefix.toLowerCase();
346         for (int i = 0; i < ex.getTrackCount(); ++i) {
347             MediaFormat format = ex.getTrackFormat(i);
348             String mime = format.getString(MediaFormat.KEY_MIME);
349             if (mime.toLowerCase().startsWith(mimePrefix)) {
350                 if (canDecode(format)) {
351                     return true;
352                 }
353                 Log.i(TAG, "no decoder for " + format);
354             }
355         }
356         return false;
357     }
358 
hasCodecsForResourceCombo( Context context, int resourceId, int track, String mimePrefix)359     private static boolean hasCodecsForResourceCombo(
360             Context context, int resourceId, int track, String mimePrefix) {
361         try {
362             AssetFileDescriptor afd = null;
363             MediaExtractor ex = null;
364             try {
365                 afd = context.getResources().openRawResourceFd(resourceId);
366                 ex = new MediaExtractor();
367                 ex.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
368                 if (mimePrefix != null) {
369                     return hasCodecForMediaAndDomain(ex, mimePrefix);
370                 } else if (track == ALL_AV_TRACKS) {
371                     return hasCodecsForMedia(ex);
372                 } else {
373                     return hasCodecForTrack(ex, track);
374                 }
375             } finally {
376                 if (ex != null) {
377                     ex.release();
378                 }
379                 if (afd != null) {
380                     afd.close();
381                 }
382             }
383         } catch (IOException e) {
384             Log.i(TAG, "could not open resource");
385         }
386         return false;
387     }
388 
389     /**
390      * return true iff all audio and video tracks are supported
391      */
hasCodecsForResource(Context context, int resourceId)392     public static boolean hasCodecsForResource(Context context, int resourceId) {
393         return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, null /* mimePrefix */);
394     }
395 
checkCodecsForResource(Context context, int resourceId)396     public static boolean checkCodecsForResource(Context context, int resourceId) {
397         return check(hasCodecsForResource(context, resourceId), "no decoder found");
398     }
399 
400     /**
401      * return true iff track is supported.
402      */
hasCodecForResource(Context context, int resourceId, int track)403     public static boolean hasCodecForResource(Context context, int resourceId, int track) {
404         return hasCodecsForResourceCombo(context, resourceId, track, null /* mimePrefix */);
405     }
406 
checkCodecForResource(Context context, int resourceId, int track)407     public static boolean checkCodecForResource(Context context, int resourceId, int track) {
408         return check(hasCodecForResource(context, resourceId, track), "no decoder found");
409     }
410 
411     /**
412      * return true iff any track starting with mimePrefix is supported
413      */
hasCodecForResourceAndDomain( Context context, int resourceId, String mimePrefix)414     public static boolean hasCodecForResourceAndDomain(
415             Context context, int resourceId, String mimePrefix) {
416         return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, mimePrefix);
417     }
418 
419     /**
420      * return true iff all audio and video tracks are supported
421      */
hasCodecsForPath(Context context, String path)422     public static boolean hasCodecsForPath(Context context, String path) {
423         MediaExtractor ex = null;
424         try {
425             ex = new MediaExtractor();
426             Uri uri = Uri.parse(path);
427             String scheme = uri.getScheme();
428             if (scheme == null) { // file
429                 ex.setDataSource(path);
430             } else if (scheme.equalsIgnoreCase("file")) {
431                 ex.setDataSource(uri.getPath());
432             } else {
433                 ex.setDataSource(context, uri, null);
434             }
435             return hasCodecsForMedia(ex);
436         } catch (IOException e) {
437             Log.i(TAG, "could not open path " + path);
438         } finally {
439             if (ex != null) {
440                 ex.release();
441             }
442         }
443         return true;
444     }
445 
checkCodecsForPath(Context context, String path)446     public static boolean checkCodecsForPath(Context context, String path) {
447         return check(hasCodecsForPath(context, path), "no decoder found");
448     }
449 
hasCodecForDomain(boolean encoder, String domain)450     public static boolean hasCodecForDomain(boolean encoder, String domain) {
451         for (MediaCodecInfo info : sMCL.getCodecInfos()) {
452             if (encoder != info.isEncoder()) {
453                 continue;
454             }
455 
456             for (String type : info.getSupportedTypes()) {
457                 if (type.toLowerCase().startsWith(domain.toLowerCase() + "/")) {
458                     Log.i(TAG, "found codec " + info.getName() + " for mime " + type);
459                     return true;
460                 }
461             }
462         }
463         return false;
464     }
465 
checkCodecForDomain(boolean encoder, String domain)466     public static boolean checkCodecForDomain(boolean encoder, String domain) {
467         return check(hasCodecForDomain(encoder, domain),
468                 "no " + domain + (encoder ? " encoder" : " decoder") + " found");
469     }
470 
hasCodecForMime(boolean encoder, String mime)471     private static boolean hasCodecForMime(boolean encoder, String mime) {
472         for (MediaCodecInfo info : sMCL.getCodecInfos()) {
473             if (encoder != info.isEncoder()) {
474                 continue;
475             }
476 
477             for (String type : info.getSupportedTypes()) {
478                 if (type.equalsIgnoreCase(mime)) {
479                     Log.i(TAG, "found codec " + info.getName() + " for mime " + mime);
480                     return true;
481                 }
482             }
483         }
484         return false;
485     }
486 
hasCodecForMimes(boolean encoder, String[] mimes)487     private static boolean hasCodecForMimes(boolean encoder, String[] mimes) {
488         for (String mime : mimes) {
489             if (!hasCodecForMime(encoder, mime)) {
490                 Log.i(TAG, "no " + (encoder ? "encoder" : "decoder") + " for mime " + mime);
491                 return false;
492             }
493         }
494         return true;
495     }
496 
497 
hasEncoder(String... mimes)498     public static boolean hasEncoder(String... mimes) {
499         return hasCodecForMimes(true /* encoder */, mimes);
500     }
501 
hasDecoder(String... mimes)502     public static boolean hasDecoder(String... mimes) {
503         return hasCodecForMimes(false /* encoder */, mimes);
504     }
505 
checkDecoder(String... mimes)506     public static boolean checkDecoder(String... mimes) {
507         return check(hasCodecForMimes(false /* encoder */, mimes), "no decoder found");
508     }
509 
checkEncoder(String... mimes)510     public static boolean checkEncoder(String... mimes) {
511         return check(hasCodecForMimes(true /* encoder */, mimes), "no encoder found");
512     }
513 
canDecodeVideo(String mime, int width, int height, float rate)514     public static boolean canDecodeVideo(String mime, int width, int height, float rate) {
515         MediaFormat format = MediaFormat.createVideoFormat(mime, width, height);
516         format.setFloat(MediaFormat.KEY_FRAME_RATE, rate);
517         return canDecode(format);
518     }
519 
canDecodeVideo( String mime, int width, int height, float rate, Integer profile, Integer level, Integer bitrate)520     public static boolean canDecodeVideo(
521             String mime, int width, int height, float rate,
522             Integer profile, Integer level, Integer bitrate) {
523         MediaFormat format = MediaFormat.createVideoFormat(mime, width, height);
524         format.setFloat(MediaFormat.KEY_FRAME_RATE, rate);
525         if (profile != null) {
526             format.setInteger(MediaFormat.KEY_PROFILE, profile);
527             if (level != null) {
528                 format.setInteger(MediaFormat.KEY_LEVEL, level);
529             }
530         }
531         if (bitrate != null) {
532             format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
533         }
534         return canDecode(format);
535     }
536 
checkEncoderForFormat(MediaFormat format)537     public static boolean checkEncoderForFormat(MediaFormat format) {
538         return check(canEncode(format), "no encoder for " + format);
539     }
540 
checkDecoderForFormat(MediaFormat format)541     public static boolean checkDecoderForFormat(MediaFormat format) {
542         return check(canDecode(format), "no decoder for " + format);
543     }
544 
545     /*
546      *  ----------------------- HELPER METHODS FOR MEDIA HANDLING -----------------------
547      */
548 
getVideoCapabilities(String codecName, String mime)549     public static VideoCapabilities getVideoCapabilities(String codecName, String mime) {
550         for (MediaCodecInfo info : sMCL.getCodecInfos()) {
551             if (!info.getName().equalsIgnoreCase(codecName)) {
552                 continue;
553             }
554             CodecCapabilities caps;
555             try {
556                 caps = info.getCapabilitiesForType(mime);
557             } catch (IllegalArgumentException e) {
558                 // mime is not supported
559                 Log.w(TAG, "not supported mime: " + mime);
560                 return null;
561             }
562             VideoCapabilities vidCaps = caps.getVideoCapabilities();
563             if (vidCaps == null) {
564                 Log.w(TAG, "not a video codec: " + codecName);
565             }
566             return vidCaps;
567         }
568         Log.w(TAG, "codec not found: " + codecName);
569         return null;
570     }
571 
getTrackFormatForResource( Context context, int resourceId, String mimeTypePrefix)572     public static MediaFormat getTrackFormatForResource(
573             Context context, int resourceId, String mimeTypePrefix)
574             throws IOException {
575         MediaFormat format = null;
576         MediaExtractor extractor = new MediaExtractor();
577         AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId);
578         try {
579             extractor.setDataSource(
580                     afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
581         } finally {
582             afd.close();
583         }
584         int trackIndex;
585         for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) {
586             MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex);
587             if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) {
588                 format = trackMediaFormat;
589                 break;
590             }
591         }
592         extractor.release();
593         afd.close();
594         if (format == null) {
595             throw new RuntimeException("couldn't get a track for " + mimeTypePrefix);
596         }
597 
598         return format;
599     }
600 
createMediaExtractorForMimeType( Context context, int resourceId, String mimeTypePrefix)601     public static MediaExtractor createMediaExtractorForMimeType(
602             Context context, int resourceId, String mimeTypePrefix)
603             throws IOException {
604         MediaExtractor extractor = new MediaExtractor();
605         AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId);
606         try {
607             extractor.setDataSource(
608                     afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
609         } finally {
610             afd.close();
611         }
612         int trackIndex;
613         for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) {
614             MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex);
615             if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) {
616                 extractor.selectTrack(trackIndex);
617                 break;
618             }
619         }
620         if (trackIndex == extractor.getTrackCount()) {
621             extractor.release();
622             throw new IllegalStateException("couldn't get a track for " + mimeTypePrefix);
623         }
624 
625         return extractor;
626     }
627 
628     /*
629      *  ---------------------- HELPER METHODS FOR CODEC CONFIGURATION
630      */
631 
632     /** Format must contain mime, width and height.
633      *  Throws Exception if encoder does not support this width and height */
setMaxEncoderFrameAndBitrates( MediaCodec encoder, MediaFormat format, int maxFps)634     public static void setMaxEncoderFrameAndBitrates(
635             MediaCodec encoder, MediaFormat format, int maxFps) {
636         String mime = format.getString(MediaFormat.KEY_MIME);
637 
638         VideoCapabilities vidCaps =
639             encoder.getCodecInfo().getCapabilitiesForType(mime).getVideoCapabilities();
640         setMaxEncoderFrameAndBitrates(vidCaps, format, maxFps);
641     }
642 
setMaxEncoderFrameAndBitrates( VideoCapabilities vidCaps, MediaFormat format, int maxFps)643     public static void setMaxEncoderFrameAndBitrates(
644             VideoCapabilities vidCaps, MediaFormat format, int maxFps) {
645         int width = format.getInteger(MediaFormat.KEY_WIDTH);
646         int height = format.getInteger(MediaFormat.KEY_HEIGHT);
647 
648         int maxWidth = vidCaps.getSupportedWidths().getUpper();
649         int maxHeight = vidCaps.getSupportedHeightsFor(maxWidth).getUpper();
650         int frameRate = Math.min(
651                 maxFps, vidCaps.getSupportedFrameRatesFor(width, height).getUpper().intValue());
652         format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
653 
654         int bitrate = vidCaps.getBitrateRange().clamp(
655             (int)(vidCaps.getBitrateRange().getUpper() /
656                   Math.sqrt((double)maxWidth * maxHeight / width / height)));
657         format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
658     }
659 
660     /*
661      *  ------------------ HELPER METHODS FOR STATISTICS AND REPORTING ------------------
662      */
663 
664     // TODO: migrate this into com.android.compatibility.common.util.Stat
665     public static class Stats {
666         /** does not support NaN or Inf in |data| */
Stats(double[] data)667         public Stats(double[] data) {
668             mData = data;
669             if (mData != null) {
670                 mNum = mData.length;
671             }
672         }
673 
getNum()674         public int getNum() {
675             return mNum;
676         }
677 
678         /** calculate mSumX and mSumXX */
analyze()679         private void analyze() {
680             if (mAnalyzed) {
681                 return;
682             }
683 
684             if (mData != null) {
685                 for (double x : mData) {
686                     if (!(x >= mMinX)) { // mMinX may be NaN
687                         mMinX = x;
688                     }
689                     if (!(x <= mMaxX)) { // mMaxX may be NaN
690                         mMaxX = x;
691                     }
692                     mSumX += x;
693                     mSumXX += x * x;
694                 }
695             }
696             mAnalyzed = true;
697         }
698 
699         /** returns the maximum or NaN if it does not exist */
getMin()700         public double getMin() {
701             analyze();
702             return mMinX;
703         }
704 
705         /** returns the minimum or NaN if it does not exist */
getMax()706         public double getMax() {
707             analyze();
708             return mMaxX;
709         }
710 
711         /** returns the average or NaN if it does not exist. */
getAverage()712         public double getAverage() {
713             analyze();
714             if (mNum == 0) {
715                 return Double.NaN;
716             } else {
717                 return mSumX / mNum;
718             }
719         }
720 
721         /** returns the standard deviation or NaN if it does not exist. */
getStdev()722         public double getStdev() {
723             analyze();
724             if (mNum == 0) {
725                 return Double.NaN;
726             } else {
727                 double average = mSumX / mNum;
728                 return Math.sqrt(mSumXX / mNum - average * average);
729             }
730         }
731 
732         /** returns the statistics for the moving average over n values */
movingAverage(int n)733         public Stats movingAverage(int n) {
734             if (n < 1 || mNum < n) {
735                 return new Stats(null);
736             } else if (n == 1) {
737                 return this;
738             }
739 
740             double[] avgs = new double[mNum - n + 1];
741             double sum = 0;
742             for (int i = 0; i < mNum; ++i) {
743                 sum += mData[i];
744                 if (i >= n - 1) {
745                     avgs[i - n + 1] = sum / n;
746                     sum -= mData[i - n + 1];
747                 }
748             }
749             return new Stats(avgs);
750         }
751 
752         /** returns the statistics for the moving average over a window over the
753          *  cumulative sum. Basically, moves a window from: [0, window] to
754          *  [sum - window, sum] over the cumulative sum, over ((sum - window) / average)
755          *  steps, and returns the average value over each window.
756          *  This method is used to average time-diff data over a window of a constant time.
757          */
movingAverageOverSum(double window)758         public Stats movingAverageOverSum(double window) {
759             if (window <= 0 || mNum < 1) {
760                 return new Stats(null);
761             }
762 
763             analyze();
764             double average = mSumX / mNum;
765             if (window >= mSumX) {
766                 return new Stats(new double[] { average });
767             }
768             int samples = (int)Math.ceil((mSumX - window) / average);
769             double[] avgs = new double[samples];
770 
771             // A somewhat brute force approach to calculating the moving average.
772             // TODO: add support for weights in Stats, so we can do a more refined approach.
773             double sum = 0; // sum of elements in the window
774             int num = 0; // number of elements in the moving window
775             int bi = 0; // index of the first element in the moving window
776             int ei = 0; // index of the last element in the moving window
777             double space = window; // space at the end of the window
778             double foot = 0; // space at the beginning of the window
779 
780             // invariants: foot + sum + space == window
781             //             bi + num == ei
782             //
783             //  window:             |-------------------------------|
784             //                      |    <-----sum------>           |
785             //                      <foot>               <---space-->
786             //                           |               |
787             //  intervals:   |-----------|-------|-------|--------------------|--------|
788             //                           ^bi             ^ei
789 
790             int ix = 0; // index in the result
791             while (ix < samples) {
792                 // add intervals while there is space in the window
793                 while (ei < mData.length && mData[ei] <= space) {
794                     space -= mData[ei];
795                     sum += mData[ei];
796                     num++;
797                     ei++;
798                 }
799 
800                 // calculate average over window and deal with odds and ends (e.g. if there are no
801                 // intervals in the current window: pick whichever element overlaps the window
802                 // most.
803                 if (num > 0) {
804                     avgs[ix++] = sum / num;
805                 } else if (bi > 0 && foot > space) {
806                     // consider previous
807                     avgs[ix++] = mData[bi - 1];
808                 } else if (ei == mData.length) {
809                     break;
810                 } else {
811                     avgs[ix++] = mData[ei];
812                 }
813 
814                 // move the window to the next position
815                 foot -= average;
816                 space += average;
817 
818                 // remove intervals that are now partially or wholly outside of the window
819                 while (bi < ei && foot < 0) {
820                     foot += mData[bi];
821                     sum -= mData[bi];
822                     num--;
823                     bi++;
824                 }
825             }
826             return new Stats(Arrays.copyOf(avgs, ix));
827         }
828 
829         /** calculate mSortedData */
sort()830         private void sort() {
831             if (mSorted || mNum == 0) {
832                 return;
833             }
834             mSortedData = Arrays.copyOf(mData, mNum);
835             Arrays.sort(mSortedData);
836             mSorted = true;
837         }
838 
839         /** returns an array of percentiles for the points using nearest rank */
getPercentiles(double... points)840         public double[] getPercentiles(double... points) {
841             sort();
842             double[] res = new double[points.length];
843             for (int i = 0; i < points.length; ++i) {
844                 if (mNum < 1 || points[i] < 0 || points[i] > 100) {
845                     res[i] = Double.NaN;
846                 } else {
847                     res[i] = mSortedData[(int)Math.round(points[i] / 100 * (mNum - 1))];
848                 }
849             }
850             return res;
851         }
852 
853         @Override
equals(Object o)854         public boolean equals(Object o) {
855             if (o instanceof Stats) {
856                 Stats other = (Stats)o;
857                 if (other.mNum != mNum) {
858                     return false;
859                 } else if (mNum == 0) {
860                     return true;
861                 }
862                 return Arrays.equals(mData, other.mData);
863             }
864             return false;
865         }
866 
867         private double[] mData;
868         private double mSumX = 0;
869         private double mSumXX = 0;
870         private double mMinX = Double.NaN;
871         private double mMaxX = Double.NaN;
872         private int mNum = 0;
873         private boolean mAnalyzed = false;
874         private double[] mSortedData;
875         private boolean mSorted = false;
876     }
877 
878     /*
879      *  -------------------------------------- END --------------------------------------
880      */
881 }
882