blob: e777528481ade82d1b44c43c1802b953fc51d39a [file] [log] [blame]
/*
* Copyright (C) 2012 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.media.cts;
import android.content.Context;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.media.cts.R;
import android.platform.test.annotations.RequiresDevice;
import android.test.AndroidTestCase;
import android.util.Log;
import androidx.test.filters.SmallTest;
import com.android.compatibility.common.util.MediaUtils;
import java.io.File;
import java.io.InputStream;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@SmallTest
@RequiresDevice
public class EncoderTest extends AndroidTestCase {
private static final String TAG = "EncoderTest";
private static final boolean VERBOSE = false;
private static final int kNumInputBytes = 512 * 1024;
private static final long kTimeoutUs = 100;
// not all combinations are valid
private static final int MODE_SILENT = 0;
private static final int MODE_RANDOM = 1;
private static final int MODE_RESOURCE = 2;
private static final int MODE_QUIET = 4;
private static final int MODE_SILENTLEAD = 8;
/*
* Set this to true to save the encoding results to /data/local/tmp
* You will need to make /data/local/tmp writeable, run "setenforce 0",
* and remove files left from a previous run.
*/
private static boolean sSaveResults = false;
@Override
public void setContext(Context context) {
super.setContext(context);
}
public void testAMRNBEncoders() {
LinkedList<MediaFormat> formats = new LinkedList<MediaFormat>();
final int kBitRates[] =
{ 4750, 5150, 5900, 6700, 7400, 7950, 10200, 12200 };
for (int j = 0; j < kBitRates.length; ++j) {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_AMR_NB);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 8000);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
format.setInteger(MediaFormat.KEY_BIT_RATE, kBitRates[j]);
formats.push(format);
}
testEncoderWithFormats(MediaFormat.MIMETYPE_AUDIO_AMR_NB, formats);
}
public void testAMRWBEncoders() {
LinkedList<MediaFormat> formats = new LinkedList<MediaFormat>();
final int kBitRates[] =
{ 6600, 8850, 12650, 14250, 15850, 18250, 19850, 23050, 23850 };
for (int j = 0; j < kBitRates.length; ++j) {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_AMR_WB);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 16000);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
format.setInteger(MediaFormat.KEY_BIT_RATE, kBitRates[j]);
formats.push(format);
}
testEncoderWithFormats(MediaFormat.MIMETYPE_AUDIO_AMR_WB, formats);
}
public void testOpusEncoders() {
LinkedList<MediaFormat> formats = new LinkedList<MediaFormat>();
final int kBitRates[] =
{ 6600, 8850, 12650, 14250, 15850, 18250, 19850, 23050, 23850 };
for (int j = 0; j < kBitRates.length; ++j) {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_OPUS);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 16000);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
format.setInteger(MediaFormat.KEY_BIT_RATE, kBitRates[j]);
formats.push(format);
}
testEncoderWithFormats(MediaFormat.MIMETYPE_AUDIO_OPUS, formats);
}
public void testAACEncoders() {
LinkedList<MediaFormat> formats = new LinkedList<MediaFormat>();
final int kAACProfiles[] = {
2 /* OMX_AUDIO_AACObjectLC */,
5 /* OMX_AUDIO_AACObjectHE */,
39 /* OMX_AUDIO_AACObjectELD */
};
final int kSampleRates[] = { 8000, 11025, 22050, 44100, 48000 };
final int kBitRates[] = { 64000, 128000 };
for (int k = 0; k < kAACProfiles.length; ++k) {
for (int i = 0; i < kSampleRates.length; ++i) {
if (kAACProfiles[k] == 5 && kSampleRates[i] < 22050) {
// Is this right? HE does not support sample rates < 22050Hz?
continue;
}
for (int j = 0; j < kBitRates.length; ++j) {
for (int ch = 1; ch <= 2; ++ch) {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_AAC);
format.setInteger(
MediaFormat.KEY_AAC_PROFILE, kAACProfiles[k]);
format.setInteger(
MediaFormat.KEY_SAMPLE_RATE, kSampleRates[i]);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, ch);
format.setInteger(MediaFormat.KEY_BIT_RATE, kBitRates[j]);
formats.push(format);
}
}
}
}
testEncoderWithFormats(MediaFormat.MIMETYPE_AUDIO_AAC, formats);
}
private void testEncoderWithFormats(
String mime, List<MediaFormat> formatList) {
MediaFormat[] formats = formatList.toArray(new MediaFormat[formatList.size()]);
String[] componentNames = MediaUtils.getEncoderNames(formats);
if (componentNames.length == 0) {
MediaUtils.skipTest("no encoders found for " + Arrays.toString(formats));
return;
}
int ThreadCount = 3;
int testsStarted = 0;
int allowPerTest = 30;
ExecutorService pool = Executors.newFixedThreadPool(ThreadCount);
for (String componentName : componentNames) {
for (MediaFormat format : formats) {
assertEquals(mime, format.getString(MediaFormat.KEY_MIME));
pool.execute(new EncoderRun(componentName, format));
testsStarted++;
}
}
try {
pool.shutdown();
int waitingSeconds = ((testsStarted + ThreadCount - 1) / ThreadCount) * allowPerTest;
waitingSeconds += 300;
Log.i(TAG, "waiting up to " + waitingSeconds + " seconds for "
+ testsStarted + " sub-tests to finish");
assertTrue("timed out waiting for encoder threads",
pool.awaitTermination(waitingSeconds, TimeUnit.SECONDS));
} catch (InterruptedException e) {
fail("interrupted while waiting for encoder threads");
}
}
// See bug 25843966
private long[] mBadSeeds = {
101833462733980l, // fail @ 23680 in all-random mode
273262699095706l, // fail @ 58880 in all-random mode
137295510492957l, // fail @ 35840 in zero-lead mode
57821391502855l, // fail @ 32000 in zero-lead mode
};
private int queueInputBuffer(
MediaCodec codec, ByteBuffer[] inputBuffers, int index,
InputStream istream, int mode, long timeUs, Random random) {
ByteBuffer buffer = inputBuffers[index];
buffer.rewind();
int size = buffer.limit();
if ((mode & MODE_RESOURCE) != 0 && istream != null) {
while (buffer.hasRemaining()) {
try {
int next = istream.read();
if (next < 0) {
break;
}
buffer.put((byte) next);
} catch (Exception ex) {
Log.i(TAG, "caught exception writing: " + ex);
break;
}
}
} else if ((mode & MODE_RANDOM) != 0) {
if ((mode & MODE_SILENTLEAD) != 0) {
buffer.putInt(0);
buffer.putInt(0);
buffer.putInt(0);
buffer.putInt(0);
}
while (true) {
try {
int next = random.nextInt();
buffer.putInt(random.nextInt());
} catch (BufferOverflowException ex) {
break;
}
}
} else {
byte[] zeroes = new byte[size];
buffer.put(zeroes);
}
if ((mode & MODE_QUIET) != 0) {
int n = buffer.limit();
for (int i = 0; i < n; i += 2) {
short s = buffer.getShort(i);
s /= 8;
buffer.putShort(i, s);
}
}
codec.queueInputBuffer(index, 0 /* offset */, size, timeUs, 0 /* flags */);
return size;
}
private void dequeueOutputBuffer(
MediaCodec codec, ByteBuffer[] outputBuffers,
int index, MediaCodec.BufferInfo info) {
codec.releaseOutputBuffer(index, false /* render */);
}
class EncoderRun implements Runnable {
String mComponentName;
MediaFormat mFormat;
EncoderRun(String componentName, MediaFormat format) {
mComponentName = componentName;
mFormat = format;
}
@Override
public void run() {
testEncoder(mComponentName, mFormat);
}
}
private void testEncoder(String componentName, MediaFormat format) {
Log.i(TAG, "testEncoder " + componentName + "/" + format);
// test with all zeroes/silence
testEncoder(componentName, format, 0, -1, MODE_SILENT);
// test with pcm input file
testEncoder(componentName, format, 0, R.raw.okgoogle123_good, MODE_RESOURCE);
testEncoder(componentName, format, 0, R.raw.okgoogle123_good, MODE_RESOURCE | MODE_QUIET);
testEncoder(componentName, format, 0, R.raw.tones, MODE_RESOURCE);
testEncoder(componentName, format, 0, R.raw.tones, MODE_RESOURCE | MODE_QUIET);
// test with random data, with and without a few leading zeroes
for (int i = 0; i < mBadSeeds.length; i++) {
testEncoder(componentName, format, mBadSeeds[i], -1, MODE_RANDOM);
testEncoder(componentName, format, mBadSeeds[i], -1, MODE_RANDOM | MODE_SILENTLEAD);
}
}
private void testEncoder(String componentName, MediaFormat format,
long startSeed, int resid, int mode) {
Log.i(TAG, "testEncoder " + componentName + "/" + mode + "/" + format);
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int inBitrate = sampleRate * channelCount * 16; // bit/sec
int outBitrate = format.getInteger(MediaFormat.KEY_BIT_RATE);
MediaMuxer muxer = null;
int muxidx = -1;
if (sSaveResults) {
try {
String outFile = "/data/local/tmp/transcoded-" + componentName +
"-" + sampleRate + "Hz-" + channelCount + "ch-" + outBitrate +
"bps-" + mode + "-" + resid + "-" + startSeed + "-" +
(android.os.Process.is64Bit() ? "64bit" : "32bit") + ".mp4";
new File("outFile").delete();
muxer = new MediaMuxer(outFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// The track can't be added until we have the codec specific data
} catch (Exception e) {
Log.i(TAG, "couldn't create muxer: " + e);
}
}
InputStream istream = null;
if ((mode & MODE_RESOURCE) != 0) {
istream = mContext.getResources().openRawResource(resid);
}
Random random = new Random(startSeed);
MediaCodec codec;
try {
codec = MediaCodec.createByCodecName(componentName);
} catch (Exception e) {
fail("codec '" + componentName + "' failed construction.");
return; /* does not get here, but avoids warning */
}
try {
codec.configure(
format,
null /* surface */,
null /* crypto */,
MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (IllegalStateException e) {
fail("codec '" + componentName + "' failed configuration.");
}
codec.start();
ByteBuffer[] codecInputBuffers = codec.getInputBuffers();
ByteBuffer[] codecOutputBuffers = codec.getOutputBuffers();
int numBytesSubmitted = 0;
boolean doneSubmittingInput = false;
int numBytesDequeued = 0;
while (true) {
int index;
if (!doneSubmittingInput) {
index = codec.dequeueInputBuffer(kTimeoutUs /* timeoutUs */);
if (index != MediaCodec.INFO_TRY_AGAIN_LATER) {
long timeUs =
(long)numBytesSubmitted * 1000000 / (2 * channelCount * sampleRate);
if (numBytesSubmitted >= kNumInputBytes) {
codec.queueInputBuffer(
index,
0 /* offset */,
0 /* size */,
timeUs,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
if (VERBOSE) {
Log.d(TAG, "queued input EOS.");
}
doneSubmittingInput = true;
} else {
int size = queueInputBuffer(
codec, codecInputBuffers, index, istream, mode, timeUs, random);
numBytesSubmitted += size;
if (VERBOSE) {
Log.d(TAG, "queued " + size + " bytes of input data.");
}
}
}
}
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
index = codec.dequeueOutputBuffer(info, kTimeoutUs /* timeoutUs */);
if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {
} else if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
} else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
codecOutputBuffers = codec.getOutputBuffers();
} else {
if (muxer != null) {
ByteBuffer buffer = codec.getOutputBuffer(index);
if (muxidx < 0) {
MediaFormat trackFormat = codec.getOutputFormat();
muxidx = muxer.addTrack(trackFormat);
muxer.start();
}
muxer.writeSampleData(muxidx, buffer, info);
}
dequeueOutputBuffer(codec, codecOutputBuffers, index, info);
numBytesDequeued += info.size;
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
if (VERBOSE) {
Log.d(TAG, "dequeued output EOS.");
}
break;
}
if (VERBOSE) {
Log.d(TAG, "dequeued " + info.size + " bytes of output data.");
}
}
}
if (VERBOSE) {
Log.d(TAG, "queued a total of " + numBytesSubmitted + "bytes, "
+ "dequeued " + numBytesDequeued + " bytes.");
}
float desiredRatio = (float)outBitrate / (float)inBitrate;
float actualRatio = (float)numBytesDequeued / (float)numBytesSubmitted;
if (actualRatio < 0.9 * desiredRatio || actualRatio > 1.1 * desiredRatio) {
Log.w(TAG, "desiredRatio = " + desiredRatio
+ ", actualRatio = " + actualRatio);
}
codec.release();
codec = null;
if (muxer != null) {
muxer.stop();
muxer.release();
muxer = null;
}
}
}