Test decoder render into SurfaceView.
b/28964957
Change-Id: Iaeeef511fab39af5e6078236a47532e7da9ce6c8
diff --git a/tests/tests/media/src/android/media/cts/DecodeAccuracyTest.java b/tests/tests/media/src/android/media/cts/DecodeAccuracyTest.java
index 92aed2d..3f4dde0 100644
--- a/tests/tests/media/src/android/media/cts/DecodeAccuracyTest.java
+++ b/tests/tests/media/src/android/media/cts/DecodeAccuracyTest.java
@@ -55,6 +55,24 @@
getLargerWidthVideoFormat(new VideoFormat(H264_VIDEO_FILE_NAME)));
}
+ public void testH264SurfaceViewVideoDecode() throws Exception {
+ runDecodeAccuracyTest(
+ new SurfaceViewFactory(),
+ new VideoFormat(H264_VIDEO_FILE_NAME));
+ }
+
+ public void testH264SurfaceViewLargerHeightVideoDecode() throws Exception {
+ runDecodeAccuracyTest(
+ new SurfaceViewFactory(),
+ getLargerHeightVideoFormat(new VideoFormat(H264_VIDEO_FILE_NAME)));
+ }
+
+ public void testH264SurfaceViewLargerWidthVideoDecode() throws Exception {
+ runDecodeAccuracyTest(
+ new SurfaceViewFactory(),
+ getLargerWidthVideoFormat(new VideoFormat(H264_VIDEO_FILE_NAME)));
+ }
+
/* <------------- Tests Using VP9 -------------> */
public void testVP9GLViewVideoDecode() throws Exception {
runDecodeAccuracyTest(
@@ -74,6 +92,24 @@
getLargerWidthVideoFormat(new VideoFormat(VP9_VIDEO_FILE_NAME)));
}
+ public void testVP9SurfaceViewVideoDecode() throws Exception {
+ runDecodeAccuracyTest(
+ new SurfaceViewFactory(),
+ new VideoFormat(VP9_VIDEO_FILE_NAME));
+ }
+
+ public void testVP9SurfaceViewLargerHeightVideoDecode() throws Exception {
+ runDecodeAccuracyTest(
+ new SurfaceViewFactory(),
+ getLargerHeightVideoFormat(new VideoFormat(VP9_VIDEO_FILE_NAME)));
+ }
+
+ public void testVP9SurfaceViewLargerWidthVideoDecode() throws Exception {
+ runDecodeAccuracyTest(
+ new SurfaceViewFactory(),
+ getLargerWidthVideoFormat(new VideoFormat(VP9_VIDEO_FILE_NAME)));
+ }
+
private void runDecodeAccuracyTest(VideoViewFactory videoViewFactory, VideoFormat videoFormat) {
checkNotNull(videoViewFactory);
checkNotNull(videoFormat);
@@ -83,15 +119,16 @@
getHelper().generateView(videoView);
}
videoViewFactory.waitForViewIsAvailable();
-
+ // In the case of SurfaceView, VideoViewSnapshot can only capture incoming frames,
+ // so it needs to be created before start decoding.
+ final VideoViewSnapshot videoViewSnapshot = videoViewFactory.getVideoViewSnapshot();
decodeVideo(videoFormat, videoViewFactory);
- validateResult(videoFormat, videoViewFactory);
+ validateResult(videoFormat, videoViewSnapshot);
if (videoView != null) {
getHelper().cleanUpView(videoView);
}
videoViewFactory.release();
- getHelper().unsetOrientation();
}
private void decodeVideo(VideoFormat videoFormat, VideoViewFactory videoViewFactory) {
@@ -103,9 +140,8 @@
assertTrue("Failed to decode the video.", playerResult.isSuccess());
}
- private void validateResult(VideoFormat videoFormat, VideoViewFactory videoViewFactory) {
- final Bitmap result = getHelper().generateBitmapFromVideoViewSnapshot(
- videoViewFactory.getVideoViewSnapshot());
+ private void validateResult(VideoFormat videoFormat, VideoViewSnapshot videoViewSnapshot) {
+ final Bitmap result = getHelper().generateBitmapFromVideoViewSnapshot(videoViewSnapshot);
final Bitmap golden;
final String mime = videoFormat.getMimeType();
if (mime.equals(MimeTypes.VIDEO_H264)) {
diff --git a/tests/tests/media/src/android/media/cts/DecodeAccuracyTestBase.java b/tests/tests/media/src/android/media/cts/DecodeAccuracyTestBase.java
index 2d5fc6f..3e3c866f 100644
--- a/tests/tests/media/src/android/media/cts/DecodeAccuracyTestBase.java
+++ b/tests/tests/media/src/android/media/cts/DecodeAccuracyTestBase.java
@@ -24,6 +24,7 @@
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.SurfaceTexture;
@@ -39,13 +40,18 @@
import android.opengl.GLSurfaceView;
import android.os.Build;
import android.os.Handler;
+import android.os.HandlerThread;
import android.os.Looper;
import android.os.SystemClock;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
+import android.view.PixelCopy;
+import android.view.PixelCopy.OnPixelCopyFinishedListener;
import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
@@ -539,7 +545,9 @@
@TargetApi(16)
class TextureViewFactory extends VideoViewFactory implements TextureView.SurfaceTextureListener {
- private final String TAG = TextureViewFactory.class.getSimpleName();
+ private static final String TAG = TextureViewFactory.class.getSimpleName();
+ private static final String NAME = "TextureView";
+
private final Object syncToken = new Object();
private TextureView textureView;
@@ -559,7 +567,7 @@
@Override
public String getName() {
- return "TextureView";
+ return NAME;
}
@Override
@@ -579,7 +587,7 @@
try {
syncToken.wait(VIEW_AVAILABLE_TIMEOUT_MS);
} catch (InterruptedException exception) {
- Log.e(TAG, "InterruptedException in waitForViewIsAvailable", exception);
+ Log.e(TAG, "Taking too long to attach a TextureView to a window.", exception);
}
}
}
@@ -607,18 +615,96 @@
}
/**
+ * Factory for building a {@link SurfaceView}
+ */
+class SurfaceViewFactory extends VideoViewFactory implements SurfaceHolder.Callback {
+
+ private static final String TAG = SurfaceViewFactory.class.getSimpleName();
+ private static final String NAME = "SurfaceView";
+
+ private final Object syncToken = new Object();
+ private SurfaceViewSnapshot surfaceViewSnapshot;
+ private SurfaceView surfaceView;
+ private SurfaceHolder surfaceHolder;
+
+ public SurfaceViewFactory() {}
+
+ @Override
+ public void release() {
+ if (surfaceViewSnapshot != null) {
+ surfaceViewSnapshot.release();
+ }
+ surfaceView = null;
+ surfaceHolder = null;
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public View createView(Context context) {
+ Looper.prepare();
+ surfaceView = new SurfaceView(context);
+ surfaceHolder = surfaceView.getHolder();
+ surfaceHolder.addCallback(this);
+ return surfaceView;
+ }
+
+ @Override
+ public void waitForViewIsAvailable() {
+ while (!getSurface().isValid()) {
+ synchronized (syncToken) {
+ try {
+ syncToken.wait(VIEW_AVAILABLE_TIMEOUT_MS);
+ } catch (InterruptedException exception) {
+ Log.e(TAG, "Taking too long to attach a SurfaceView to a window.", exception);
+ }
+ }
+ }
+ }
+
+ @Override
+ public Surface getSurface() {
+ return surfaceHolder.getSurface();
+ }
+
+ @Override
+ public VideoViewSnapshot getVideoViewSnapshot() {
+ surfaceViewSnapshot = new SurfaceViewSnapshot(surfaceView, VIEW_WIDTH, VIEW_HEIGHT);
+ return surfaceViewSnapshot;
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ synchronized (syncToken) {
+ syncToken.notify();
+ }
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {}
+
+}
+
+/**
* Factory for building EGL and GLES that could render to GLSurfaceView.
* {@link GLSurfaceView} {@link EGL10} {@link GLES20}.
*/
@TargetApi(16)
class GLSurfaceViewFactory extends VideoViewFactory {
- private final String TAG = GLSurfaceViewFactory.class.getSimpleName();
+ private static final String TAG = GLSurfaceViewFactory.class.getSimpleName();
+ private static final String NAME = "GLSurfaceView";
+
private final Object surfaceSyncToken = new Object();
- private final Object snapshotSyncToken = new Object();
private GLSurfaceViewThread glSurfaceViewThread;
- private boolean snapshotIsReady = false;
+ private boolean byteBufferIsReady = false;
public GLSurfaceViewFactory() {}
@@ -630,7 +716,7 @@
@Override
public String getName() {
- return "GLSurfaceView";
+ return NAME;
}
@Override
@@ -649,7 +735,7 @@
try {
surfaceSyncToken.wait(VIEW_AVAILABLE_TIMEOUT_MS);
} catch (InterruptedException exception) {
- Log.e(TAG, "InterruptedException in waitForViewIsAvailable", exception);
+ Log.e(TAG, "Taking too long for the surface to become available.", exception);
}
}
}
@@ -662,17 +748,15 @@
@Override
public VideoViewSnapshot getVideoViewSnapshot() {
- while (!snapshotIsReady) {
- synchronized (snapshotSyncToken) {
- try {
- snapshotSyncToken.wait(VIEW_AVAILABLE_TIMEOUT_MS);
- } catch (InterruptedException exception) {
- Log.e(TAG, "InterruptedException in getVideoViewSnapshot", exception);
- }
- }
- }
- return new GLSurfaceViewSnapshot(
- glSurfaceViewThread.getByteBuffer(), VIEW_WIDTH, VIEW_HEIGHT);
+ return new GLSurfaceViewSnapshot(this, VIEW_WIDTH, VIEW_HEIGHT);
+ }
+
+ public boolean byteBufferIsReady() {
+ return byteBufferIsReady;
+ }
+
+ public ByteBuffer getByteBuffer() {
+ return glSurfaceViewThread.getByteBuffer();
}
/* Does all GL operations. */
@@ -749,7 +833,6 @@
synchronized (surfaceSyncToken) {
surfaceSyncToken.notify();
}
-
// Store pixels from surface
byteBuffer = ByteBuffer.allocateDirect(VIEW_WIDTH * VIEW_HEIGHT * 4);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
@@ -761,13 +844,8 @@
checkGlError("before updateTexImage");
surfaceTexture.updateTexImage();
st.getTransformMatrix(textureTransform);
-
drawFrame();
saveFrame();
- snapshotIsReady = true;
- synchronized(snapshotSyncToken) {
- snapshotSyncToken.notify();
- }
}
/* Prepares EGL to use GLES 2.0 context and a surface that supports pbuffer. */
@@ -909,9 +987,11 @@
/* Reads the pixels to a ByteBuffer. */
public void saveFrame() {
+ byteBufferIsReady = false;
byteBuffer.clear();
GLES20.glReadPixels(0, 0, VIEW_WIDTH, VIEW_HEIGHT, GLES20.GL_RGBA,
GLES20.GL_UNSIGNED_BYTE, byteBuffer);
+ byteBufferIsReady = true;
}
public int getTextureId() {
@@ -1034,26 +1114,140 @@
}
/**
+ * Method to get bitmap of a {@link SurfaceView}.
+ */
+class SurfaceViewSnapshot extends VideoViewSnapshot {
+
+ private static final String TAG = SurfaceViewSnapshot.class.getSimpleName();
+ private static final int PIXELCOPY_REQUEST_SLEEP_MS = 30;
+ private static final int PIXELCOPY_REQUEST_MAX_ATTEMPTS = 20;
+ private static final int PIXELCOPY_TIMEOUT_MS = 1000;
+
+ private final Thread copyThread;
+ private Bitmap bitmap;
+ private int copyResult;
+
+ public SurfaceViewSnapshot(final SurfaceView surfaceView, final int width, final int height) {
+ this.copyResult = -1;
+ this.copyThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ SynchronousPixelCopy copyHelper = new SynchronousPixelCopy();
+ bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
+ try {
+ // Wait for SurfaceView to be available.
+ for (int i = 0; i < PIXELCOPY_REQUEST_MAX_ATTEMPTS; i++) {
+ copyResult = copyHelper.request(surfaceView, bitmap);
+ if (copyResult == PixelCopy.SUCCESS) {
+ break;
+ }
+ Thread.sleep(PIXELCOPY_REQUEST_SLEEP_MS);
+ }
+ } catch (InterruptedException ex) {
+ Log.w(TAG, "Pixel Copy is stopped/interrupted before it finishes", ex);
+ }
+ copyHelper.release();
+ }
+ });
+ copyThread.start();
+ }
+
+ @Override
+ public synchronized void run() {}
+
+ @Override
+ public Bitmap getBitmap() {
+ return bitmap;
+ }
+
+ @Override
+ public boolean isBitmapReady() {
+ return copyResult == PixelCopy.SUCCESS;
+ }
+
+ public void release() {
+ if (copyThread.isAlive()) {
+ copyThread.interrupt();
+ }
+ }
+
+ private static class SynchronousPixelCopy implements OnPixelCopyFinishedListener {
+
+ private final Handler handler;
+ private final HandlerThread thread;
+
+ private int status = -1;
+
+ public SynchronousPixelCopy() {
+ this.thread = new HandlerThread("PixelCopyHelper");
+ thread.start();
+ this.handler = new Handler(thread.getLooper());
+ }
+
+ public void release() {
+ thread.quit();
+ }
+
+ public int request(SurfaceView source, Bitmap dest) {
+ synchronized (this) {
+ PixelCopy.request(source, dest, this, handler);
+ return getResultLocked();
+ }
+ }
+
+ private int getResultLocked() {
+ try {
+ this.wait(PIXELCOPY_TIMEOUT_MS);
+ } catch (InterruptedException e) { /* PixelCopy request didn't complete within 1s */ }
+ return status;
+ }
+
+ @Override
+ public void onPixelCopyFinished(int copyResult) {
+ synchronized (this) {
+ status = copyResult;
+ this.notify();
+ }
+ }
+
+ }
+
+}
+
+/**
* Runnable to get a bitmap from a GLSurfaceView on the UI thread via a handler.
* Note, because of how the bitmap is captured in GLSurfaceView,
* this method does not have to be a runnable.
*/
class GLSurfaceViewSnapshot extends VideoViewSnapshot {
- private Bitmap bitmap = null;
- private ByteBuffer byteBuffer;
+ private static final String TAG = GLSurfaceViewSnapshot.class.getSimpleName();
+ private static final int GET_BYTEBUFFER_SLEEP_MS = 30;
+ private static final int GET_BYTEBUFFER_MAX_ATTEMPTS = 20;
+
+ private final GLSurfaceViewFactory glSurfaceViewFactory;
private final int width;
private final int height;
+
+ private Bitmap bitmap = null;
private boolean bitmapIsReady = false;
- public GLSurfaceViewSnapshot(ByteBuffer byteBuffer, int width, int height) {
- this.byteBuffer = DecodeAccuracyTestBase.checkNotNull(byteBuffer);
+ public GLSurfaceViewSnapshot(GLSurfaceViewFactory glSurfaceViewFactory, int width, int height) {
+ this.glSurfaceViewFactory = DecodeAccuracyTestBase.checkNotNull(glSurfaceViewFactory);
this.width = width;
this.height = height;
}
@Override
public synchronized void run() {
+ try {
+ waitForByteBuffer();
+ } catch (InterruptedException exception) {
+ Log.w(TAG, exception.getMessage());
+ Log.w(TAG, "ByteBuffer may contain incorrect pixels.");
+ }
+ // Get ByteBuffer anyway. Let the test fail if ByteBuffer contains incorrect pixels.
+ ByteBuffer byteBuffer = glSurfaceViewFactory.getByteBuffer();
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
byteBuffer.rewind();
bitmap.copyPixelsFromBuffer(byteBuffer);
@@ -1070,6 +1264,17 @@
return bitmapIsReady;
}
+ public void waitForByteBuffer() throws InterruptedException {
+ // Wait for byte buffer to be ready.
+ for (int i = 0; i < GET_BYTEBUFFER_MAX_ATTEMPTS; i++) {
+ if (glSurfaceViewFactory.byteBufferIsReady()) {
+ return;
+ }
+ Thread.sleep(GET_BYTEBUFFER_SLEEP_MS);
+ }
+ throw new InterruptedException("Taking too long to read pixels into a ByteBuffer.");
+ }
+
}
/* Stores information of a video. */