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