blob: b92b426246a6119fc1fc2b459d1450c0be8fc467 [file] [log] [blame]
/*
* Copyright (C) 2020 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.res.AssetFileDescriptor;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodec.CodecException;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.media.cts.R;
import android.platform.test.annotations.Presubmit;
import android.platform.test.annotations.RequiresDevice;
import android.test.AndroidTestCase;
import android.util.Log;
import android.view.Surface;
import androidx.test.filters.SmallTest;
import com.android.compatibility.common.util.MediaUtils;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
/**
* MediaCodec tests with CONFIGURE_FLAG_USE_BLOCK_MODEL.
*/
@Presubmit
@SmallTest
@RequiresDevice
public class MediaCodecBlockModelTest extends AndroidTestCase {
private static final String TAG = "MediaCodecBlockModelTest";
private static final boolean VERBOSE = false; // lots of logging
// H.264 Advanced Video Coding
private static final String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
private static final int APP_BUFFER_SIZE = 1024 * 1024; // 1 MB
// Input buffers from this input video are queued up to and including the video frame with
// timestamp LAST_BUFFER_TIMESTAMP_US.
private static final int INPUT_RESOURCE_ID =
R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz;
private static final long LAST_BUFFER_TIMESTAMP_US = 166666;
// The test should fail if the codec never produces output frames for the truncated input.
// Time out processing, as we have no way to query whether the decoder will produce output.
private static final int TIMEOUT_MS = 2000;
/**
* Tests whether decoding a short group-of-pictures succeeds. The test queues a few video frames
* then signals end-of-stream. The test fails if the decoder doesn't output the queued frames.
*/
public void testDecodeShortVideo() throws InterruptedException {
runThread(() -> runDecodeShortVideo(
INPUT_RESOURCE_ID,
LAST_BUFFER_TIMESTAMP_US,
true /* obtainBlockForEachBuffer */));
runThread(() -> runDecodeShortVideo(
INPUT_RESOURCE_ID,
LAST_BUFFER_TIMESTAMP_US,
false /* obtainBlockForEachBuffer */));
}
/**
* Tests whether decoding a short audio succeeds. The test queues a few audio frames
* then signals end-of-stream. The test fails if the decoder doesn't output the queued frames.
*/
public void testDecodeShortAudio() throws InterruptedException {
runThread(() -> runDecodeShortAudio(
INPUT_RESOURCE_ID,
LAST_BUFFER_TIMESTAMP_US,
true /* obtainBlockForEachBuffer */));
runThread(() -> runDecodeShortAudio(
INPUT_RESOURCE_ID,
LAST_BUFFER_TIMESTAMP_US,
false /* obtainBlockForEachBuffer */));
}
/**
* Tests whether encoding a short audio succeeds. The test queues a few audio frames
* then signals end-of-stream. The test fails if the encoder doesn't output the queued frames.
*/
public void testEncodeShortAudio() throws InterruptedException {
runThread(() -> runEncodeShortAudio());
}
private void runThread(BooleanSupplier supplier) throws InterruptedException {
final AtomicBoolean completed = new AtomicBoolean(false);
Thread thread = new Thread(new Runnable() {
public void run() {
completed.set(supplier.getAsBoolean());
}
});
final AtomicReference<Throwable> throwable = new AtomicReference<>();
thread.setUncaughtExceptionHandler((Thread t, Throwable e) -> {
throwable.set(e);
});
thread.start();
thread.join(TIMEOUT_MS);
Throwable t = throwable.get();
if (t != null) {
Log.e(TAG, "There was an error while running the thread:", t);
fail("There was an error while running the thread; please check log");
}
assertTrue("timed out decoding to end-of-stream", completed.get());
}
private static class InputBlock {
MediaCodec.LinearBlock block;
ByteBuffer buffer;
int offset;
}
private static interface InputSlotListener {
public void onInputSlot(MediaCodec codec, int index, InputBlock input) throws Exception;
}
private static class ExtractorInputSlotListener implements InputSlotListener {
public ExtractorInputSlotListener(
MediaExtractor extractor,
long lastBufferTimestampUs,
boolean obtainBlockForEachBuffer,
LinkedBlockingQueue<Long> timestampQueue) {
mExtractor = extractor;
mLastBufferTimestampUs = lastBufferTimestampUs;
mObtainBlockForEachBuffer = obtainBlockForEachBuffer;
mTimestampQueue = timestampQueue;
}
@Override
public void onInputSlot(MediaCodec codec, int index, InputBlock input) throws Exception {
// Try to feed more data into the codec.
if (mExtractor.getSampleTrackIndex() == -1 || mSignaledEos) {
return;
}
long size = mExtractor.getSampleSize();
String[] codecNames = new String[]{ codec.getName() };
if (mObtainBlockForEachBuffer) {
input.block.recycle();
input.block = MediaCodec.LinearBlock.obtain(Math.toIntExact(size), codecNames);
assertTrue("Blocks obtained through LinearBlock.obtain must be mappable",
input.block.isMappable());
input.buffer = input.block.map();
input.offset = 0;
}
if (input.buffer.capacity() < size) {
input.block.recycle();
input.block = MediaCodec.LinearBlock.obtain(
Math.toIntExact(size * 2), codecNames);
assertTrue("Blocks obtained through LinearBlock.obtain must be mappable",
input.block.isMappable());
input.buffer = input.block.map();
input.offset = 0;
} else if (input.buffer.capacity() - input.offset < size) {
long capacity = input.buffer.capacity();
input.block.recycle();
input.block = MediaCodec.LinearBlock.obtain(
Math.toIntExact(capacity), codecNames);
assertTrue("Blocks obtained through LinearBlock.obtain must be mappable",
input.block.isMappable());
input.buffer = input.block.map();
input.offset = 0;
}
long timestampUs = mExtractor.getSampleTime();
int written = mExtractor.readSampleData(input.buffer, input.offset);
mExtractor.advance();
mSignaledEos = mExtractor.getSampleTrackIndex() == -1
|| timestampUs >= mLastBufferTimestampUs;
MediaCodec.QueueRequest request = codec.getQueueRequest(index);
request.setLinearBlock(input.block, input.offset, written);
request.setPresentationTimeUs(timestampUs);
request.setFlags(mSignaledEos ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
if (mSetParams) {
request.setIntegerParameter("vendor.int", 0);
request.setLongParameter("vendor.long", 0);
request.setFloatParameter("vendor.float", (float)0);
request.setStringParameter("vendor.string", "str");
request.setByteBufferParameter("vendor.buffer", ByteBuffer.allocate(1));
mSetParams = false;
}
request.queue();
input.offset += written;
if (mTimestampQueue != null) {
mTimestampQueue.offer(timestampUs);
}
}
private final MediaExtractor mExtractor;
private final long mLastBufferTimestampUs;
private final boolean mObtainBlockForEachBuffer;
private final LinkedBlockingQueue<Long> mTimestampQueue;
private boolean mSignaledEos = false;
private boolean mSetParams = true;
}
private static interface OutputSlotListener {
// Returns true if EOS is met
public boolean onOutputSlot(MediaCodec codec, int index) throws Exception;
}
private static class DummyOutputSlotListener implements OutputSlotListener {
public DummyOutputSlotListener(boolean graphic, LinkedBlockingQueue<Long> timestampQueue) {
mGraphic = graphic;
mTimestampQueue = timestampQueue;
}
@Override
public boolean onOutputSlot(MediaCodec codec, int index) throws Exception {
MediaCodec.OutputFrame frame = codec.getOutputFrame(index);
boolean eos = (frame.getFlags() & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
if (mGraphic && frame.getHardwareBuffer() != null) {
frame.getHardwareBuffer().close();
}
if (!mGraphic && frame.getLinearBlock() != null) {
frame.getLinearBlock().recycle();
}
Long ts = mTimestampQueue.peek();
if (ts != null && ts == frame.getPresentationTimeUs()) {
mTimestampQueue.poll();
}
codec.releaseOutputBuffer(index, false);
return eos;
}
private final boolean mGraphic;
private final LinkedBlockingQueue<Long> mTimestampQueue;
}
private static class SurfaceOutputSlotListener implements OutputSlotListener {
public SurfaceOutputSlotListener(
OutputSurface surface, LinkedBlockingQueue<Long> timestampQueue) {
mOutputSurface = surface;
mTimestampQueue = timestampQueue;
}
@Override
public boolean onOutputSlot(MediaCodec codec, int index) throws Exception {
MediaCodec.OutputFrame frame = codec.getOutputFrame(index);
boolean eos = (frame.getFlags() & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
boolean render = false;
if (frame.getHardwareBuffer() != null) {
frame.getHardwareBuffer().close();
render = true;
}
Long ts = mTimestampQueue.peek();
if (ts != null && ts == frame.getPresentationTimeUs()) {
mTimestampQueue.poll();
}
codec.releaseOutputBuffer(index, render);
if (render) {
mOutputSurface.awaitNewImage();
}
return eos;
}
private final OutputSurface mOutputSurface;
private final LinkedBlockingQueue<Long> mTimestampQueue;
}
private static class SlotEvent {
SlotEvent(boolean input, int index) {
this.input = input;
this.index = index;
}
final boolean input;
final int index;
}
private boolean runDecodeShortVideo(
int inputResourceId,
long lastBufferTimestampUs,
boolean obtainBlockForEachBuffer) {
OutputSurface outputSurface = null;
MediaExtractor mediaExtractor = null;
MediaCodec mediaCodec = null;
try {
outputSurface = new OutputSurface(1, 1);
mediaExtractor = getMediaExtractorForMimeType(inputResourceId, "video/");
MediaFormat mediaFormat =
mediaExtractor.getTrackFormat(mediaExtractor.getSampleTrackIndex());
// TODO: b/147748978
String[] codecs = MediaUtils.getDecoderNames(true /* isGoog */, mediaFormat);
if (codecs.length == 0) {
Log.i(TAG, "No decoder found for format= " + mediaFormat);
return true;
}
mediaCodec = MediaCodec.createByCodecName(codecs[0]);
LinkedBlockingQueue<Long> timestampQueue = new LinkedBlockingQueue<>();
boolean result = runComponentWithLinearInput(
mediaCodec,
mediaFormat,
outputSurface.getSurface(),
false, // encoder
new ExtractorInputSlotListener(
mediaExtractor,
lastBufferTimestampUs,
obtainBlockForEachBuffer,
timestampQueue),
new SurfaceOutputSlotListener(outputSurface, timestampQueue));
if (result) {
assertTrue("Timestamp should match between input / output",
timestampQueue.isEmpty());
}
return result;
} catch (IOException e) {
throw new RuntimeException("error reading input resource", e);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (mediaCodec != null) {
mediaCodec.stop();
mediaCodec.release();
}
if (mediaExtractor != null) {
mediaExtractor.release();
}
if (outputSurface != null) {
outputSurface.release();
}
}
}
private boolean runDecodeShortAudio(
int inputResourceId,
long lastBufferTimestampUs,
boolean obtainBlockForEachBuffer) {
MediaExtractor mediaExtractor = null;
MediaCodec mediaCodec = null;
try {
mediaExtractor = getMediaExtractorForMimeType(inputResourceId, "audio/");
MediaFormat mediaFormat =
mediaExtractor.getTrackFormat(mediaExtractor.getSampleTrackIndex());
// TODO: b/147748978
String[] codecs = MediaUtils.getDecoderNames(true /* isGoog */, mediaFormat);
if (codecs.length == 0) {
Log.i(TAG, "No decoder found for format= " + mediaFormat);
return true;
}
mediaCodec = MediaCodec.createByCodecName(codecs[0]);
LinkedBlockingQueue<Long> timestampQueue = new LinkedBlockingQueue<>();
boolean result = runComponentWithLinearInput(
mediaCodec,
mediaFormat,
null, // surface
false, // encoder
new ExtractorInputSlotListener(
mediaExtractor,
lastBufferTimestampUs,
obtainBlockForEachBuffer,
timestampQueue),
new DummyOutputSlotListener(false /* graphic */, timestampQueue));
if (result) {
assertTrue("Timestamp should match between input / output",
timestampQueue.isEmpty());
}
return result;
} catch (IOException e) {
throw new RuntimeException("error reading input resource", e);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (mediaCodec != null) {
mediaCodec.stop();
mediaCodec.release();
}
if (mediaExtractor != null) {
mediaExtractor.release();
}
}
}
private boolean runEncodeShortAudio() {
MediaExtractor mediaExtractor = null;
MediaCodec mediaCodec = null;
try {
mediaExtractor = getMediaExtractorForMimeType(
R.raw.okgoogle123_good, MediaFormat.MIMETYPE_AUDIO_RAW);
MediaFormat mediaFormat = new MediaFormat(
mediaExtractor.getTrackFormat(mediaExtractor.getSampleTrackIndex()));
mediaFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_AAC);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128000);
// TODO: b/147748978
String[] codecs = MediaUtils.getEncoderNames(true /* isGoog */, mediaFormat);
if (codecs.length == 0) {
Log.i(TAG, "No encoder found for format= " + mediaFormat);
return true;
}
mediaCodec = MediaCodec.createByCodecName(codecs[0]);
LinkedBlockingQueue<Long> timestampQueue = new LinkedBlockingQueue<>();
boolean result = runComponentWithLinearInput(
mediaCodec,
mediaFormat,
null, // surface
true, // encoder
new ExtractorInputSlotListener(
mediaExtractor,
LAST_BUFFER_TIMESTAMP_US,
false,
timestampQueue),
new DummyOutputSlotListener(false /* graphic */, timestampQueue));
if (result) {
assertTrue("Timestamp should match between input / output",
timestampQueue.isEmpty());
}
return result;
} catch (IOException e) {
throw new RuntimeException("error reading input resource", e);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (mediaCodec != null) {
mediaCodec.stop();
mediaCodec.release();
}
if (mediaExtractor != null) {
mediaExtractor.release();
}
}
}
private boolean runComponentWithLinearInput(
MediaCodec mediaCodec,
MediaFormat mediaFormat,
Surface surface,
boolean encoder,
InputSlotListener inputListener,
OutputSlotListener outputListener) throws Exception {
final LinkedBlockingQueue<SlotEvent> queue = new LinkedBlockingQueue<>();
mediaCodec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
queue.offer(new SlotEvent(true, index));
}
@Override
public void onOutputBufferAvailable(
MediaCodec codec, int index, MediaCodec.BufferInfo info) {
queue.offer(new SlotEvent(false, index));
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
}
@Override
public void onError(MediaCodec codec, CodecException e) {
}
});
String[] codecNames = new String[]{ mediaCodec.getName() };
InputBlock input = new InputBlock();
if (!mediaCodec.getCodecInfo().isVendor() && mediaCodec.getName().startsWith("c2.")) {
assertTrue("Google default c2.* codecs are copy-free compatible with LinearBlocks",
MediaCodec.LinearBlock.isCodecCopyFreeCompatible(codecNames));
}
input.block = MediaCodec.LinearBlock.obtain(
APP_BUFFER_SIZE, codecNames);
assertTrue("Blocks obtained through LinearBlock.obtain must be mappable",
input.block.isMappable());
input.buffer = input.block.map();
input.offset = 0;
int flags = MediaCodec.CONFIGURE_FLAG_USE_BLOCK_MODEL;
if (encoder) {
flags |= MediaCodec.CONFIGURE_FLAG_ENCODE;
}
mediaCodec.configure(mediaFormat, surface, null, flags);
mediaCodec.start();
boolean eos = false;
boolean signaledEos = false;
while (!eos && !Thread.interrupted()) {
SlotEvent event;
try {
event = queue.take();
} catch (InterruptedException e) {
return false;
}
if (event.input) {
inputListener.onInputSlot(mediaCodec, event.index, input);
} else {
eos = outputListener.onOutputSlot(mediaCodec, event.index);
}
}
input.block.recycle();
return eos;
}
private MediaExtractor getMediaExtractorForMimeType(int resourceId, String mimeTypePrefix)
throws IOException {
MediaExtractor mediaExtractor = new MediaExtractor();
AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(resourceId);
try {
mediaExtractor.setDataSource(
afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
} finally {
afd.close();
}
int trackIndex;
for (trackIndex = 0; trackIndex < mediaExtractor.getTrackCount(); trackIndex++) {
MediaFormat trackMediaFormat = mediaExtractor.getTrackFormat(trackIndex);
if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) {
mediaExtractor.selectTrack(trackIndex);
break;
}
}
if (trackIndex == mediaExtractor.getTrackCount()) {
throw new IllegalStateException("couldn't get a video track");
}
return mediaExtractor;
}
private static boolean supportsCodec(String mimeType, boolean encoder) {
MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS);
for (MediaCodecInfo info : list.getCodecInfos()) {
if (encoder != info.isEncoder()) {
continue;
}
for (String type : info.getSupportedTypes()) {
if (type.equalsIgnoreCase(mimeType)) {
return true;
}
}
}
return false;
}
}