blob: 3ea6b6376863163f9ff028cceae339fb60d3a2ca [file] [log] [blame]
/*
* Copyright 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.compatibility.common.util;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.drm.DrmConvertedStatus;
import android.drm.DrmManagerClient;
import android.graphics.ImageFormat;
import android.media.Image;
import android.media.Image.Plane;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.VideoCapabilities;
import android.media.MediaCodecList;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.net.Uri;
import android.util.Log;
import android.util.Range;
import com.android.compatibility.common.util.DeviceReportLog;
import com.android.compatibility.common.util.ResultType;
import com.android.compatibility.common.util.ResultUnit;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import static java.lang.reflect.Modifier.isPublic;
import static java.lang.reflect.Modifier.isStatic;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static junit.framework.Assert.assertTrue;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
public class MediaUtils {
private static final String TAG = "MediaUtils";
/*
* ----------------------- HELPER METHODS FOR SKIPPING TESTS -----------------------
*/
private static final int ALL_AV_TRACKS = -1;
private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
/**
* Returns the test name (heuristically).
*
* Since it uses heuristics, this method has only been verified for media
* tests. This centralizes the way to signal errors during a test.
*/
public static String getTestName() {
return getTestName(false /* withClass */);
}
/**
* Returns the test name with the full class (heuristically).
*
* Since it uses heuristics, this method has only been verified for media
* tests. This centralizes the way to signal errors during a test.
*/
public static String getTestNameWithClass() {
return getTestName(true /* withClass */);
}
private static String getTestName(boolean withClass) {
int bestScore = -1;
String testName = "test???";
Map<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces();
for (Map.Entry<Thread, StackTraceElement[]> entry : traces.entrySet()) {
StackTraceElement[] stack = entry.getValue();
for (int index = 0; index < stack.length; ++index) {
// method name must start with "test"
String methodName = stack[index].getMethodName();
if (!methodName.startsWith("test")) {
continue;
}
int score = 0;
// see if there is a public non-static void method that takes no argument
Class<?> clazz;
try {
clazz = Class.forName(stack[index].getClassName());
++score;
for (final Method method : clazz.getDeclaredMethods()) {
if (method.getName().equals(methodName)
&& isPublic(method.getModifiers())
&& !isStatic(method.getModifiers())
&& method.getParameterTypes().length == 0
&& method.getReturnType().equals(Void.TYPE)) {
++score;
break;
}
}
if (score == 1) {
// if we could read the class, but method is not public void, it is
// not a candidate
continue;
}
} catch (ClassNotFoundException e) {
}
// even if we cannot verify the method signature, there are signals in the stack
// usually test method is invoked by reflection
int depth = 1;
while (index + depth < stack.length
&& stack[index + depth].getMethodName().equals("invoke")
&& stack[index + depth].getClassName().equals(
"java.lang.reflect.Method")) {
++depth;
}
if (depth > 1) {
++score;
// and usually test method is run by runMethod method in android.test package
if (index + depth < stack.length) {
if (stack[index + depth].getClassName().startsWith("android.test.")) {
++score;
}
if (stack[index + depth].getMethodName().equals("runMethod")) {
++score;
}
}
}
if (score > bestScore) {
bestScore = score;
testName = methodName;
if (withClass) {
testName = stack[index].getClassName() + "." + testName;
}
}
}
}
return testName;
}
/**
* Finds test name (heuristically) and prints out standard skip message.
*
* Since it uses heuristics, this method has only been verified for media
* tests. This centralizes the way to signal a skipped test.
*/
public static void skipTest(String tag, String reason) {
Log.i(tag, "SKIPPING " + getTestName() + "(): " + reason);
DeviceReportLog log = new DeviceReportLog("CtsMediaSkippedTests", "test_skipped");
try {
log.addValue("reason", reason, ResultType.NEUTRAL, ResultUnit.NONE);
log.addValue(
"test", getTestNameWithClass(), ResultType.NEUTRAL, ResultUnit.NONE);
log.submit();
} catch (NullPointerException e) { }
}
/**
* Finds test name (heuristically) and prints out standard skip message.
*
* Since it uses heuristics, this method has only been verified for media
* tests. This centralizes the way to signal a skipped test.
*/
public static void skipTest(String reason) {
skipTest(TAG, reason);
}
public static boolean check(boolean result, String message) {
if (!result) {
skipTest(message);
}
return result;
}
/*
* ------------------- HELPER METHODS FOR CHECKING CODEC SUPPORT -------------------
*/
public static boolean isGoogle(String codecName) {
codecName = codecName.toLowerCase();
return codecName.startsWith("omx.google.")
|| codecName.startsWith("c2.android.")
|| codecName.startsWith("c2.google.");
}
// returns the list of codecs that support any one of the formats
private static String[] getCodecNames(
boolean isEncoder, Boolean isGoog, MediaFormat... formats) {
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
ArrayList<String> result = new ArrayList<>();
for (MediaCodecInfo info : mcl.getCodecInfos()) {
if (info.isEncoder() != isEncoder) {
continue;
}
if (isGoog != null && isGoogle(info.getName()) != isGoog) {
continue;
}
for (MediaFormat format : formats) {
String mime = format.getString(MediaFormat.KEY_MIME);
CodecCapabilities caps = null;
try {
caps = info.getCapabilitiesForType(mime);
} catch (IllegalArgumentException e) { // mime is not supported
continue;
}
if (caps.isFormatSupported(format)) {
result.add(info.getName());
break;
}
}
}
return result.toArray(new String[result.size()]);
}
/* Use isGoog = null to query all decoders */
public static String[] getDecoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) {
return getCodecNames(false /* isEncoder */, isGoog, formats);
}
public static String[] getDecoderNames(MediaFormat... formats) {
return getCodecNames(false /* isEncoder */, null /* isGoog */, formats);
}
/* Use isGoog = null to query all decoders */
public static String[] getEncoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) {
return getCodecNames(true /* isEncoder */, isGoog, formats);
}
public static String[] getEncoderNames(MediaFormat... formats) {
return getCodecNames(true /* isEncoder */, null /* isGoog */, formats);
}
public static String[] getDecoderNamesForMime(String mime) {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, mime);
return getCodecNames(false /* isEncoder */, null /* isGoog */, format);
}
public static String[] getEncoderNamesForMime(String mime) {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, mime);
return getCodecNames(true /* isEncoder */, null /* isGoog */, format);
}
public static void verifyNumCodecs(
int count, boolean isEncoder, Boolean isGoog, MediaFormat... formats) {
String desc = (isEncoder ? "encoders" : "decoders") + " for "
+ (formats.length == 1 ? formats[0].toString() : Arrays.toString(formats));
if (isGoog != null) {
desc = (isGoog ? "Google " : "non-Google ") + desc;
}
String[] codecs = getCodecNames(isEncoder, isGoog, formats);
assertTrue("test can only verify " + count + " " + desc + "; found " + codecs.length + ": "
+ Arrays.toString(codecs), codecs.length <= count);
}
public static MediaCodec getDecoder(MediaFormat format) {
String decoder = sMCL.findDecoderForFormat(format);
if (decoder != null) {
try {
return MediaCodec.createByCodecName(decoder);
} catch (IOException e) {
}
}
return null;
}
public static boolean canEncode(MediaFormat format) {
if (sMCL.findEncoderForFormat(format) == null) {
Log.i(TAG, "no encoder for " + format);
return false;
}
return true;
}
public static boolean canDecode(MediaFormat format) {
if (sMCL.findDecoderForFormat(format) == null) {
Log.i(TAG, "no decoder for " + format);
return false;
}
return true;
}
public static boolean supports(String codecName, String mime, int w, int h) {
// While this could be simply written as such, give more graceful feedback.
// MediaFormat format = MediaFormat.createVideoFormat(mime, w, h);
// return supports(codecName, format);
VideoCapabilities vidCap = getVideoCapabilities(codecName, mime);
if (vidCap == null) {
return false;
} else if (vidCap.isSizeSupported(w, h)) {
return true;
}
Log.w(TAG, "unsupported size " + w + "x" + h);
return false;
}
public static boolean supports(String codecName, MediaFormat format) {
MediaCodec codec;
try {
codec = MediaCodec.createByCodecName(codecName);
} catch (IOException e) {
Log.w(TAG, "codec not found: " + codecName);
return false;
}
String mime = format.getString(MediaFormat.KEY_MIME);
CodecCapabilities cap = null;
try {
cap = codec.getCodecInfo().getCapabilitiesForType(mime);
return cap.isFormatSupported(format);
} catch (IllegalArgumentException e) {
Log.w(TAG, "not supported mime: " + mime);
return false;
} finally {
codec.release();
}
}
public static boolean hasCodecForTrack(MediaExtractor ex, int track) {
int count = ex.getTrackCount();
if (track < 0 || track >= count) {
throw new IndexOutOfBoundsException(track + " not in [0.." + (count - 1) + "]");
}
return canDecode(ex.getTrackFormat(track));
}
/**
* return true iff all audio and video tracks are supported
*/
public static boolean hasCodecsForMedia(MediaExtractor ex) {
for (int i = 0; i < ex.getTrackCount(); ++i) {
MediaFormat format = ex.getTrackFormat(i);
// only check for audio and video codecs
String mime = format.getString(MediaFormat.KEY_MIME).toLowerCase();
if (!mime.startsWith("audio/") && !mime.startsWith("video/")) {
continue;
}
if (!canDecode(format)) {
return false;
}
}
return true;
}
/**
* return true iff any track starting with mimePrefix is supported
*/
public static boolean hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix) {
mimePrefix = mimePrefix.toLowerCase();
for (int i = 0; i < ex.getTrackCount(); ++i) {
MediaFormat format = ex.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.toLowerCase().startsWith(mimePrefix)) {
if (canDecode(format)) {
return true;
}
Log.i(TAG, "no decoder for " + format);
}
}
return false;
}
private static boolean hasCodecsForResourceCombo(
Context context, int resourceId, int track, String mimePrefix) {
try {
AssetFileDescriptor afd = null;
MediaExtractor ex = null;
try {
afd = context.getResources().openRawResourceFd(resourceId);
ex = new MediaExtractor();
ex.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
if (mimePrefix != null) {
return hasCodecForMediaAndDomain(ex, mimePrefix);
} else if (track == ALL_AV_TRACKS) {
return hasCodecsForMedia(ex);
} else {
return hasCodecForTrack(ex, track);
}
} finally {
if (ex != null) {
ex.release();
}
if (afd != null) {
afd.close();
}
}
} catch (IOException e) {
Log.i(TAG, "could not open resource");
}
return false;
}
/**
* return true iff all audio and video tracks are supported
*/
public static boolean hasCodecsForResource(Context context, int resourceId) {
return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, null /* mimePrefix */);
}
public static boolean checkCodecsForResource(Context context, int resourceId) {
return check(hasCodecsForResource(context, resourceId), "no decoder found");
}
/**
* return true iff track is supported.
*/
public static boolean hasCodecForResource(Context context, int resourceId, int track) {
return hasCodecsForResourceCombo(context, resourceId, track, null /* mimePrefix */);
}
public static boolean checkCodecForResource(Context context, int resourceId, int track) {
return check(hasCodecForResource(context, resourceId, track), "no decoder found");
}
/**
* return true iff any track starting with mimePrefix is supported
*/
public static boolean hasCodecForResourceAndDomain(
Context context, int resourceId, String mimePrefix) {
return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, mimePrefix);
}
/**
* return true iff all audio and video tracks are supported
*/
public static boolean hasCodecsForPath(Context context, String path) {
MediaExtractor ex = null;
try {
ex = getExtractorForPath(context, path);
return hasCodecsForMedia(ex);
} catch (IOException e) {
Log.i(TAG, "could not open path " + path);
} finally {
if (ex != null) {
ex.release();
}
}
return true;
}
private static MediaExtractor getExtractorForPath(Context context, String path)
throws IOException {
Uri uri = Uri.parse(path);
String scheme = uri.getScheme();
MediaExtractor ex = new MediaExtractor();
try {
if (scheme == null) { // file
ex.setDataSource(path);
} else if (scheme.equalsIgnoreCase("file")) {
ex.setDataSource(uri.getPath());
} else {
ex.setDataSource(context, uri, null);
}
} catch (IOException e) {
ex.release();
throw e;
}
return ex;
}
public static boolean checkCodecsForPath(Context context, String path) {
return check(hasCodecsForPath(context, path), "no decoder found");
}
public static boolean hasCodecForDomain(boolean encoder, String domain) {
for (MediaCodecInfo info : sMCL.getCodecInfos()) {
if (encoder != info.isEncoder()) {
continue;
}
for (String type : info.getSupportedTypes()) {
if (type.toLowerCase().startsWith(domain.toLowerCase() + "/")) {
Log.i(TAG, "found codec " + info.getName() + " for mime " + type);
return true;
}
}
}
return false;
}
public static boolean checkCodecForDomain(boolean encoder, String domain) {
return check(hasCodecForDomain(encoder, domain),
"no " + domain + (encoder ? " encoder" : " decoder") + " found");
}
private static boolean hasCodecForMime(boolean encoder, String mime) {
for (MediaCodecInfo info : sMCL.getCodecInfos()) {
if (encoder != info.isEncoder()) {
continue;
}
for (String type : info.getSupportedTypes()) {
if (type.equalsIgnoreCase(mime)) {
Log.i(TAG, "found codec " + info.getName() + " for mime " + mime);
return true;
}
}
}
return false;
}
private static boolean hasCodecForMimes(boolean encoder, String[] mimes) {
for (String mime : mimes) {
if (!hasCodecForMime(encoder, mime)) {
Log.i(TAG, "no " + (encoder ? "encoder" : "decoder") + " for mime " + mime);
return false;
}
}
return true;
}
public static boolean hasEncoder(String... mimes) {
return hasCodecForMimes(true /* encoder */, mimes);
}
public static boolean hasDecoder(String... mimes) {
return hasCodecForMimes(false /* encoder */, mimes);
}
public static boolean checkDecoder(String... mimes) {
return check(hasCodecForMimes(false /* encoder */, mimes), "no decoder found");
}
public static boolean checkEncoder(String... mimes) {
return check(hasCodecForMimes(true /* encoder */, mimes), "no encoder found");
}
public static boolean canDecodeVideo(String mime, int width, int height, float rate) {
MediaFormat format = MediaFormat.createVideoFormat(mime, width, height);
format.setFloat(MediaFormat.KEY_FRAME_RATE, rate);
return canDecode(format);
}
public static boolean canDecodeVideo(
String mime, int width, int height, float rate,
Integer profile, Integer level, Integer bitrate) {
MediaFormat format = MediaFormat.createVideoFormat(mime, width, height);
format.setFloat(MediaFormat.KEY_FRAME_RATE, rate);
if (profile != null) {
format.setInteger(MediaFormat.KEY_PROFILE, profile);
if (level != null) {
format.setInteger(MediaFormat.KEY_LEVEL, level);
}
}
if (bitrate != null) {
format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
}
return canDecode(format);
}
public static boolean checkEncoderForFormat(MediaFormat format) {
return check(canEncode(format), "no encoder for " + format);
}
public static boolean checkDecoderForFormat(MediaFormat format) {
return check(canDecode(format), "no decoder for " + format);
}
/*
* ----------------------- HELPER METHODS FOR MEDIA HANDLING -----------------------
*/
public static VideoCapabilities getVideoCapabilities(String codecName, String mime) {
for (MediaCodecInfo info : sMCL.getCodecInfos()) {
if (!info.getName().equalsIgnoreCase(codecName)) {
continue;
}
CodecCapabilities caps;
try {
caps = info.getCapabilitiesForType(mime);
} catch (IllegalArgumentException e) {
// mime is not supported
Log.w(TAG, "not supported mime: " + mime);
return null;
}
VideoCapabilities vidCaps = caps.getVideoCapabilities();
if (vidCaps == null) {
Log.w(TAG, "not a video codec: " + codecName);
}
return vidCaps;
}
Log.w(TAG, "codec not found: " + codecName);
return null;
}
public static MediaFormat getTrackFormatForResource(
Context context,
int resourceId,
String mimeTypePrefix) throws IOException {
MediaExtractor extractor = new MediaExtractor();
AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId);
try {
extractor.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
} finally {
afd.close();
}
return getTrackFormatForExtractor(extractor, mimeTypePrefix);
}
public static MediaFormat getTrackFormatForPath(
Context context, String path, String mimeTypePrefix)
throws IOException {
MediaExtractor extractor = getExtractorForPath(context, path);
return getTrackFormatForExtractor(extractor, mimeTypePrefix);
}
private static MediaFormat getTrackFormatForExtractor(
MediaExtractor extractor,
String mimeTypePrefix) {
int trackIndex;
MediaFormat format = null;
for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) {
MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex);
if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) {
format = trackMediaFormat;
break;
}
}
extractor.release();
if (format == null) {
throw new RuntimeException("couldn't get a track for " + mimeTypePrefix);
}
return format;
}
public static MediaExtractor createMediaExtractorForMimeType(
Context context, int resourceId, String mimeTypePrefix)
throws IOException {
MediaExtractor extractor = new MediaExtractor();
AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId);
try {
extractor.setDataSource(
afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
} finally {
afd.close();
}
int trackIndex;
for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) {
MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex);
if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) {
extractor.selectTrack(trackIndex);
break;
}
}
if (trackIndex == extractor.getTrackCount()) {
extractor.release();
throw new IllegalStateException("couldn't get a track for " + mimeTypePrefix);
}
return extractor;
}
/*
* ---------------------- HELPER METHODS FOR CODEC CONFIGURATION
*/
/** Format must contain mime, width and height.
* Throws Exception if encoder does not support this width and height */
public static void setMaxEncoderFrameAndBitrates(
MediaCodec encoder, MediaFormat format, int maxFps) {
String mime = format.getString(MediaFormat.KEY_MIME);
VideoCapabilities vidCaps =
encoder.getCodecInfo().getCapabilitiesForType(mime).getVideoCapabilities();
setMaxEncoderFrameAndBitrates(vidCaps, format, maxFps);
}
public static void setMaxEncoderFrameAndBitrates(
VideoCapabilities vidCaps, MediaFormat format, int maxFps) {
int width = format.getInteger(MediaFormat.KEY_WIDTH);
int height = format.getInteger(MediaFormat.KEY_HEIGHT);
int maxWidth = vidCaps.getSupportedWidths().getUpper();
int maxHeight = vidCaps.getSupportedHeightsFor(maxWidth).getUpper();
int frameRate = Math.min(
maxFps, vidCaps.getSupportedFrameRatesFor(width, height).getUpper().intValue());
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
int bitrate = vidCaps.getBitrateRange().clamp(
(int)(vidCaps.getBitrateRange().getUpper() /
Math.sqrt((double)maxWidth * maxHeight / width / height)));
format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
}
/*
* ------------------ HELPER METHODS FOR STATISTICS AND REPORTING ------------------
*/
// TODO: migrate this into com.android.compatibility.common.util.Stat
public static class Stats {
/** does not support NaN or Inf in |data| */
public Stats(double[] data) {
mData = data;
if (mData != null) {
mNum = mData.length;
}
}
public int getNum() {
return mNum;
}
/** calculate mSumX and mSumXX */
private void analyze() {
if (mAnalyzed) {
return;
}
if (mData != null) {
for (double x : mData) {
if (!(x >= mMinX)) { // mMinX may be NaN
mMinX = x;
}
if (!(x <= mMaxX)) { // mMaxX may be NaN
mMaxX = x;
}
mSumX += x;
mSumXX += x * x;
}
}
mAnalyzed = true;
}
/** returns the maximum or NaN if it does not exist */
public double getMin() {
analyze();
return mMinX;
}
/** returns the minimum or NaN if it does not exist */
public double getMax() {
analyze();
return mMaxX;
}
/** returns the average or NaN if it does not exist. */
public double getAverage() {
analyze();
if (mNum == 0) {
return Double.NaN;
} else {
return mSumX / mNum;
}
}
/** returns the standard deviation or NaN if it does not exist. */
public double getStdev() {
analyze();
if (mNum == 0) {
return Double.NaN;
} else {
double average = mSumX / mNum;
return Math.sqrt(mSumXX / mNum - average * average);
}
}
/** returns the statistics for the moving average over n values */
public Stats movingAverage(int n) {
if (n < 1 || mNum < n) {
return new Stats(null);
} else if (n == 1) {
return this;
}
double[] avgs = new double[mNum - n + 1];
double sum = 0;
for (int i = 0; i < mNum; ++i) {
sum += mData[i];
if (i >= n - 1) {
avgs[i - n + 1] = sum / n;
sum -= mData[i - n + 1];
}
}
return new Stats(avgs);
}
/** returns the statistics for the moving average over a window over the
* cumulative sum. Basically, moves a window from: [0, window] to
* [sum - window, sum] over the cumulative sum, over ((sum - window) / average)
* steps, and returns the average value over each window.
* This method is used to average time-diff data over a window of a constant time.
*/
public Stats movingAverageOverSum(double window) {
if (window <= 0 || mNum < 1) {
return new Stats(null);
}
analyze();
double average = mSumX / mNum;
if (window >= mSumX) {
return new Stats(new double[] { average });
}
int samples = (int)Math.ceil((mSumX - window) / average);
double[] avgs = new double[samples];
// A somewhat brute force approach to calculating the moving average.
// TODO: add support for weights in Stats, so we can do a more refined approach.
double sum = 0; // sum of elements in the window
int num = 0; // number of elements in the moving window
int bi = 0; // index of the first element in the moving window
int ei = 0; // index of the last element in the moving window
double space = window; // space at the end of the window
double foot = 0; // space at the beginning of the window
// invariants: foot + sum + space == window
// bi + num == ei
//
// window: |-------------------------------|
// | <-----sum------> |
// <foot> <---space-->
// | |
// intervals: |-----------|-------|-------|--------------------|--------|
// ^bi ^ei
int ix = 0; // index in the result
while (ix < samples) {
// add intervals while there is space in the window
while (ei < mData.length && mData[ei] <= space) {
space -= mData[ei];
sum += mData[ei];
num++;
ei++;
}
// calculate average over window and deal with odds and ends (e.g. if there are no
// intervals in the current window: pick whichever element overlaps the window
// most.
if (num > 0) {
avgs[ix++] = sum / num;
} else if (bi > 0 && foot > space) {
// consider previous
avgs[ix++] = mData[bi - 1];
} else if (ei == mData.length) {
break;
} else {
avgs[ix++] = mData[ei];
}
// move the window to the next position
foot -= average;
space += average;
// remove intervals that are now partially or wholly outside of the window
while (bi < ei && foot < 0) {
foot += mData[bi];
sum -= mData[bi];
num--;
bi++;
}
}
return new Stats(Arrays.copyOf(avgs, ix));
}
/** calculate mSortedData */
private void sort() {
if (mSorted || mNum == 0) {
return;
}
mSortedData = Arrays.copyOf(mData, mNum);
Arrays.sort(mSortedData);
mSorted = true;
}
/** returns an array of percentiles for the points using nearest rank */
public double[] getPercentiles(double... points) {
sort();
double[] res = new double[points.length];
for (int i = 0; i < points.length; ++i) {
if (mNum < 1 || points[i] < 0 || points[i] > 100) {
res[i] = Double.NaN;
} else {
res[i] = mSortedData[(int)Math.round(points[i] / 100 * (mNum - 1))];
}
}
return res;
}
@Override
public boolean equals(Object o) {
if (o instanceof Stats) {
Stats other = (Stats)o;
if (other.mNum != mNum) {
return false;
} else if (mNum == 0) {
return true;
}
return Arrays.equals(mData, other.mData);
}
return false;
}
private double[] mData;
private double mSumX = 0;
private double mSumXX = 0;
private double mMinX = Double.NaN;
private double mMaxX = Double.NaN;
private int mNum = 0;
private boolean mAnalyzed = false;
private double[] mSortedData;
private boolean mSorted = false;
}
/**
* Convert a forward lock .dm message stream to a .fl file
* @param context Context to use
* @param dmStream The .dm message
* @param flFile The output file to be written
* @return success
*/
public static boolean convertDmToFl(
Context context,
InputStream dmStream,
RandomAccessFile flFile) {
final String MIMETYPE_DRM_MESSAGE = "application/vnd.oma.drm.message";
byte[] dmData = new byte[10000];
int totalRead = 0;
int numRead;
while (true) {
try {
numRead = dmStream.read(dmData, totalRead, dmData.length - totalRead);
} catch (IOException e) {
Log.w(TAG, "Failed to read from input file");
return false;
}
if (numRead == -1) {
break;
}
totalRead += numRead;
if (totalRead == dmData.length) {
// grow array
dmData = Arrays.copyOf(dmData, dmData.length + 10000);
}
}
byte[] fileData = Arrays.copyOf(dmData, totalRead);
DrmManagerClient drmClient = null;
try {
drmClient = new DrmManagerClient(context);
} catch (IllegalArgumentException e) {
Log.w(TAG, "DrmManagerClient instance could not be created, context is Illegal.");
return false;
} catch (IllegalStateException e) {
Log.w(TAG, "DrmManagerClient didn't initialize properly.");
return false;
}
try {
int convertSessionId = -1;
try {
convertSessionId = drmClient.openConvertSession(MIMETYPE_DRM_MESSAGE);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Conversion of Mimetype: " + MIMETYPE_DRM_MESSAGE
+ " is not supported.", e);
return false;
} catch (IllegalStateException e) {
Log.w(TAG, "Could not access Open DrmFramework.", e);
return false;
}
if (convertSessionId < 0) {
Log.w(TAG, "Failed to open session.");
return false;
}
DrmConvertedStatus convertedStatus = null;
try {
convertedStatus = drmClient.convertData(convertSessionId, fileData);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Buffer with data to convert is illegal. Convertsession: "
+ convertSessionId, e);
return false;
} catch (IllegalStateException e) {
Log.w(TAG, "Could not convert data. Convertsession: " + convertSessionId, e);
return false;
}
if (convertedStatus == null ||
convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK ||
convertedStatus.convertedData == null) {
Log.w(TAG, "Error in converting data. Convertsession: " + convertSessionId);
try {
DrmConvertedStatus result = drmClient.closeConvertSession(convertSessionId);
if (result.statusCode != DrmConvertedStatus.STATUS_OK) {
Log.w(TAG, "Conversion failed with status: " + result.statusCode);
return false;
}
} catch (IllegalStateException e) {
Log.w(TAG, "Could not close session. Convertsession: " +
convertSessionId, e);
}
return false;
}
try {
flFile.write(convertedStatus.convertedData, 0, convertedStatus.convertedData.length);
} catch (IOException e) {
Log.w(TAG, "Failed to write to output file: " + e);
return false;
}
try {
convertedStatus = drmClient.closeConvertSession(convertSessionId);
} catch (IllegalStateException e) {
Log.w(TAG, "Could not close convertsession. Convertsession: " +
convertSessionId, e);
return false;
}
if (convertedStatus == null ||
convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK ||
convertedStatus.convertedData == null) {
Log.w(TAG, "Error in closing session. Convertsession: " + convertSessionId);
return false;
}
try {
flFile.seek(convertedStatus.offset);
flFile.write(convertedStatus.convertedData);
} catch (IOException e) {
Log.w(TAG, "Could not update file.", e);
return false;
}
return true;
} finally {
drmClient.close();
}
}
/**
* @param decoder new MediaCodec object
* @param ex MediaExtractor after setDataSource and selectTrack
* @param frameMD5Sums reference MD5 checksum for decoded frames
* @return true if decoded frames checksums matches reference checksums
* @throws IOException
*/
public static boolean verifyDecoder(
MediaCodec decoder, MediaExtractor ex, List<String> frameMD5Sums)
throws IOException {
int trackIndex = ex.getSampleTrackIndex();
MediaFormat format = ex.getTrackFormat(trackIndex);
decoder.configure(format, null /* surface */, null /* crypto */, 0 /* flags */);
decoder.start();
boolean sawInputEOS = false;
boolean sawOutputEOS = false;
final long kTimeOutUs = 5000; // 5ms timeout
int decodedFrameCount = 0;
int expectedFrameCount = frameMD5Sums.size();
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
while (!sawOutputEOS) {
// handle input
if (!sawInputEOS) {
int inIdx = decoder.dequeueInputBuffer(kTimeOutUs);
if (inIdx >= 0) {
ByteBuffer buffer = decoder.getInputBuffer(inIdx);
int sampleSize = ex.readSampleData(buffer, 0);
if (sampleSize < 0) {
final int flagEOS = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
decoder.queueInputBuffer(inIdx, 0, 0, 0, flagEOS);
sawInputEOS = true;
} else {
decoder.queueInputBuffer(inIdx, 0, sampleSize, ex.getSampleTime(), 0);
ex.advance();
}
}
}
// handle output
int outputBufIndex = decoder.dequeueOutputBuffer(info, kTimeOutUs);
if (outputBufIndex >= 0) {
try {
if (info.size > 0) {
// Disregard 0-sized buffers at the end.
String md5CheckSum = "";
Image image = decoder.getOutputImage(outputBufIndex);
md5CheckSum = getImageMD5Checksum(image);
if (!md5CheckSum.equals(frameMD5Sums.get(decodedFrameCount))) {
Log.d(TAG,
String.format(
"Frame %d md5sum mismatch: %s(actual) vs %s(expected)",
decodedFrameCount, md5CheckSum,
frameMD5Sums.get(decodedFrameCount)));
return false;
}
decodedFrameCount++;
}
} catch (Exception e) {
Log.e(TAG, "getOutputImage md5CheckSum failed", e);
return false;
} finally {
decoder.releaseOutputBuffer(outputBufIndex, false /* render */);
}
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
sawOutputEOS = true;
}
} else if (outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat decOutputFormat = decoder.getOutputFormat();
Log.d(TAG, "output format " + decOutputFormat);
} else if (outputBufIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
Log.i(TAG, "Skip handling MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED");
} else if (outputBufIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
continue;
} else {
Log.w(TAG, "decoder.dequeueOutputBuffer() unrecognized index: " + outputBufIndex);
return false;
}
}
if (decodedFrameCount != expectedFrameCount) {
return false;
}
return true;
}
public static String getImageMD5Checksum(Image image) throws Exception {
int format = image.getFormat();
if (ImageFormat.YUV_420_888 != format) {
Log.w(TAG, "unsupported image format");
return "";
}
MessageDigest md = MessageDigest.getInstance("MD5");
int imageWidth = image.getWidth();
int imageHeight = image.getHeight();
Image.Plane[] planes = image.getPlanes();
for (int i = 0; i < planes.length; ++i) {
ByteBuffer buf = planes[i].getBuffer();
int width, height, rowStride, pixelStride, x, y;
rowStride = planes[i].getRowStride();
pixelStride = planes[i].getPixelStride();
if (i == 0) {
width = imageWidth;
height = imageHeight;
} else {
width = imageWidth / 2;
height = imageHeight /2;
}
// local contiguous pixel buffer
byte[] bb = new byte[width * height];
if (buf.hasArray()) {
byte b[] = buf.array();
int offs = buf.arrayOffset();
if (pixelStride == 1) {
for (y = 0; y < height; ++y) {
System.arraycopy(bb, y * width, b, y * rowStride + offs, width);
}
} else {
// do it pixel-by-pixel
for (y = 0; y < height; ++y) {
int lineOffset = offs + y * rowStride;
for (x = 0; x < width; ++x) {
bb[y * width + x] = b[lineOffset + x * pixelStride];
}
}
}
} else { // almost always ends up here due to direct buffers
int pos = buf.position();
if (pixelStride == 1) {
for (y = 0; y < height; ++y) {
buf.position(pos + y * rowStride);
buf.get(bb, y * width, width);
}
} else {
// local line buffer
byte[] lb = new byte[rowStride];
// do it pixel-by-pixel
for (y = 0; y < height; ++y) {
buf.position(pos + y * rowStride);
// we're only guaranteed to have pixelStride * (width - 1) + 1 bytes
buf.get(lb, 0, pixelStride * (width - 1) + 1);
for (x = 0; x < width; ++x) {
bb[y * width + x] = lb[x * pixelStride];
}
}
}
buf.position(pos);
}
md.update(bb, 0, width * height);
}
return convertByteArrayToHEXString(md.digest());
}
private static String convertByteArrayToHEXString(byte[] ba) throws Exception {
StringBuilder result = new StringBuilder();
for (int i = 0; i < ba.length; i++) {
result.append(Integer.toString((ba[i] & 0xff) + 0x100, 16).substring(1));
}
return result.toString();
}
/*
* -------------------------------------- END --------------------------------------
*/
}