blob: b4f41cdfe072d1ec1aba89189d7380065b7f506d [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.hardware.HardwareBuffer;
import android.media.Image;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodec.CodecException;
import android.media.MediaCodecInfo;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.media.cts.MediaCodecBlockModelHelper;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.platform.test.annotations.AppModeFull;
import android.platform.test.annotations.Presubmit;
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.ApiLevelUtil;
import com.android.compatibility.common.util.MediaUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import androidx.test.filters.SdkSuppress;
/**
* MediaCodec tests with CONFIGURE_FLAG_USE_BLOCK_MODEL.
*/
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
@NonMediaMainlineTest
@AppModeFull(reason = "Instant apps cannot access the SD card")
public class MediaCodecBlockModelTest extends AndroidTestCase {
private static final String TAG = "MediaCodecBlockModelTest";
private static final boolean VERBOSE = false; // lots of logging
// Input buffers from this input video are queued up to and including the video frame with
// timestamp LAST_BUFFER_TIMESTAMP_US.
private static final String INPUT_RESOURCE =
"video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz.mp4";
private static final long LAST_BUFFER_TIMESTAMP_US = 166666;
private boolean mIsAtLeastR = ApiLevelUtil.isAtLeast(Build.VERSION_CODES.R);
static final String mInpPrefix = WorkDir.getMediaDirString();
protected static AssetFileDescriptor getAssetFileDescriptorFor(final String res)
throws FileNotFoundException {
File inpFile = new File(mInpPrefix + res);
Preconditions.assertTestFileExists(mInpPrefix + res);
ParcelFileDescriptor parcelFD =
ParcelFileDescriptor.open(inpFile, ParcelFileDescriptor.MODE_READ_ONLY);
return new AssetFileDescriptor(parcelFD, 0, parcelFD.getStatSize());
}
/**
* 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.
*/
@Presubmit
@SmallTest
@RequiresDevice
public void testDecodeShortVideo() throws InterruptedException {
if (!MediaUtils.check(mIsAtLeastR, "test needs Android 11")) return;
MediaCodecBlockModelHelper.runThread(() -> runDecodeShortVideo(
INPUT_RESOURCE,
LAST_BUFFER_TIMESTAMP_US,
true /* obtainBlockForEachBuffer */));
MediaCodecBlockModelHelper.runThread(() -> runDecodeShortVideo(
INPUT_RESOURCE,
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.
*/
@Presubmit
@SmallTest
@RequiresDevice
public void testDecodeShortAudio() throws InterruptedException {
if (!MediaUtils.check(mIsAtLeastR, "test needs Android 11")) return;
MediaCodecBlockModelHelper.runThread(() -> runDecodeShortAudio(
INPUT_RESOURCE,
LAST_BUFFER_TIMESTAMP_US,
true /* obtainBlockForEachBuffer */));
MediaCodecBlockModelHelper.runThread(() -> runDecodeShortAudio(
INPUT_RESOURCE,
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.
*/
@Presubmit
@SmallTest
@RequiresDevice
public void testEncodeShortAudio() throws InterruptedException {
if (!MediaUtils.check(mIsAtLeastR, "test needs Android 11")) return;
MediaCodecBlockModelHelper.runThread(() -> runEncodeShortAudio());
}
/**
* Tests whether encoding a short video succeeds. The test queues a few video frames
* then signals end-of-stream. The test fails if the encoder doesn't output the queued frames.
*/
@Presubmit
@SmallTest
@RequiresDevice
public void testEncodeShortVideo() throws InterruptedException {
if (!MediaUtils.check(mIsAtLeastR, "test needs Android 11")) return;
MediaCodecBlockModelHelper.runThread(() -> runEncodeShortVideo());
}
private MediaCodecBlockModelHelper.Result runDecodeShortAudio(
String inputResource,
long lastBufferTimestampUs,
boolean obtainBlockForEachBuffer) {
MediaExtractor mediaExtractor = null;
MediaCodec mediaCodec = null;
try {
mediaExtractor = getMediaExtractorForMimeType(inputResource, "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 MediaCodecBlockModelHelper.Result.SKIP;
}
mediaCodec = MediaCodec.createByCodecName(codecs[0]);
List<Long> timestampList = Collections.synchronizedList(new ArrayList<>());
MediaCodecBlockModelHelper.Result result =
MediaCodecBlockModelHelper.runComponentWithLinearInput(
mediaCodec,
null, // crypto
mediaFormat,
null, // surface
false, // encoder
new MediaCodecBlockModelHelper.ExtractorInputSlotListener
.Builder()
.setExtractor(mediaExtractor)
.setLastBufferTimestampUs(lastBufferTimestampUs)
.setObtainBlockForEachBuffer(obtainBlockForEachBuffer)
.setTimestampQueue(timestampList)
.build(),
new MediaCodecBlockModelHelper.DummyOutputSlotListener(
false /* graphic */, timestampList));
if (result == MediaCodecBlockModelHelper.Result.SUCCESS) {
assertTrue("Timestamp should match between input / output: " + timestampList,
timestampList.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 MediaCodecBlockModelHelper.Result runEncodeShortAudio() {
MediaExtractor mediaExtractor = null;
MediaCodec mediaCodec = null;
try {
mediaExtractor = getMediaExtractorForMimeType(
"okgoogle123_good.wav", 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 MediaCodecBlockModelHelper.Result.SKIP;
}
mediaCodec = MediaCodec.createByCodecName(codecs[0]);
List<Long> timestampList = Collections.synchronizedList(new ArrayList<>());
MediaCodecBlockModelHelper.Result result =
MediaCodecBlockModelHelper.runComponentWithLinearInput(
mediaCodec,
null, // crypto
mediaFormat,
null, // surface
true, // encoder
new MediaCodecBlockModelHelper.ExtractorInputSlotListener
.Builder()
.setExtractor(mediaExtractor)
.setLastBufferTimestampUs(LAST_BUFFER_TIMESTAMP_US)
.setTimestampQueue(timestampList)
.build(),
new MediaCodecBlockModelHelper.DummyOutputSlotListener(
false /* graphic */, timestampList));
if (result == MediaCodecBlockModelHelper.Result.SUCCESS) {
assertTrue("Timestamp should match between input / output: " + timestampList,
timestampList.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 MediaCodecBlockModelHelper.Result runEncodeShortVideo() {
final int kWidth = 176;
final int kHeight = 144;
final int kFrameRate = 15;
MediaCodec mediaCodec = null;
ArrayList<HardwareBuffer> hardwareBuffers = new ArrayList<>();
try {
MediaFormat mediaFormat = MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_AVC, kWidth, kHeight);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, kFrameRate);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1000000);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
mediaFormat.setInteger(
MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
// TODO: b/147748978
String[] codecs = MediaUtils.getEncoderNames(true /* isGoog */, mediaFormat);
if (codecs.length == 0) {
Log.i(TAG, "No encoder found for format= " + mediaFormat);
return MediaCodecBlockModelHelper.Result.SKIP;
}
mediaCodec = MediaCodec.createByCodecName(codecs[0]);
long usage = HardwareBuffer.USAGE_CPU_READ_OFTEN;
usage |= HardwareBuffer.USAGE_CPU_WRITE_OFTEN;
if (mediaCodec.getCodecInfo().isHardwareAccelerated()) {
usage |= HardwareBuffer.USAGE_VIDEO_ENCODE;
}
if (!HardwareBuffer.isSupported(
kWidth, kHeight, HardwareBuffer.YCBCR_420_888, 1 /* layer */, usage)) {
Log.i(TAG, "HardwareBuffer doesn't support " + kWidth + "x" + kHeight
+ "; YCBCR_420_888; usage(" + Long.toHexString(usage) + ")");
return MediaCodecBlockModelHelper.Result.SKIP;
}
List<Long> timestampList = Collections.synchronizedList(new ArrayList<>());
final LinkedBlockingQueue<MediaCodecBlockModelHelper.SlotEvent> queue =
new LinkedBlockingQueue<>();
mediaCodec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
queue.offer(new MediaCodecBlockModelHelper.SlotEvent(true, index));
}
@Override
public void onOutputBufferAvailable(
MediaCodec codec, int index, MediaCodec.BufferInfo info) {
queue.offer(new MediaCodecBlockModelHelper.SlotEvent(false, index));
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
}
@Override
public void onError(MediaCodec codec, CodecException e) {
}
});
int flags = MediaCodec.CONFIGURE_FLAG_USE_BLOCK_MODEL;
flags |= MediaCodec.CONFIGURE_FLAG_ENCODE;
mediaCodec.configure(mediaFormat, null, null, flags);
mediaCodec.start();
boolean eos = false;
boolean signaledEos = false;
int frameIndex = 0;
while (!eos && !Thread.interrupted()) {
MediaCodecBlockModelHelper.SlotEvent event;
try {
event = queue.take();
} catch (InterruptedException e) {
return MediaCodecBlockModelHelper.Result.FAIL;
}
if (event.input) {
if (signaledEos) {
continue;
}
while (hardwareBuffers.size() <= event.index) {
hardwareBuffers.add(null);
}
HardwareBuffer buffer = hardwareBuffers.get(event.index);
if (buffer == null) {
buffer = HardwareBuffer.create(
kWidth, kHeight, HardwareBuffer.YCBCR_420_888, 1, usage);
hardwareBuffers.set(event.index, buffer);
}
try (Image image = MediaCodec.mapHardwareBuffer(buffer)) {
assertNotNull("CPU readable/writable image must be mappable", image);
assertEquals(kWidth, image.getWidth());
assertEquals(kHeight, image.getHeight());
// For Y plane
int rowSampling = 1;
int colSampling = 1;
for (Image.Plane plane : image.getPlanes()) {
ByteBuffer planeBuffer = plane.getBuffer();
for (int row = 0; row < kHeight / rowSampling; ++row) {
int rowOffset = row * plane.getRowStride();
for (int col = 0; col < kWidth / rowSampling; ++col) {
planeBuffer.put(
rowOffset + col * plane.getPixelStride(),
(byte)(frameIndex * 4));
}
}
// For Cb and Cr planes
rowSampling = 2;
colSampling = 2;
}
}
long timestampUs = 1000000l * frameIndex / kFrameRate;
++frameIndex;
if (frameIndex >= 32) {
signaledEos = true;
}
timestampList.add(timestampUs);
mediaCodec.getQueueRequest(event.index)
.setHardwareBuffer(buffer)
.setPresentationTimeUs(timestampUs)
.setFlags(signaledEos ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0)
.queue();
} else {
MediaCodec.OutputFrame frame = mediaCodec.getOutputFrame(event.index);
eos = (frame.getFlags() & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
if (!eos) {
assertNotNull(frame.getLinearBlock());
frame.getLinearBlock().recycle();
}
timestampList.remove(frame.getPresentationTimeUs());
mediaCodec.releaseOutputBuffer(event.index, false);
}
}
if (!timestampList.isEmpty()) {
assertTrue("Timestamp should match between input / output: " + timestampList,
timestampList.isEmpty());
}
return eos ? MediaCodecBlockModelHelper.Result.SUCCESS
: MediaCodecBlockModelHelper.Result.FAIL;
} 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();
}
for (HardwareBuffer buffer : hardwareBuffers) {
if (buffer != null) {
buffer.close();
}
}
}
}
private MediaCodecBlockModelHelper.Result runDecodeShortVideo(
String inputResource,
long lastBufferTimestampUs,
boolean obtainBlockForEachBuffer) {
return MediaCodecBlockModelHelper.runDecodeShortVideo(
getMediaExtractorForMimeType(inputResource, "video/"),
lastBufferTimestampUs, obtainBlockForEachBuffer, null, null, null);
}
private static MediaExtractor getMediaExtractorForMimeType(final String resource,
String mimeTypePrefix) {
MediaExtractor mediaExtractor = new MediaExtractor();
try (AssetFileDescriptor afd = getAssetFileDescriptorFor(resource)) {
mediaExtractor.setDataSource(
afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
} catch (IOException e) {
throw new RuntimeException(e);
}
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;
}
}