Benchmark: Add SDK Decoder

Test: adb shell am instrument -w -r -e class\
      'com.android.media.benchmark.tests.DecoderTest'\
      com.android.media.benchmark/androidx.test.runner.AndroidJUnitRunner

Bug: 140051680
Change-Id: Iba07810fa36a4ad378a7a91bfa8b1bca0f615108
gugelfrei
Shivaansh Agrawal 5 years ago
parent 1f72838954
commit fcf3a3c834

@ -43,4 +43,8 @@ android_library {
srcs: ["src/main/**/*.java"],
sdk_version: "system_current",
static_libs: [
"androidx.test.core",
],
}

@ -1,3 +1,4 @@
<resources>
<string name="input_file_path">/data/local/tmp/MediaBenchmark/res/</string>
</resources>
<string name="output_file_path">/data/local/tmp/MediaBenchmark/output/</string>
</resources>

@ -0,0 +1,197 @@
/*
* Copyright (C) 2019 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.media.benchmark.tests;
import android.content.Context;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.media.benchmark.R;
import com.android.media.benchmark.library.CodecUtils;
import com.android.media.benchmark.library.Decoder;
import com.android.media.benchmark.library.Extractor;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@RunWith(Parameterized.class)
public class DecoderTest {
private static final Context mContext =
InstrumentationRegistry.getInstrumentation().getTargetContext();
private static final String mInputFilePath = mContext.getString(R.string.input_file_path);
private static final String mOutputFilePath = mContext.getString(R.string.output_file_path);
private static final String TAG = "DecoderTest";
private static final long PER_TEST_TIMEOUT_MS = 60000;
private static final boolean DEBUG = false;
private static final boolean WRITE_OUTPUT = false;
private String mInputFile;
private boolean mAsyncMode;
public DecoderTest(String inputFile, boolean asyncMode) {
this.mInputFile = inputFile;
this.mAsyncMode = asyncMode;
}
@Parameterized.Parameters
public static Collection<Object[]> input() {
return Arrays.asList(new Object[][]{
//Audio Sync Test
{"bbb_44100hz_2ch_128kbps_aac_30sec.mp4", false},
{"bbb_44100hz_2ch_128kbps_mp3_30sec.mp3", false},
{"bbb_8000hz_1ch_8kbps_amrnb_30sec.3gp", false},
{"bbb_16000hz_1ch_9kbps_amrwb_30sec.3gp", false},
{"bbb_44100hz_2ch_80kbps_vorbis_30sec.mp4", false},
{"bbb_44100hz_2ch_600kbps_flac_30sec.mp4", false},
{"bbb_48000hz_2ch_100kbps_opus_30sec.webm", false},
// Audio Async Test
{"bbb_44100hz_2ch_128kbps_aac_30sec.mp4", true},
{"bbb_44100hz_2ch_128kbps_mp3_30sec.mp3", true},
{"bbb_8000hz_1ch_8kbps_amrnb_30sec.3gp", true},
{"bbb_16000hz_1ch_9kbps_amrwb_30sec.3gp", true},
{"bbb_44100hz_2ch_80kbps_vorbis_30sec.mp4", true},
{"bbb_44100hz_2ch_600kbps_flac_30sec.mp4", true},
{"bbb_48000hz_2ch_100kbps_opus_30sec.webm", true},
// Video Sync Test
{"crowd_1920x1080_25fps_4000kbps_vp9.webm", false},
{"crowd_1920x1080_25fps_4000kbps_vp8.webm", false},
{"crowd_1920x1080_25fps_4000kbps_av1.webm", false},
{"crowd_1920x1080_25fps_7300kbps_mpeg2.mp4", false},
{"crowd_1920x1080_25fps_6000kbps_mpeg4.mp4", false},
{"crowd_352x288_25fps_6000kbps_h263.3gp", false},
{"crowd_1920x1080_25fps_6700kbps_h264.ts", false},
{"crowd_1920x1080_25fps_4000kbps_h265.mkv", false},
// Video Async Test
{"crowd_1920x1080_25fps_4000kbps_vp9.webm", true},
{"crowd_1920x1080_25fps_4000kbps_vp8.webm", true},
{"crowd_1920x1080_25fps_4000kbps_av1.webm", true},
{"crowd_1920x1080_25fps_7300kbps_mpeg2.mp4", true},
{"crowd_1920x1080_25fps_6000kbps_mpeg4.mp4", true},
{"crowd_352x288_25fps_6000kbps_h263.3gp", true},
{"crowd_1920x1080_25fps_6700kbps_h264.ts", true},
{"crowd_1920x1080_25fps_4000kbps_h265.mkv", true}});
}
@Test(timeout = PER_TEST_TIMEOUT_MS)
public void testDecoder() throws IOException {
File inputFile = new File(mInputFilePath + mInputFile);
if (inputFile.exists()) {
FileInputStream fileInput = new FileInputStream(inputFile);
FileDescriptor fileDescriptor = fileInput.getFD();
Extractor extractor = new Extractor();
int trackCount = extractor.setUpExtractor(fileDescriptor);
ArrayList<ByteBuffer> inputBuffer = new ArrayList<>();
ArrayList<MediaCodec.BufferInfo> frameInfo = new ArrayList<>();
if (trackCount <= 0) {
Log.e(TAG, "Extraction failed. No tracks for file: " + mInputFile);
return;
}
for (int currentTrack = 0; currentTrack < trackCount; currentTrack++) {
extractor.selectExtractorTrack(currentTrack);
MediaFormat format = extractor.getFormat(currentTrack);
String mime = format.getString(MediaFormat.KEY_MIME);
ArrayList<String> mediaCodecs = CodecUtils.selectCodecs(mime, false);
if (mediaCodecs.size() <= 0) {
Log.e(TAG,
"No suitable codecs found for file: " + mInputFile
+ " track : " + currentTrack + " mime: " + mime);
continue;
}
// Get samples from extractor
int sampleSize;
do {
sampleSize = extractor.getFrameSample();
MediaCodec.BufferInfo bufInfo = new MediaCodec.BufferInfo();
MediaCodec.BufferInfo info = extractor.getBufferInfo();
ByteBuffer dataBuffer = ByteBuffer.allocate(info.size);
dataBuffer.put(extractor.getFrameBuffer().array(), 0, info.size);
bufInfo.set(info.offset, info.size, info.presentationTimeUs, info.flags);
inputBuffer.add(dataBuffer);
frameInfo.add(bufInfo);
if (DEBUG) {
Log.d(TAG,
"Extracted bufInfo: flag = " + bufInfo.flags + " timestamp = "
+ bufInfo.presentationTimeUs + " size = " + bufInfo.size);
}
} while (sampleSize > 0);
for (String codecName : mediaCodecs) {
FileOutputStream decodeOutputStream = null;
if (WRITE_OUTPUT) {
if (!Paths.get(mOutputFilePath).toFile().exists()) {
Files.createDirectories(Paths.get(mOutputFilePath));
}
File outFile = new File(mOutputFilePath + "decoder.out");
if (outFile.exists()) {
if (!outFile.delete()) {
Log.e(TAG, " Unable to delete existing file" + outFile.toString());
}
}
if (outFile.createNewFile()) {
decodeOutputStream = new FileOutputStream(outFile);
} else {
Log.e(TAG, "Unable to create file: " + outFile.toString());
}
}
Decoder decoder = new Decoder();
decoder.setupDecoder(decodeOutputStream);
int status =
decoder.decode(inputBuffer, frameInfo, mAsyncMode, format, codecName);
decoder.deInitCodec();
if (status == 0) {
decoder.dumpStatistics(
mInputFile + " " + codecName, extractor.getClipDuration());
Log.i(TAG,
"Decoding Successful for file: " + mInputFile
+ " with codec: " + codecName);
} else {
Log.e(TAG,
"Decoder returned error " + status + " for file: " + mInputFile
+ " with codec: " + codecName);
}
decoder.resetDecoder();
if (decodeOutputStream != null) {
decodeOutputStream.close();
}
}
extractor.unselectExtractorTrack(currentTrack);
inputBuffer.clear();
frameInfo.clear();
}
extractor.deinitExtractor();
fileInput.close();
} else {
Log.w(TAG,
"Warning: Test Skipped. Cannot find " + mInputFile + " in directory "
+ mInputFilePath);
}
}
}

@ -0,0 +1,39 @@
package com.android.media.benchmark.library;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.os.Build;
import java.util.ArrayList;
public class CodecUtils {
private CodecUtils() {}
/**
* Queries the MediaCodecList and returns codec names of supported codecs.
*
* @param mimeType Mime type of input
* @param isEncoder Specifies encoder or decoder
* @return ArrayList of codec names
*/
public static ArrayList<String> selectCodecs(String mimeType, boolean isEncoder) {
MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
MediaCodecInfo[] codecInfos = codecList.getCodecInfos();
ArrayList<String> supportedCodecs = new ArrayList<>();
for (MediaCodecInfo codecInfo : codecInfos) {
if (isEncoder != codecInfo.isEncoder()) {
continue;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && codecInfo.isAlias()) {
continue;
}
String[] types = codecInfo.getSupportedTypes();
for (String type : types) {
if (type.equalsIgnoreCase(mimeType)) {
supportedCodecs.add(codecInfo.getName());
}
}
}
return supportedCodecs;
}
}

@ -0,0 +1,301 @@
/*
* Copyright (C) 2019 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.media.benchmark.library;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaFormat;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
public class Decoder {
private static final String TAG = "Decoder";
private static final boolean DEBUG = false;
private static final int kQueueDequeueTimeoutUs = 1000;
private final Object mLock = new Object();
private MediaCodec mCodec;
private ArrayList<BufferInfo> mInputBufferInfo;
private Stats mStats;
private boolean mSawInputEOS;
private boolean mSawOutputEOS;
private boolean mSignalledError;
private int mNumOutputFrame;
private int mIndex;
private ArrayList<ByteBuffer> mInputBuffer;
private FileOutputStream mOutputStream;
public Decoder() { mStats = new Stats(); }
/**
* Setup of decoder
*
* @param outputStream Will dump the output in this stream if not null.
*/
public void setupDecoder(FileOutputStream outputStream) {
mSignalledError = false;
mOutputStream = outputStream;
}
private MediaCodec createCodec(String codecName, MediaFormat format) throws IOException {
String mime = format.getString(MediaFormat.KEY_MIME);
try {
MediaCodec codec;
if (codecName.isEmpty()) {
Log.i(TAG, "File mime type: " + mime);
if (mime != null) {
codec = MediaCodec.createDecoderByType(mime);
Log.i(TAG, "Decoder created for mime type " + mime);
return codec;
} else {
Log.e(TAG, "Mime type is null, please specify a mime type to create decoder");
return null;
}
} else {
codec = MediaCodec.createByCodecName(codecName);
Log.i(TAG, "Decoder created with codec name: " + codecName + " mime: " + mime);
return codec;
}
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
Log.e(TAG, "Failed to create decoder for " + codecName + " mime:" + mime);
return null;
}
}
/**
* Decodes the given input buffer,
* provided valid list of buffer info and format are passed as inputs.
*
* @param inputBuffer Decode the provided list of ByteBuffers
* @param inputBufferInfo List of buffer info corresponding to provided input buffers
* @param asyncMode Will run on async implementation if true
* @param format For creating the decoder if codec name is empty and configuring it
* @param codecName Will create the decoder with codecName
* @return 0 if decode was successful , -1 for fail, -2 for decoder not created
* @throws IOException if the codec cannot be created.
*/
public int decode(@NonNull ArrayList<ByteBuffer> inputBuffer,
@NonNull ArrayList<BufferInfo> inputBufferInfo, final boolean asyncMode,
@NonNull MediaFormat format, String codecName) throws IOException {
mInputBuffer = new ArrayList<>(inputBuffer.size());
mInputBuffer.addAll(inputBuffer);
mInputBufferInfo = new ArrayList<>(inputBufferInfo.size());
mInputBufferInfo.addAll(inputBufferInfo);
mSawInputEOS = false;
mSawOutputEOS = false;
mNumOutputFrame = 0;
mIndex = 0;
long sTime = mStats.getCurTime();
mCodec = createCodec(codecName, format);
if (mCodec == null) {
return -2;
}
if (asyncMode) {
mCodec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(
@NonNull MediaCodec mediaCodec, int inputBufferId) {
try {
mStats.addInputTime();
onInputAvailable(inputBufferId, mediaCodec);
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, e.toString());
}
}
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec,
int outputBufferId, @NonNull MediaCodec.BufferInfo bufferInfo) {
mStats.addOutputTime();
onOutputAvailable(mediaCodec, outputBufferId, bufferInfo);
if (mSawOutputEOS) {
Log.i(TAG, "Saw output EOS");
synchronized (mLock) { mLock.notify(); }
}
}
@Override
public void onOutputFormatChanged(
@NonNull MediaCodec mediaCodec, @NonNull MediaFormat format) {
Log.i(TAG, "Output format changed. Format: " + format.toString());
}
@Override
public void onError(
@NonNull MediaCodec mediaCodec, @NonNull MediaCodec.CodecException e) {
mSignalledError = true;
Log.e(TAG, "Codec Error: " + e.toString());
e.printStackTrace();
synchronized (mLock) { mLock.notify(); }
}
});
}
int isEncoder = 0;
if (DEBUG) {
Log.d(TAG, "Media Format : " + format.toString());
}
mCodec.configure(format, null, null, isEncoder);
mCodec.start();
Log.i(TAG, "Codec started ");
long eTime = mStats.getCurTime();
mStats.setInitTime(mStats.getTimeDiff(sTime, eTime));
mStats.setStartTime();
if (asyncMode) {
try {
synchronized (mLock) { mLock.wait(); }
if (mSignalledError) {
return -1;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
while (!mSawOutputEOS && !mSignalledError) {
/* Queue input data */
if (!mSawInputEOS) {
int inputBufferId = mCodec.dequeueInputBuffer(kQueueDequeueTimeoutUs);
if (inputBufferId < 0 && inputBufferId != MediaCodec.INFO_TRY_AGAIN_LATER) {
Log.e(TAG,
"MediaCodec.dequeueInputBuffer "
+ " returned invalid index : " + inputBufferId);
return -1;
}
mStats.addInputTime();
onInputAvailable(inputBufferId, mCodec);
}
/* Dequeue output data */
BufferInfo outputBufferInfo = new BufferInfo();
int outputBufferId =
mCodec.dequeueOutputBuffer(outputBufferInfo, kQueueDequeueTimeoutUs);
if (outputBufferId < 0) {
if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat outFormat = mCodec.getOutputFormat();
Log.i(TAG, "Output format changed. Format: " + outFormat.toString());
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
Log.i(TAG, "Ignoring deprecated flag: INFO_OUTPUT_BUFFERS_CHANGED");
} else if (outputBufferId != MediaCodec.INFO_TRY_AGAIN_LATER) {
Log.e(TAG,
"MediaCodec.dequeueOutputBuffer"
+ " returned invalid index " + outputBufferId);
return -1;
}
} else {
mStats.addOutputTime();
if (DEBUG) {
Log.d(TAG, "Dequeue O/P buffer with BufferID " + outputBufferId);
}
onOutputAvailable(mCodec, outputBufferId, outputBufferInfo);
}
if (outputBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
Log.i(TAG, "Saw output EOS");
}
}
}
mInputBuffer.clear();
mInputBufferInfo.clear();
return 0;
}
/**
* Stops the codec and releases codec resources.
*/
public void deInitCodec() {
long sTime = mStats.getCurTime();
if (mCodec != null) {
mCodec.stop();
mCodec.release();
mCodec = null;
}
long eTime = mStats.getCurTime();
mStats.setDeInitTime(mStats.getTimeDiff(sTime, eTime));
}
/**
* Prints out the statistics in the information log
*
* @param inputReference The operation being performed, in this case decode
* @param durationUs Duration of the clip in microseconds
*/
public void dumpStatistics(String inputReference, long durationUs) {
String operation = "decode";
mStats.dumpStatistics(operation, inputReference, durationUs);
}
/**
* Resets the stats
*/
public void resetDecoder() { mStats.reset(); }
private void onInputAvailable(int inputBufferId, MediaCodec mediaCodec) {
if ((inputBufferId >= 0) && !mSawInputEOS) {
ByteBuffer inputCodecBuffer = mediaCodec.getInputBuffer(inputBufferId);
BufferInfo bufInfo = mInputBufferInfo.get(mIndex);
inputCodecBuffer.put(mInputBuffer.get(mIndex).array());
mIndex++;
if (bufInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
mSawInputEOS = true;
Log.i(TAG, "Saw input EOS");
}
mStats.addFrameSize(bufInfo.size);
mediaCodec.queueInputBuffer(inputBufferId, bufInfo.offset, bufInfo.size,
bufInfo.presentationTimeUs, bufInfo.flags);
if (DEBUG) {
Log.d(TAG,
"Codec Input: "
+ "flag = " + bufInfo.flags + " timestamp = "
+ bufInfo.presentationTimeUs + " size = " + bufInfo.size);
}
}
}
private void onOutputAvailable(
MediaCodec mediaCodec, int outputBufferId, BufferInfo outputBufferInfo) {
if (mSawOutputEOS || outputBufferId < 0) {
return;
}
mNumOutputFrame++;
if (DEBUG) {
Log.d(TAG,
"In OutputBufferAvailable ,"
+ " output frame number = " + mNumOutputFrame);
}
if (mOutputStream != null) {
try {
ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
byte[] bytesOutput = new byte[outputBuffer.remaining()];
outputBuffer.get(bytesOutput);
mOutputStream.write(bytesOutput);
} catch (IOException e) {
e.printStackTrace();
Log.d(TAG, "Error Dumping File: Exception " + e.toString());
}
}
mediaCodec.releaseOutputBuffer(outputBufferId, false);
mSawOutputEOS = (outputBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM);
}
}

@ -96,3 +96,10 @@ The test extracts elementary stream and benchmarks the extractors available in S
```
adb shell am instrument -w -r -e class 'com.android.media.benchmark.tests.ExtractorTest' com.android.media.benchmark/androidx.test.runner.AndroidJUnitRunner
```
## Decoder
The test decodes input stream and benchmarks the decoders available in SDK.
```
adb shell am instrument -w -r -e class 'com.android.media.benchmark.tests.DecoderTest' com.android.media.benchmark/androidx.test.runner.AndroidJUnitRunner
```

Loading…
Cancel
Save