/* * 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.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.AssetFileDescriptor; import android.drm.DrmConvertedStatus; import android.drm.DrmManagerClient; import android.graphics.ImageFormat; import android.graphics.Rect; 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.os.BatteryManager; import android.os.Build; import android.os.SystemProperties; import android.os.ParcelFileDescriptor; import android.util.DisplayMetrics; import android.util.Log; import android.util.Range; import android.view.WindowManager; import androidx.test.platform.app.InstrumentationRegistry; import com.android.compatibility.common.util.DeviceReportLog; import com.android.compatibility.common.util.ResultType; import com.android.compatibility.common.util.ResultUnit; import java.io.File; 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"; private static final Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); private static final PackageManager pm = mContext.getPackageManager(); private static final boolean FIRST_SDK_IS_AT_LEAST_R = ApiLevelUtil.isFirstApiAtLeast(Build.VERSION_CODES.R); /* * ----------------------- 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 traces = Thread.getAllStackTraces(); for (Map.Entry 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) { ArrayList result = new ArrayList<>(); for (MediaCodecInfo info : sMCL.getCodecInfos()) { if (info.isAlias()) { // don't consider aliases here continue; } 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) { return canDecode(format, 0.0); } // this is "do we claim to decode"; caller is on the hook to determine // if we actually meet that claim, specifically around speed. public static boolean canDecode(MediaFormat format, double rate ) { String decoder = sMCL.findDecoderForFormat(format); if (decoder == null) { Log.i(TAG, "no decoder for " + format); return false; } if (rate == 0.0) { return true; } // before Q, we always said yes once we found a decoder for the format. if (ApiLevelUtil.isBefore(Build.VERSION_CODES.Q)) { return true; } // we care about speed of decoding Log.d(TAG, "checking for decoding " + format + " at " + rate + " fps with " + decoder); String mime = format.getString(MediaFormat.KEY_MIME); int width = format.getInteger(MediaFormat.KEY_WIDTH); int height = format.getInteger(MediaFormat.KEY_HEIGHT); MediaCodecInfo[] mciList = sMCL.getCodecInfos(); if (mciList == null) { Log.d(TAG, "did not get list of MediaCodecInfo"); return false; } MediaCodecInfo mci = null; for (MediaCodecInfo mci2 : mciList) { if (mci2.getName().equals(decoder)) { mci = mci2; break; } } if (mci == null) { return false; } if (!mci.getName().equals(decoder)) { Log.e(TAG, "did not find expected " + decoder); return false; } if (ApiLevelUtil.isAtLeast(Build.VERSION_CODES.Q) && PropertyUtil.isVendorApiLevelAtLeast(Build.VERSION_CODES.Q) && mci.isHardwareAccelerated()) { MediaCodecInfo.VideoCapabilities caps = mci.getCapabilitiesForType(mime).getVideoCapabilities(); List pp = caps.getSupportedPerformancePoints(); VideoCapabilities.PerformancePoint target = new VideoCapabilities.PerformancePoint(width, height, (int) rate); for (MediaCodecInfo.VideoCapabilities.PerformancePoint point : pp) { if (point.covers(target)) { Log.i(TAG, "target " + target.toString() + " covered by point " + point.toString()); return true; } } Log.i(TAG, "NOT covered by any hardware performance point"); return false; } else { String verified = MediaPerfUtils.areAchievableFrameRates( decoder, mime, width, height, rate); if (verified == null) { Log.d(TAG, "claims to decode content at " + rate + " fps"); return true; } Log.d(TAG, "achieveable framerates says: " + verified); return false; } } 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; } catch (NullPointerException e) { Log.w(TAG, "codec name is null"); 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(final String resource, int track, String mimePrefix) { try { AssetFileDescriptor afd = null; MediaExtractor ex = null; try { ex = new MediaExtractor(); ex.setDataSource(resource); 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; } 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"); } public static boolean hasCodecsForResource(final String resource) { return hasCodecsForResourceCombo(resource, ALL_AV_TRACKS, null /* mimePrefix */); } public static boolean checkCodecsForResource(final String resource) { return check(hasCodecsForResource(resource), "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"); } public static boolean hasCodecForResource(final String resource, int track) { return hasCodecsForResourceCombo(resource, track, null /* mimePrefix */); } public static boolean checkCodecForResource(final String resource, int track) { return check(hasCodecForResource(resource, 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); } public static boolean hasCodecForResourceAndDomain(String resource, String mimePrefix) { return hasCodecsForResourceCombo(resource, 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"); } // checks format, does not address actual speed of decoding public static boolean canDecodeVideo(String mime, int width, int height, float rate) { return canDecodeVideo(mime, width, height, rate, (float)0.0); } // format + decode rate public static boolean canDecodeVideo(String mime, int width, int height, float rate, float decodeRate) { MediaFormat format = MediaFormat.createVideoFormat(mime, width, height); format.setFloat(MediaFormat.KEY_FRAME_RATE, rate); return canDecode(format, decodeRate); } public static boolean canDecodeVideo( String mime, int width, int height, float rate, Integer profile, Integer level, Integer bitrate) { return canDecodeVideo(mime, width, height, rate, profile, level, bitrate, (float)0.0); } public static boolean canDecodeVideo( String mime, int width, int height, float rate, Integer profile, Integer level, Integer bitrate, float decodeRate) { 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, decodeRate); } 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 getTrackFormatForResource( final String resource, String mimeTypePrefix) throws IOException { MediaExtractor extractor = new MediaExtractor(); try { extractor.setDataSource(resource); } catch (IOException e) { e.printStackTrace(); } 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, String resource, String mimeTypePrefix) throws IOException { MediaExtractor extractor = new MediaExtractor(); File inpFile = new File(resource); ParcelFileDescriptor parcelFD = ParcelFileDescriptor.open(inpFile, ParcelFileDescriptor.MODE_READ_ONLY); AssetFileDescriptor afd = new AssetFileDescriptor(parcelFD, 0, parcelFD.getStatSize()); 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); } public static boolean hasHardwareCodec(String mime, boolean encode) { for (MediaCodecInfo info : sMCL.getCodecInfos()) { if (info.isEncoder() == encode && info.isHardwareAccelerated()) { try { if (info.getCapabilitiesForType(mime) != null) { return true; } } catch (IllegalArgumentException e) { // mime is not supported Log.w(TAG, "not supported mime: " + mime); } } } return false; } /* * ------------------ 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------> | // <---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 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"); Rect crop = image.getCropRect(); int cropLeft = crop.left; int cropRight = crop.right; int cropTop = crop.top; int cropBottom = crop.bottom; int imageWidth = cropRight - cropLeft; int imageHeight = cropBottom - cropTop; 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, top, left; rowStride = planes[i].getRowStride(); pixelStride = planes[i].getPixelStride(); if (i == 0) { width = imageWidth; height = imageHeight; left = cropLeft; top = cropTop; } else { width = imageWidth / 2; height = imageHeight /2; left = cropLeft / 2; top = cropTop / 2; } // local contiguous pixel buffer byte[] bb = new byte[width * height]; if (buf.hasArray()) { byte b[] = buf.array(); int offs = buf.arrayOffset() + left * pixelStride; if (pixelStride == 1) { for (y = 0; y < height; ++y) { System.arraycopy(bb, y * width, b, (top + y) * rowStride + offs, width); } } else { // do it pixel-by-pixel for (y = 0; y < height; ++y) { int lineOffset = offs + (top + 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 + left + (top + 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 + left * pixelStride + (top + 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(); } /* * ------------------- HELPER METHODS FOR DETECTING DEVICE TYPES ------------------- */ public static boolean hasDeviceGotBattery() { final Intent batteryInfo = mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); return batteryInfo.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true); } public static double getScreenSizeInInches() { DisplayMetrics dm = mContext.getResources().getDisplayMetrics(); double widthInInchesSquared = Math.pow(dm.widthPixels/dm.xdpi,2); double heightInInchesSquared = Math.pow(dm.heightPixels/dm.ydpi,2); double diagonalInInches = Math.sqrt(widthInInchesSquared + heightInInchesSquared); return diagonalInInches; } public static boolean isTv() { return pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) || pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION); } public static boolean hasMicrophone() { return pm.hasSystemFeature(PackageManager.FEATURE_MICROPHONE); } public static boolean hasCamera() { return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); } public static boolean isWatch() { return pm.hasSystemFeature(PackageManager.FEATURE_WATCH); } public static boolean isAutomotive() { return pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); } public static boolean isPc() { return pm.hasSystemFeature(PackageManager.FEATURE_PC); } public static boolean hasAudioOutput() { return pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT); } public static boolean isHandheld() { double screenSize = getScreenSizeInInches(); if (screenSize < (FIRST_SDK_IS_AT_LEAST_R ? 3.3 : 2.5)) return false; if (screenSize > 8.0) return false; if (!hasDeviceGotBattery()) return false; // handheld nature is not exposed to package manager, so for now, // in addition to physical screen size, the following checks are // also required: if (!pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) return false; if (isWatch()) return false; if (isTv()) return false; if (isAutomotive()) return false; if (isPc()) return false; return true; } public static boolean isTablet() { double screenSize = getScreenSizeInInches(); if (screenSize < 7.0) return false; if (screenSize > 18.0) return false; if (!hasDeviceGotBattery()) return false; // tablet nature is not exposed to package manager, so for now, // in addition to physical screen size, the following checks are // also required: if (!pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) return false; if (isWatch()) return false; if (isTv()) return false; if (isAutomotive()) return false; if (isPc()) return false; return true; } /* * ------------------- HELPER METHODS FOR DETECTING NON-PRODUCTION DEVICES ------------------- */ /* * Some parts of media CTS verifies device characterization that does not make sense for * non-production devices (such as GSI and cuttlefish). We call these devices 'frankenDevices'. * We may also limit test duration on these devices. */ public static boolean onFrankenDevice() throws IOException { String systemBrand = PropertyUtil.getProperty("ro.product.system.brand"); String systemModel = PropertyUtil.getProperty("ro.product.system.model"); String systemProduct = PropertyUtil.getProperty("ro.product.system.name"); // not all devices may have system_ext partition, but if they do use that { String systemExtProduct = PropertyUtil.getProperty("ro.product.system_ext.name"); if (systemExtProduct != null) { systemProduct = systemExtProduct; } String systemExtModel = PropertyUtil.getProperty("ro.product.system_ext.model"); if (systemExtModel != null) { systemModel = systemExtModel; } } if (("Android".equals(systemBrand) || "generic".equals(systemBrand) || "mainline".equals(systemBrand)) && (systemModel.startsWith("AOSP on ") || systemProduct.startsWith("aosp_") || systemModel.startsWith("GSI on ") || systemProduct.startsWith("gsi_"))) { return true; } // Return true for cuttlefish instances if ((systemBrand.equals("Android") || systemBrand.equals("google")) && (systemProduct.startsWith("cf_") || systemProduct.startsWith("aosp_cf_") || systemModel.startsWith("Cuttlefish "))) { return true; } return false; } /* * -------------------------------------- END -------------------------------------- */ }