| /* |
| * Copyright (C) 2021 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 android.video.cts; |
| |
| import android.media.MediaCodec; |
| import android.media.MediaCodecInfo; |
| import android.media.MediaCodecList; |
| import android.media.MediaExtractor; |
| import android.media.MediaFormat; |
| import android.os.Build; |
| import android.os.SystemProperties; |
| import android.util.Range; |
| import android.view.Surface; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.junit.Before; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| import static org.junit.Assume.assumeTrue; |
| |
| class CodecPerformanceTestBase { |
| private static final String LOG_TAG = CodecPerformanceTestBase.class.getSimpleName(); |
| static final long Q_DEQ_TIMEOUT_US = 5000; // block at most 5ms while looking for io buffers |
| static final int PER_TEST_TIMEOUT_LARGE_TEST_MS = 300000; |
| static final int MIN_FRAME_COUNT = 500; |
| static final int SELECT_ALL = 0; // Select all codecs |
| static final int SELECT_HARDWARE = 1; // Select Hardware codecs only |
| static final int SELECT_SOFTWARE = 2; // Select Software codecs only |
| // allowed tolerance in measured fps vs expected fps, i.e. codecs achieving fps |
| // that is greater than (FPS_TOLERANCE_FACTOR * expectedFps) will be considered as |
| // passing the test |
| static final double FPS_TOLERANCE_FACTOR; |
| static final boolean IS_AT_LEAST_VNDK_S; |
| |
| static final int DEVICE_INITIAL_SDK; |
| |
| // Some older devices can not support concurrent instances of both decoder and encoder |
| // at max resolution. To handle such cases, this test is limited to test the |
| // resolutions that are less than half of max supported frame sizes of encoder. |
| static final boolean EXCLUDE_ENCODER_MAX_RESOLUTION; |
| |
| // Some older devices can not support concurrent instances of both decoder and encoder |
| // for operating rates > 0 and < 30 for resolutions 4k |
| static final boolean EXCLUDE_ENCODER_OPRATE_0_TO_30_FOR_4K; |
| |
| static final String mInputPrefix = WorkDir.getMediaDirString(); |
| |
| ArrayList<MediaCodec.BufferInfo> mBufferInfos; |
| ByteBuffer mBuff; |
| |
| final String mDecoderName; |
| final String mTestFile; |
| final int mKeyPriority; |
| final float mMaxOpRateScalingFactor; |
| |
| String mDecoderMime; |
| int mWidth; |
| int mHeight; |
| int mFrameRate; |
| |
| boolean mSawDecInputEOS = false; |
| boolean mSawDecOutputEOS = false; |
| int mDecInputNum = 0; |
| int mDecOutputNum = 0; |
| int mSampleIndex = 0; |
| |
| MediaCodec mDecoder; |
| MediaFormat mDecoderFormat; |
| Surface mSurface; |
| double mOperatingRateExpected; |
| |
| static final float[] SCALING_FACTORS_LIST = new float[]{2.5f, 1.25f, 1.0f, 0.75f, 0.0f, -1.0f}; |
| static final int[] KEY_PRIORITIES_LIST = new int[]{1, 0}; |
| |
| static { |
| // os.Build.VERSION.DEVICE_INITIAL_SDK_INT can be used here, but it was called |
| // os.Build.VERSION.FIRST_SDK_INT in Android R and below. Using DEVICE_INITIAL_SDK_INT |
| // will mean that the tests built in Android S can't be run on Android R and below. |
| DEVICE_INITIAL_SDK = SystemProperties.getInt("ro.product.first_api_level", 0); |
| |
| // fps tolerance factor is kept quite low for devices launched on Android R and lower |
| FPS_TOLERANCE_FACTOR = DEVICE_INITIAL_SDK <= Build.VERSION_CODES.R ? 0.67 : 0.95; |
| |
| IS_AT_LEAST_VNDK_S = SystemProperties.getInt("ro.vndk.version", 0) > Build.VERSION_CODES.R; |
| |
| // Encoders on devices launched on Android Q and lower aren't tested at maximum resolution |
| EXCLUDE_ENCODER_MAX_RESOLUTION = DEVICE_INITIAL_SDK <= Build.VERSION_CODES.Q; |
| |
| // Encoders on devices launched on Android R and lower aren't tested when operating rate |
| // that is set is > 0 and < 30 for resolution 4k. |
| // This includes devices launched on Android S with R or lower vendor partition. |
| EXCLUDE_ENCODER_OPRATE_0_TO_30_FOR_4K = |
| !IS_AT_LEAST_VNDK_S || (DEVICE_INITIAL_SDK <= Build.VERSION_CODES.R); |
| } |
| |
| @Before |
| public void prologue() { |
| assumeTrue("For VNDK R and below, operating rate <= 0 isn't tested", |
| IS_AT_LEAST_VNDK_S || mMaxOpRateScalingFactor > 0.0); |
| |
| assumeTrue("For devices launched on Android P and below, operating rate tests are disabled", |
| DEVICE_INITIAL_SDK > Build.VERSION_CODES.P); |
| |
| if (DEVICE_INITIAL_SDK <= Build.VERSION_CODES.Q) { |
| assumeTrue("For devices launched with Android Q and below, operating rate tests are " + |
| "limited to operating rate scaling factor > 0.0 and <= 1.25", |
| mMaxOpRateScalingFactor > 0.0 && mMaxOpRateScalingFactor <= 1.25); |
| } |
| } |
| |
| public CodecPerformanceTestBase(String decoderName, String testFile, int keyPriority, |
| float maxOpRateScalingFactor) { |
| mDecoderName = decoderName; |
| mTestFile = testFile; |
| mKeyPriority = keyPriority; |
| mMaxOpRateScalingFactor = maxOpRateScalingFactor; |
| mBufferInfos = new ArrayList<>(); |
| } |
| |
| static MediaFormat getVideoFormat(String filePath) throws IOException { |
| final String input = mInputPrefix + filePath; |
| MediaExtractor extractor = new MediaExtractor(); |
| extractor.setDataSource(input); |
| for (int trackID = 0; trackID < extractor.getTrackCount(); trackID++) { |
| MediaFormat format = extractor.getTrackFormat(trackID); |
| if (format.getString(MediaFormat.KEY_MIME).startsWith("video/")) { |
| extractor.release(); |
| return format; |
| } |
| } |
| extractor.release(); |
| return null; |
| } |
| |
| static ArrayList<String> selectCodecs(String mime, ArrayList<MediaFormat> formats, |
| String[] features, boolean isEncoder) { |
| return selectCodecs(mime, formats, features, isEncoder, SELECT_ALL); |
| } |
| |
| static ArrayList<String> selectHardwareCodecs(String mime, ArrayList<MediaFormat> formats, |
| String[] features, boolean isEncoder) { |
| return selectCodecs(mime, formats, features, isEncoder, SELECT_HARDWARE); |
| } |
| |
| static ArrayList<String> selectCodecs(String mime, ArrayList<MediaFormat> formats, |
| String[] features, boolean isEncoder, int selectCodecOption) { |
| MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS); |
| MediaCodecInfo[] codecInfos = codecList.getCodecInfos(); |
| ArrayList<String> listOfCodecs = new ArrayList<>(); |
| for (MediaCodecInfo codecInfo : codecInfos) { |
| if (codecInfo.isEncoder() != isEncoder) continue; |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && codecInfo.isAlias()) continue; |
| if (selectCodecOption == SELECT_HARDWARE && !codecInfo.isHardwareAccelerated()) |
| continue; |
| else if (selectCodecOption == SELECT_SOFTWARE && !codecInfo.isSoftwareOnly()) |
| continue; |
| String[] types = codecInfo.getSupportedTypes(); |
| for (String type : types) { |
| if (type.equalsIgnoreCase(mime)) { |
| boolean isOk = true; |
| MediaCodecInfo.CodecCapabilities codecCapabilities = |
| codecInfo.getCapabilitiesForType(type); |
| if (formats != null) { |
| for (MediaFormat format : formats) { |
| if (!codecCapabilities.isFormatSupported(format)) { |
| isOk = false; |
| break; |
| } |
| } |
| } |
| if (features != null) { |
| for (String feature : features) { |
| if (!codecCapabilities.isFeatureSupported(feature)) { |
| isOk = false; |
| break; |
| } |
| } |
| } |
| if (isOk) listOfCodecs.add(codecInfo.getName()); |
| } |
| } |
| } |
| return listOfCodecs; |
| } |
| |
| MediaFormat setUpDecoderInput() throws IOException { |
| final String input = mInputPrefix + mTestFile; |
| MediaExtractor extractor = new MediaExtractor(); |
| extractor.setDataSource(input); |
| for (int trackID = 0; trackID < extractor.getTrackCount(); trackID++) { |
| MediaFormat format = extractor.getTrackFormat(trackID); |
| if (format.getString(MediaFormat.KEY_MIME).startsWith("video/")) { |
| extractor.selectTrack(trackID); |
| File file = new File(input); |
| int bufferSize = (int) file.length(); |
| mBuff = ByteBuffer.allocate(bufferSize); |
| int offset = 0; |
| long maxPTS = 0; |
| while (true) { |
| MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); |
| bufferInfo.size = extractor.readSampleData(mBuff, offset); |
| if (bufferInfo.size < 0) break; |
| bufferInfo.offset = offset; |
| bufferInfo.presentationTimeUs = extractor.getSampleTime(); |
| maxPTS = Math.max(maxPTS, bufferInfo.presentationTimeUs); |
| int flags = extractor.getSampleFlags(); |
| bufferInfo.flags = 0; |
| if ((flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) { |
| bufferInfo.flags |= MediaCodec.BUFFER_FLAG_KEY_FRAME; |
| } |
| mBufferInfos.add(bufferInfo); |
| extractor.advance(); |
| offset += bufferInfo.size; |
| } |
| |
| // If the clip doesn't have sufficient frames, loopback by copying bufferInfos |
| // from the start of the list and incrementing the timestamp. |
| int actualBufferInfosCount = mBufferInfos.size(); |
| long ptsOffset; |
| while (mBufferInfos.size() < MIN_FRAME_COUNT) { |
| ptsOffset = maxPTS + 1000000L; |
| for (int i = 0; i < actualBufferInfosCount; i++) { |
| MediaCodec.BufferInfo tmpBufferInfo = mBufferInfos.get(i); |
| MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); |
| bufferInfo.set(tmpBufferInfo.offset, tmpBufferInfo.size, |
| ptsOffset + tmpBufferInfo.presentationTimeUs, |
| tmpBufferInfo.flags); |
| maxPTS = Math.max(maxPTS, bufferInfo.presentationTimeUs); |
| mBufferInfos.add(bufferInfo); |
| if (mBufferInfos.size() >= MIN_FRAME_COUNT) break; |
| } |
| } |
| MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); |
| bufferInfo.set(0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); |
| mBufferInfos.add(bufferInfo); |
| mDecoderMime = format.getString(MediaFormat.KEY_MIME); |
| mWidth = format.getInteger(MediaFormat.KEY_WIDTH); |
| mHeight = format.getInteger(MediaFormat.KEY_HEIGHT); |
| mFrameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE, 30); |
| extractor.release(); |
| return format; |
| } |
| } |
| extractor.release(); |
| fail("No video track found in file: " + mTestFile); |
| return null; |
| } |
| |
| // TODO (b/193458026) Limit max expected fps |
| static int getMaxExpectedFps(int width, int height) { |
| int numSamples = width * height; |
| if (numSamples > 3840 * 2160 * 2) { // 8K |
| return 30; |
| } else if (numSamples > 1920 * 1088 * 2) { // 4K |
| return 120; |
| } else { |
| return 240; |
| } |
| } |
| |
| int getMaxOperatingRate(String codecName, String mime) throws IOException { |
| MediaCodec codec = MediaCodec.createByCodecName(codecName); |
| MediaCodecInfo mediaCodecInfo = codec.getCodecInfo(); |
| List<MediaCodecInfo.VideoCapabilities.PerformancePoint> pps = mediaCodecInfo |
| .getCapabilitiesForType(mime).getVideoCapabilities() |
| .getSupportedPerformancePoints(); |
| assertTrue(pps.size() > 0); |
| MediaCodecInfo.VideoCapabilities.PerformancePoint cpp = |
| new MediaCodecInfo.VideoCapabilities.PerformancePoint(mWidth, mHeight, mFrameRate); |
| int macroblocks = cpp.getMaxMacroBlocks(); |
| int maxOperatingRate = -1; |
| for (MediaCodecInfo.VideoCapabilities.PerformancePoint pp : pps) { |
| if (pp.covers(cpp)) { |
| maxOperatingRate = Math.max(Math.min(pp.getMaxFrameRate(), |
| (int) pp.getMaxMacroBlockRate() / macroblocks), maxOperatingRate); |
| } |
| } |
| codec.release(); |
| assertTrue(maxOperatingRate != -1); |
| return maxOperatingRate; |
| } |
| |
| int getEncoderMinComplexity(String codecName, String mime) throws IOException { |
| MediaCodec codec = MediaCodec.createByCodecName(codecName); |
| MediaCodecInfo mediaCodecInfo = codec.getCodecInfo(); |
| int minComplexity = -1; |
| if (mediaCodecInfo.isEncoder()) { |
| Range<Integer> complexityRange = mediaCodecInfo |
| .getCapabilitiesForType(mime).getEncoderCapabilities() |
| .getComplexityRange(); |
| minComplexity = complexityRange.getLower(); |
| } |
| codec.release(); |
| return minComplexity; |
| } |
| |
| static int getMaxFrameSize(String codecName, String mime) throws IOException { |
| MediaCodec codec = MediaCodec.createByCodecName(codecName); |
| MediaCodecInfo.CodecCapabilities codecCapabilities = |
| codec.getCodecInfo().getCapabilitiesForType(mime); |
| MediaCodecInfo.VideoCapabilities vc = codecCapabilities.getVideoCapabilities(); |
| Range<Integer> heights = vc.getSupportedHeights(); |
| Range<Integer> widths = vc.getSupportedWidthsFor(heights.getUpper()); |
| int maxFrameSize = heights.getUpper() * widths.getUpper(); |
| codec.release(); |
| return maxFrameSize; |
| } |
| |
| void enqueueDecoderInput(int bufferIndex) { |
| MediaCodec.BufferInfo info = mBufferInfos.get(mSampleIndex++); |
| if (info.size > 0 && (info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) { |
| ByteBuffer dstBuf = mDecoder.getInputBuffer(bufferIndex); |
| dstBuf.put(mBuff.array(), info.offset, info.size); |
| mDecInputNum++; |
| } |
| if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { |
| mSawDecInputEOS = true; |
| } |
| mDecoder.queueInputBuffer(bufferIndex, 0, info.size, info.presentationTimeUs, info.flags); |
| } |
| |
| void dequeueDecoderOutput(int bufferIndex, MediaCodec.BufferInfo info, boolean render) { |
| if (info.size > 0) { |
| mDecOutputNum++; |
| } |
| if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { |
| mSawDecOutputEOS = true; |
| } |
| mDecoder.releaseOutputBuffer(bufferIndex, render); |
| } |
| } |