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     // returns the list of codecs that support any one of the 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 */
    231     public static String[] getDecoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) {
    232         return getCodecNames(false /* isEncoder */, isGoog, formats);
    233     }
    234 
    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 */
    240     public static String[] getEncoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) {
    241         return getCodecNames(true /* isEncoder */, isGoog, formats);
    242     }
    243 
    244     public static String[] getEncoderNames(MediaFormat... formats) {
    245         return getCodecNames(true /* isEncoder */, null /* isGoog */, formats);
    246     }
    247 
    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 
    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 
    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 
    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 
    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 
    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 
    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      */
    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      */
    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 
    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      */
    403     public static boolean hasCodecsForResource(Context context, int resourceId) {
    404         return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, null /* mimePrefix */);
    405     }
    406 
    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      */
    414     public static boolean hasCodecForResource(Context context, int resourceId, int track) {
    415         return hasCodecsForResourceCombo(context, resourceId, track, null /* mimePrefix */);
    416     }
    417 
    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      */
    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      */
    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 
    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 
    468     public static boolean checkCodecsForPath(Context context, String path) {
    469         return check(hasCodecsForPath(context, path), "no decoder found");
    470     }
    471 
    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 
    488     public static boolean checkCodecForDomain(boolean encoder, String domain) {
    489         return check(hasCodecForDomain(encoder, domain),
    490                 "no " + domain + (encoder ? " encoder" : " decoder") + " found");
    491     }
    492 
    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 
    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 
    520     public static boolean hasEncoder(String... mimes) {
    521         return hasCodecForMimes(true /* encoder */, mimes);
    522     }
    523 
    524     public static boolean hasDecoder(String... mimes) {
    525         return hasCodecForMimes(false /* encoder */, mimes);
    526     }
    527 
    528     public static boolean checkDecoder(String... mimes) {
    529         return check(hasCodecForMimes(false /* encoder */, mimes), "no decoder found");
    530     }
    531 
    532     public static boolean checkEncoder(String... mimes) {
    533         return check(hasCodecForMimes(true /* encoder */, mimes), "no encoder found");
    534     }
    535 
    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 
    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 
    559     public static boolean checkEncoderForFormat(MediaFormat format) {
    560         return check(canEncode(format), "no encoder for " + format);
    561     }
    562 
    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 
    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 
    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 
    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 
    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 
    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 */
    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 
    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| */
    701         public Stats(double[] data) {
    702             mData = data;
    703             if (mData != null) {
    704                 mNum = mData.length;
    705             }
    706         }
    707 
    708         public int getNum() {
    709             return mNum;
    710         }
    711 
    712         /** calculate mSumX and mSumXX */
    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 */
    734         public double getMin() {
    735             analyze();
    736             return mMinX;
    737         }
    738 
    739         /** returns the minimum or NaN if it does not exist */
    740         public double getMax() {
    741             analyze();
    742             return mMaxX;
    743         }
    744 
    745         /** returns the average or NaN if it does not exist. */
    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. */
    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 */
    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          */
    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 */
    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 */
    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
    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      */
    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      */
   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 
   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 
   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