Home | History | Annotate | Download | only in util
      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      */
     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      */
     84     public static String getTestNameWithClass() {
     85         return getTestName(true /* withClass */);
     86     }
     87 
     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      */
    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      */
    183     public static void skipTest(String reason) {
    184         skipTest(TAG, reason);
    185     }
    186 
    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     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
    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 */
    237     public static String[] getDecoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) {
    238         return getCodecNames(false /* isEncoder */, isGoog, formats);
    239     }
    240 
    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 */
    246     public static String[] getEncoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) {
    247         return getCodecNames(true /* isEncoder */, isGoog, formats);
    248     }
    249 
    250     public static String[] getEncoderNames(MediaFormat... formats) {
    251         return getCodecNames(true /* isEncoder */, null /* isGoog */, formats);
    252     }
    253 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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      */
    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      */
    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 
    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      */
    421     public static boolean hasCodecsForResource(Context context, int resourceId) {
    422         return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, null /* mimePrefix */);
    423     }
    424 
    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      */
    432     public static boolean hasCodecForResource(Context context, int resourceId, int track) {
    433         return hasCodecsForResourceCombo(context, resourceId, track, null /* mimePrefix */);
    434     }
    435 
    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      */
    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      */
    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 
    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 
    486     public static boolean checkCodecsForPath(Context context, String path) {
    487         return check(hasCodecsForPath(context, path), "no decoder found");
    488     }
    489 
    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 
    506     public static boolean checkCodecForDomain(boolean encoder, String domain) {
    507         return check(hasCodecForDomain(encoder, domain),
    508                 "no " + domain + (encoder ? " encoder" : " decoder") + " found");
    509     }
    510 
    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 
    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 
    538     public static boolean hasEncoder(String... mimes) {
    539         return hasCodecForMimes(true /* encoder */, mimes);
    540     }
    541 
    542     public static boolean hasDecoder(String... mimes) {
    543         return hasCodecForMimes(false /* encoder */, mimes);
    544     }
    545 
    546     public static boolean checkDecoder(String... mimes) {
    547         return check(hasCodecForMimes(false /* encoder */, mimes), "no decoder found");
    548     }
    549 
    550     public static boolean checkEncoder(String... mimes) {
    551         return check(hasCodecForMimes(true /* encoder */, mimes), "no encoder found");
    552     }
    553 
    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 
    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 
    577     public static boolean checkEncoderForFormat(MediaFormat format) {
    578         return check(canEncode(format), "no encoder for " + format);
    579     }
    580 
    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 
    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 
    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 
    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 
    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 
    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 */
    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 
    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| */
    719         public Stats(double[] data) {
    720             mData = data;
    721             if (mData != null) {
    722                 mNum = mData.length;
    723             }
    724         }
    725 
    726         public int getNum() {
    727             return mNum;
    728         }
    729 
    730         /** calculate mSumX and mSumXX */
    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 */
    752         public double getMin() {
    753             analyze();
    754             return mMinX;
    755         }
    756 
    757         /** returns the minimum or NaN if it does not exist */
    758         public double getMax() {
    759             analyze();
    760             return mMaxX;
    761         }
    762 
    763         /** returns the average or NaN if it does not exist. */
    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. */
    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 */
    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          */
    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 */
    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 */
    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
    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      */
    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      */
   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 
   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 
   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