blob: ba67a42caac6959eeb6ca5114dbc8e9809760c14 [file] [log] [blame]
/*
* Copyright (C) 2013 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.app.Presentation;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.opengl.GLES20;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.test.AndroidTestCase;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.Surface;
import android.view.WindowManager;
import android.view.ViewGroup.LayoutParams;
import android.widget.ImageView;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* Tests connecting a virtual display to the input of a MediaCodec encoder.
* <p>
* Other test cases exercise these independently in more depth. The goal here is to make sure
* that virtual displays and MediaCodec can be used together.
* <p>
* We can't control frame-by-frame what appears on the virtual display, because we're
* just throwing a Presentation and a View at it. Further, it's possible that frames
* will be dropped if they arrive faster than they are consumed, so any given frame
* may not appear at all. We can't wait for a series of actions to complete by watching
* the output, because the frames are going directly to the encoder, and the encoder may
* collect a number of frames before producing output.
* <p>
* The test puts up a series of colored screens, expecting to see all of them, and in order.
* Any black screens that appear before or after are ignored.
*/
public class EncodeVirtualDisplayTest extends AndroidTestCase {
private static final String TAG = "EncodeVirtualTest";
private static final boolean VERBOSE = false; // lots of logging
private static final boolean DEBUG_SAVE_FILE = false; // save copy of encoded movie
private static final String DEBUG_FILE_NAME_BASE = "/sdcard/test.";
// Encoder parameters table, sort by encoder level from high to low.
private static final int[][] ENCODER_PARAM_TABLE = {
// width, height, bitrate, framerate /* level */
{ 1280, 720, 14000000, 30 }, /* AVCLevel31 */
{ 720, 480, 10000000, 30 }, /* AVCLevel3 */
{ 720, 480, 4000000, 15 }, /* AVCLevel22 */
{ 352, 576, 4000000, 25 }, /* AVCLevel21 */
};
// Virtual display characteristics. Scaled down from full display size because not all
// devices can encode at the resolution of their own display.
private static final String NAME = TAG;
private static int sWidth = ENCODER_PARAM_TABLE[ENCODER_PARAM_TABLE.length-1][0];
private static int sHeight = ENCODER_PARAM_TABLE[ENCODER_PARAM_TABLE.length-1][1];
private static final int DENSITY = DisplayMetrics.DENSITY_HIGH;
private static final int UI_TIMEOUT_MS = 2000;
private static final int UI_RENDER_PAUSE_MS = 400;
// Encoder parameters. We use the same width/height as the virtual display.
private static final String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
private static int sFrameRate = 15; // 15fps
private static final int IFRAME_INTERVAL = 10; // 10 seconds between I-frames
private static int sBitRate = 6000000; // 6Mbps
// Colors to test (RGB). These must convert cleanly to and from BT.601 YUV.
private static final int TEST_COLORS[] = {
makeColor(10, 100, 200), // YCbCr 89,186,82
makeColor(100, 200, 10), // YCbCr 144,60,98
makeColor(200, 10, 100), // YCbCr 203,10,103
makeColor(10, 200, 100), // YCbCr 130,113,52
makeColor(100, 10, 200), // YCbCr 67,199,154
makeColor(200, 100, 10), // YCbCr 119,74,179
};
private final ByteBuffer mPixelBuf = ByteBuffer.allocateDirect(4);
private Handler mUiHandler; // Handler on main Looper
private DisplayManager mDisplayManager;
volatile boolean mInputDone;
/* TEST_COLORS static initialization; need ARGB for ColorDrawable */
private static int makeColor(int red, int green, int blue) {
return 0xff << 24 | (red & 0xff) << 16 | (green & 0xff) << 8 | (blue & 0xff);
}
@Override
protected void setUp() throws Exception {
super.setUp();
mUiHandler = new Handler(Looper.getMainLooper());
mDisplayManager = (DisplayManager)mContext.getSystemService(Context.DISPLAY_SERVICE);
setupEncoderParameters();
}
/**
* Basic test.
*
* @throws Exception
*/
public void testEncodeVirtualDisplay() throws Throwable {
EncodeVirtualWrapper.runTest(this);
}
/**
* Wraps encodeVirtualTest, running it in a new thread. Required because of the way
* SurfaceTexture.OnFrameAvailableListener works when the current thread has a Looper
* configured.
*/
private static class EncodeVirtualWrapper implements Runnable {
private Throwable mThrowable;
private EncodeVirtualDisplayTest mTest;
private EncodeVirtualWrapper(EncodeVirtualDisplayTest test) {
mTest = test;
}
@Override
public void run() {
try {
mTest.encodeVirtualDisplayTest();
} catch (Throwable th) {
mThrowable = th;
}
}
/** Entry point. */
public static void runTest(EncodeVirtualDisplayTest obj) throws Throwable {
EncodeVirtualWrapper wrapper = new EncodeVirtualWrapper(obj);
Thread th = new Thread(wrapper, "codec test");
th.start();
th.join();
if (wrapper.mThrowable != null) {
throw wrapper.mThrowable;
}
}
}
/**
* Returns true if the encoder level, specified in the ENCODER_PARAM_TABLE, can be supported.
*/
private static boolean verifySupportForEncoderLevel(int i) {
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
MediaFormat format = MediaFormat.createVideoFormat(
MIME_TYPE, ENCODER_PARAM_TABLE[i][0], ENCODER_PARAM_TABLE[i][1]);
format.setInteger(MediaFormat.KEY_BIT_RATE, ENCODER_PARAM_TABLE[i][2]);
format.setInteger(MediaFormat.KEY_FRAME_RATE, ENCODER_PARAM_TABLE[i][3]);
return mcl.findEncoderForFormat(format) != null;
}
/**
* Initialize the encoder parameters according to the device capability.
*/
private static void setupEncoderParameters() {
// Loop over each tabel entry until a proper encoder setting is found.
for (int i = 0; i < ENCODER_PARAM_TABLE.length; i++) {
// Check if we can support it?
if (verifySupportForEncoderLevel(i)) {
sWidth = ENCODER_PARAM_TABLE[i][0];
sHeight = ENCODER_PARAM_TABLE[i][1];
sBitRate = ENCODER_PARAM_TABLE[i][2];
sFrameRate = ENCODER_PARAM_TABLE[i][3];
Log.d(TAG, "encoder parameters changed: width = " + sWidth + ", height = " + sHeight
+ ", bitrate = " + sBitRate + ", framerate = " + sFrameRate);
break;
}
}
}
/**
* Prepares the encoder, decoder, and virtual display.
*/
private void encodeVirtualDisplayTest() throws IOException {
MediaCodec encoder = null;
MediaCodec decoder = null;
OutputSurface outputSurface = null;
VirtualDisplay virtualDisplay = null;
try {
// Encoded video resolution matches virtual display.
MediaFormat encoderFormat = MediaFormat.createVideoFormat(MIME_TYPE, sWidth, sHeight);
encoderFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
encoderFormat.setInteger(MediaFormat.KEY_BIT_RATE, sBitRate);
encoderFormat.setInteger(MediaFormat.KEY_FRAME_RATE, sFrameRate);
encoderFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
String codec = mcl.findEncoderForFormat(encoderFormat);
if (codec == null) {
// Don't run the test if the codec isn't present.
Log.i(TAG, "SKIPPING test: no support for " + encoderFormat);
return;
}
encoder = MediaCodec.createByCodecName(codec);
encoder.configure(encoderFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface inputSurface = encoder.createInputSurface();
encoder.start();
// Create a virtual display that will output to our encoder.
virtualDisplay = mDisplayManager.createVirtualDisplay(NAME,
sWidth, sHeight, DENSITY, inputSurface, 0);
// We also need a decoder to check the output of the encoder.
decoder = MediaCodec.createDecoderByType(MIME_TYPE);
MediaFormat decoderFormat = MediaFormat.createVideoFormat(MIME_TYPE, sWidth, sHeight);
outputSurface = new OutputSurface(sWidth, sHeight);
decoder.configure(decoderFormat, outputSurface.getSurface(), null, 0);
decoder.start();
// Run the color slide show on a separate thread.
mInputDone = false;
new ColorSlideShow(virtualDisplay.getDisplay()).start();
// Record everything we can and check the results.
doTestEncodeVirtual(encoder, decoder, outputSurface);
} finally {
if (VERBOSE) Log.d(TAG, "releasing codecs, surfaces, and virtual display");
if (virtualDisplay != null) {
virtualDisplay.release();
}
if (outputSurface != null) {
outputSurface.release();
}
if (encoder != null) {
encoder.stop();
encoder.release();
}
if (decoder != null) {
decoder.stop();
decoder.release();
}
}
}
/**
* Drives the encoder and decoder.
*/
private void doTestEncodeVirtual(MediaCodec encoder, MediaCodec decoder,
OutputSurface outputSurface) {
final int TIMEOUT_USEC = 10000;
ByteBuffer[] encoderOutputBuffers = encoder.getOutputBuffers();
ByteBuffer[] decoderInputBuffers = decoder.getInputBuffers();
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean inputEosSignaled = false;
int lastIndex = -1;
int goodFrames = 0;
int debugFrameCount = 0;
// Save a copy to disk. Useful for debugging the test. Note this is a raw elementary
// stream, not a .mp4 file, so not all players will know what to do with it.
FileOutputStream outputStream = null;
if (DEBUG_SAVE_FILE) {
String fileName = DEBUG_FILE_NAME_BASE + sWidth + "x" + sHeight + ".mp4";
try {
outputStream = new FileOutputStream(fileName);
Log.d(TAG, "encoded output will be saved as " + fileName);
} catch (IOException ioe) {
Log.w(TAG, "Unable to create debug output file " + fileName);
throw new RuntimeException(ioe);
}
}
// Loop until the output side is done.
boolean encoderDone = false;
boolean outputDone = false;
while (!outputDone) {
if (VERBOSE) Log.d(TAG, "loop");
if (!inputEosSignaled && mInputDone) {
if (VERBOSE) Log.d(TAG, "signaling input EOS");
encoder.signalEndOfInputStream();
inputEosSignaled = true;
}
boolean decoderOutputAvailable = true;
boolean encoderOutputAvailable = !encoderDone;
while (decoderOutputAvailable || encoderOutputAvailable) {
// Start by draining any pending output from the decoder. It's important to
// do this before we try to stuff any more data in.
int decoderStatus = decoder.dequeueOutputBuffer(info, TIMEOUT_USEC);
if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// no output available yet
if (VERBOSE) Log.d(TAG, "no output from decoder available");
decoderOutputAvailable = false;
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
if (VERBOSE) Log.d(TAG, "decoder output buffers changed (but we don't care)");
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// this happens before the first frame is returned
MediaFormat decoderOutputFormat = decoder.getOutputFormat();
if (VERBOSE) Log.d(TAG, "decoder output format changed: " +
decoderOutputFormat);
} else if (decoderStatus < 0) {
fail("unexpected result from deocder.dequeueOutputBuffer: " + decoderStatus);
} else { // decoderStatus >= 0
if (VERBOSE) Log.d(TAG, "surface decoder given buffer " + decoderStatus +
" (size=" + info.size + ")");
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
if (VERBOSE) Log.d(TAG, "output EOS");
outputDone = true;
}
// The ByteBuffers are null references, but we still get a nonzero size for
// the decoded data.
boolean doRender = (info.size != 0);
// As soon as we call releaseOutputBuffer, the buffer will be forwarded
// to SurfaceTexture to convert to a texture. The API doesn't guarantee
// that the texture will be available before the call returns, so we
// need to wait for the onFrameAvailable callback to fire. If we don't
// wait, we risk dropping frames.
outputSurface.makeCurrent();
decoder.releaseOutputBuffer(decoderStatus, doRender);
if (doRender) {
if (VERBOSE) Log.d(TAG, "awaiting frame " + (lastIndex+1));
outputSurface.awaitNewImage();
outputSurface.drawImage();
int foundIndex = checkSurfaceFrame();
if (foundIndex == lastIndex + 1) {
// found the next one in the series
lastIndex = foundIndex;
goodFrames++;
} else if (foundIndex == lastIndex) {
// Sometimes we see the same color two frames in a row.
if (VERBOSE) Log.d(TAG, "Got another " + lastIndex);
} else if (foundIndex > 0) {
// Looks like we missed a color frame. It's possible something
// stalled and we dropped a frame. Skip forward to see if we
// can catch the rest.
if (foundIndex < lastIndex) {
Log.w(TAG, "Ignoring backward skip from " +
lastIndex + " to " + foundIndex);
} else {
Log.w(TAG, "Frame skipped, advancing lastIndex from " +
lastIndex + " to " + foundIndex);
goodFrames++;
lastIndex = foundIndex;
}
}
}
}
if (decoderStatus != MediaCodec.INFO_TRY_AGAIN_LATER) {
// Continue attempts to drain output.
continue;
}
// Decoder is drained, check to see if we've got a new buffer of output from
// the encoder.
if (!encoderDone) {
int encoderStatus = encoder.dequeueOutputBuffer(info, TIMEOUT_USEC);
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// no output available yet
if (VERBOSE) Log.d(TAG, "no output from encoder available");
encoderOutputAvailable = false;
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// not expected for an encoder
encoderOutputBuffers = encoder.getOutputBuffers();
if (VERBOSE) Log.d(TAG, "encoder output buffers changed");
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// received before first buffer
MediaFormat newFormat = encoder.getOutputFormat();
if (VERBOSE) Log.d(TAG, "encoder output format changed: " + newFormat);
} else if (encoderStatus < 0) {
fail("unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus);
} else { // encoderStatus >= 0
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
if (encodedData == null) {
fail("encoderOutputBuffer " + encoderStatus + " was null");
}
// It's usually necessary to adjust the ByteBuffer values to match BufferInfo.
encodedData.position(info.offset);
encodedData.limit(info.offset + info.size);
if (outputStream != null) {
byte[] data = new byte[info.size];
encodedData.get(data);
encodedData.position(info.offset);
try {
outputStream.write(data);
} catch (IOException ioe) {
Log.w(TAG, "failed writing debug data to file");
throw new RuntimeException(ioe);
}
debugFrameCount++;
}
// Get a decoder input buffer, blocking until it's available. We just
// drained the decoder output, so we expect there to be a free input
// buffer now or in the near future (i.e. this should never deadlock
// if the codec is meeting requirements).
//
// The first buffer of data we get will have the BUFFER_FLAG_CODEC_CONFIG
// flag set; the decoder will see this and finish configuring itself.
int inputBufIndex = decoder.dequeueInputBuffer(-1);
ByteBuffer inputBuf = decoderInputBuffers[inputBufIndex];
inputBuf.clear();
inputBuf.put(encodedData);
decoder.queueInputBuffer(inputBufIndex, 0, info.size,
info.presentationTimeUs, info.flags);
// If everything from the encoder has been passed to the decoder, we
// can stop polling the encoder output. (This just an optimization.)
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
encoderDone = true;
encoderOutputAvailable = false;
}
if (VERBOSE) Log.d(TAG, "passed " + info.size + " bytes to decoder"
+ (encoderDone ? " (EOS)" : ""));
encoder.releaseOutputBuffer(encoderStatus, false);
}
}
}
}
if (outputStream != null) {
try {
outputStream.close();
if (VERBOSE) Log.d(TAG, "Wrote " + debugFrameCount + " frames");
} catch (IOException ioe) {
Log.w(TAG, "failed closing debug file");
throw new RuntimeException(ioe);
}
}
if (goodFrames != TEST_COLORS.length) {
fail("Found " + goodFrames + " of " + TEST_COLORS.length + " expected frames");
}
}
/**
* Checks the contents of the current EGL surface to see if it matches expectations.
* <p>
* The surface may be black or one of the colors we've drawn. We have sufficiently little
* control over the rendering process that we don't know how many (if any) black frames
* will appear between each color frame.
* <p>
* @return the color index, or -2 for black
* @throw RuntimeException if the color isn't recognized (probably because the RGB<->YUV
* conversion introduced too much variance)
*/
private int checkSurfaceFrame() {
boolean frameFailed = false;
// Read a pixel from the center of the surface. Might want to read from multiple points
// and average them together.
int x = sWidth / 2;
int y = sHeight / 2;
GLES20.glReadPixels(x, y, 1, 1, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, mPixelBuf);
int r = mPixelBuf.get(0) & 0xff;
int g = mPixelBuf.get(1) & 0xff;
int b = mPixelBuf.get(2) & 0xff;
if (VERBOSE) Log.d(TAG, "GOT: r=" + r + " g=" + g + " b=" + b);
if (approxEquals(0, r) && approxEquals(0, g) && approxEquals(0, b)) {
return -2;
}
// Walk through the color list and try to find a match. These may have gone through
// RGB<->YCbCr conversions, so don't expect exact matches.
for (int i = 0; i < TEST_COLORS.length; i++) {
int testRed = (TEST_COLORS[i] >> 16) & 0xff;
int testGreen = (TEST_COLORS[i] >> 8) & 0xff;
int testBlue = TEST_COLORS[i] & 0xff;
if (approxEquals(testRed, r) && approxEquals(testGreen, g) &&
approxEquals(testBlue, b)) {
if (VERBOSE) Log.d(TAG, "Matched color " + i + ": r=" + r + " g=" + g + " b=" + b);
return i;
}
}
throw new RuntimeException("No match for color r=" + r + " g=" + g + " b=" + b);
}
/**
* Determines if two color values are approximately equal.
*/
private static boolean approxEquals(int expected, int actual) {
final int MAX_DELTA = 4;
return Math.abs(expected - actual) <= MAX_DELTA;
}
/**
* Creates a series of colorful Presentations on the specified Display.
*/
private class ColorSlideShow extends Thread {
private Display mDisplay;
public ColorSlideShow(Display display) {
mDisplay = display;
}
@Override
public void run() {
for (int i = 0; i < TEST_COLORS.length; i++) {
showPresentation(TEST_COLORS[i]);
}
if (VERBOSE) Log.d(TAG, "slide show finished");
mInputDone = true;
}
private void showPresentation(final int color) {
final TestPresentation[] presentation = new TestPresentation[1];
try {
runOnUiThread(new Runnable() {
@Override
public void run() {
// Want to create presentation on UI thread so it finds the right Looper
// when setting up the Dialog.
presentation[0] = new TestPresentation(getContext(), mDisplay, color);
if (VERBOSE) Log.d(TAG, "showing color=0x" + Integer.toHexString(color));
presentation[0].show();
}
});
// Give the presentation an opportunity to render. We don't have a way to
// monitor the output, so we just sleep for a bit.
try { Thread.sleep(UI_RENDER_PAUSE_MS); }
catch (InterruptedException ignore) {}
} finally {
if (presentation[0] != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
presentation[0].dismiss();
}
});
}
}
}
}
/**
* Executes a runnable on the UI thread, and waits for it to complete.
*/
private void runOnUiThread(Runnable runnable) {
Runnable waiter = new Runnable() {
@Override
public void run() {
synchronized (this) {
notifyAll();
}
}
};
synchronized (waiter) {
mUiHandler.post(runnable);
mUiHandler.post(waiter);
try {
waiter.wait(UI_TIMEOUT_MS);
} catch (InterruptedException ex) {
}
}
}
/**
* Presentation we can show on a virtual display. The view is set to a single color value.
*/
private class TestPresentation extends Presentation {
private final int mColor;
public TestPresentation(Context context, Display display, int color) {
super(context, display);
mColor = color;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle("Encode Virtual Test");
getWindow().setType(WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION);
// Create a solid color image to use as the content of the presentation.
ImageView view = new ImageView(getContext());
view.setImageDrawable(new ColorDrawable(mColor));
view.setLayoutParams(new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
setContentView(view);
}
}
}