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