media: add VideoEncoderTest

This test exercises the range of sizes supported by each video encoder.

This is also a test for asynchronous video decoding and encoding.

Bug: 18513091
Change-Id: Ibacfc37f74ac3fd1ae634ae3de8d2064cff620f3
diff --git a/tests/tests/media/libmediandkjni/Android.mk b/tests/tests/media/libmediandkjni/Android.mk
index 2d2033f..59ff7bb 100644
--- a/tests/tests/media/libmediandkjni/Android.mk
+++ b/tests/tests/media/libmediandkjni/Android.mk
@@ -20,7 +20,9 @@
 
 LOCAL_MODULE_TAGS := optional
 
-LOCAL_SRC_FILES := native-media-jni.cpp
+LOCAL_SRC_FILES := \
+	native-media-jni.cpp \
+	codec-utils-jni.cpp
 
 LOCAL_C_INCLUDES := $(JNI_H_INCLUDE)
 
diff --git a/tests/tests/media/libmediandkjni/codec-utils-jni.cpp b/tests/tests/media/libmediandkjni/codec-utils-jni.cpp
new file mode 100644
index 0000000..f99f1c8
--- /dev/null
+++ b/tests/tests/media/libmediandkjni/codec-utils-jni.cpp
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/* Original code copied from NDK Native-media sample code */
+
+#undef NDEBUG
+//#define LOG_NDEBUG 0
+#include <stdint.h>
+#include <sys/types.h>
+#include <jni.h>
+
+#include <ScopedLocalRef.h>
+#include <JNIHelp.h>
+
+typedef ssize_t offs_t;
+
+// for __android_log_print(ANDROID_LOG_INFO, "YourApp", "formatted message");
+#include <android/log.h>
+#define TAG "CodecUtilsJNI"
+#define __ALOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, TAG, __VA_ARGS__)
+#if LOG_NDEBUG
+#define ALOGV(...) do { if (0) { __ALOGV(__VA_ARGS__); } } while (0)
+#else
+#define ALOGV(...) __ALOGV(__VA_ARGS__)
+#endif
+#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
+#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
+#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
+#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
+
+struct NativeImage {
+    struct crop {
+        int left;
+        int top;
+        int right;
+        int bottom;
+    } crop;
+    struct plane {
+        const uint8_t *buffer;
+        size_t size;
+        ssize_t colInc;
+        ssize_t rowInc;
+        offs_t cropOffs;
+        size_t cropWidth;
+        size_t cropHeight;
+    } plane[3];
+    int width;
+    int height;
+    int format;
+    long timestamp;
+    size_t numPlanes;
+};
+
+struct ChecksumAlg {
+    virtual void init() = 0;
+    virtual void update(uint8_t c) = 0;
+    virtual uint32_t checksum() = 0;
+    virtual size_t length() = 0;
+protected:
+    virtual ~ChecksumAlg() {}
+};
+
+struct Adler32 : ChecksumAlg {
+    Adler32() {
+        init();
+    }
+    void init() {
+        a = 1;
+        len = b = 0;
+    }
+    void update(uint8_t c) {
+        a += c;
+        b += a;
+        ++len;
+    }
+    uint32_t checksum() {
+        return (a % 65521) + ((b % 65521) << 16);
+    }
+    size_t length() {
+        return len;
+    }
+private:
+    uint32_t a, b;
+    size_t len;
+};
+
+static struct ImageFieldsAndMethods {
+    // android.graphics.ImageFormat
+    int YUV_420_888;
+    // android.media.Image
+    jmethodID methodWidth;
+    jmethodID methodHeight;
+    jmethodID methodFormat;
+    jmethodID methodTimestamp;
+    jmethodID methodPlanes;
+    jmethodID methodCrop;
+    // android.media.Image.Plane
+    jmethodID methodBuffer;
+    jmethodID methodPixelStride;
+    jmethodID methodRowStride;
+    // android.graphics.Rect
+    jfieldID fieldLeft;
+    jfieldID fieldTop;
+    jfieldID fieldRight;
+    jfieldID fieldBottom;
+} gFields;
+static bool gFieldsInitialized = false;
+
+void initializeGlobalFields(JNIEnv *env) {
+    if (gFieldsInitialized) {
+        return;
+    }
+    {   // ImageFormat
+        jclass imageFormatClazz = env->FindClass("android/graphics/ImageFormat");
+        const jfieldID fieldYUV420888 = env->GetStaticFieldID(imageFormatClazz, "YUV_420_888", "I");
+        gFields.YUV_420_888 = env->GetStaticIntField(imageFormatClazz, fieldYUV420888);
+        env->DeleteLocalRef(imageFormatClazz);
+        imageFormatClazz = NULL;
+    }
+
+    {   // Image
+        jclass imageClazz = env->FindClass("android/media/Image");
+        gFields.methodWidth  = env->GetMethodID(imageClazz, "getWidth", "()I");
+        gFields.methodHeight = env->GetMethodID(imageClazz, "getHeight", "()I");
+        gFields.methodFormat = env->GetMethodID(imageClazz, "getFormat", "()I");
+        gFields.methodTimestamp = env->GetMethodID(imageClazz, "getTimestamp", "()J");
+        gFields.methodPlanes = env->GetMethodID(
+                imageClazz, "getPlanes", "()[Landroid/media/Image$Plane;");
+        gFields.methodCrop   = env->GetMethodID(
+                imageClazz, "getCropRect", "()Landroid/graphics/Rect;");
+        env->DeleteLocalRef(imageClazz);
+        imageClazz = NULL;
+    }
+
+    {   // Image.Plane
+        jclass planeClazz = env->FindClass("android/media/Image$Plane");
+        gFields.methodBuffer = env->GetMethodID(planeClazz, "getBuffer", "()Ljava/nio/ByteBuffer;");
+        gFields.methodPixelStride = env->GetMethodID(planeClazz, "getPixelStride", "()I");
+        gFields.methodRowStride = env->GetMethodID(planeClazz, "getRowStride", "()I");
+        env->DeleteLocalRef(planeClazz);
+        planeClazz = NULL;
+    }
+
+    {   // Rect
+        jclass rectClazz = env->FindClass("android/graphics/Rect");
+        gFields.fieldLeft   = env->GetFieldID(rectClazz, "left", "I");
+        gFields.fieldTop    = env->GetFieldID(rectClazz, "top", "I");
+        gFields.fieldRight  = env->GetFieldID(rectClazz, "right", "I");
+        gFields.fieldBottom = env->GetFieldID(rectClazz, "bottom", "I");
+        env->DeleteLocalRef(rectClazz);
+        rectClazz = NULL;
+    }
+    gFieldsInitialized = true;
+}
+
+NativeImage *getNativeImage(JNIEnv *env, jobject image) {
+    if (image == NULL) {
+        jniThrowNullPointerException(env, "image is null");
+        return NULL;
+    }
+
+    initializeGlobalFields(env);
+
+    NativeImage *img = new NativeImage;
+    img->format = env->CallIntMethod(image, gFields.methodFormat);
+    img->width  = env->CallIntMethod(image, gFields.methodWidth);
+    img->height = env->CallIntMethod(image, gFields.methodHeight);
+    img->timestamp = env->CallLongMethod(image, gFields.methodTimestamp);
+
+    jobject cropRect = env->CallObjectMethod(image, gFields.methodCrop);
+    img->crop.left   = env->GetIntField(cropRect, gFields.fieldLeft);
+    img->crop.top    = env->GetIntField(cropRect, gFields.fieldTop);
+    img->crop.right  = env->GetIntField(cropRect, gFields.fieldRight);
+    img->crop.bottom = env->GetIntField(cropRect, gFields.fieldBottom);
+    if (img->crop.right == 0 && img->crop.bottom == 0) {
+        img->crop.right  = img->width;
+        img->crop.bottom = img->height;
+    }
+    env->DeleteLocalRef(cropRect);
+    cropRect = NULL;
+
+    if (img->format != gFields.YUV_420_888) {
+        jniThrowException(
+                env, "java/lang/UnsupportedOperationException",
+                "only support YUV_420_888 images");
+        delete img;
+        img = NULL;
+        return NULL;
+    }
+    img->numPlanes = 3;
+
+    ScopedLocalRef<jobjectArray> planesArray(
+            env, (jobjectArray)env->CallObjectMethod(image, gFields.methodPlanes));
+    int xDecim = 0;
+    int yDecim = 0;
+    for (size_t ix = 0; ix < img->numPlanes; ++ix) {
+        ScopedLocalRef<jobject> plane(
+                env, env->GetObjectArrayElement(planesArray.get(), (jsize)ix));
+        img->plane[ix].colInc = env->CallIntMethod(plane.get(), gFields.methodPixelStride);
+        img->plane[ix].rowInc = env->CallIntMethod(plane.get(), gFields.methodRowStride);
+        ScopedLocalRef<jobject> buffer(
+                env, env->CallObjectMethod(plane.get(), gFields.methodBuffer));
+
+        img->plane[ix].buffer = (const uint8_t *)env->GetDirectBufferAddress(buffer.get());
+        img->plane[ix].size = env->GetDirectBufferCapacity(buffer.get());
+
+        img->plane[ix].cropOffs =
+            (img->crop.left >> xDecim) * img->plane[ix].colInc
+                    + (img->crop.top >> yDecim) * img->plane[ix].rowInc;
+        img->plane[ix].cropHeight =
+            ((img->crop.bottom + (1 << yDecim) - 1) >> yDecim) - (img->crop.top >> yDecim);
+        img->plane[ix].cropWidth =
+            ((img->crop.right + (1 << xDecim) - 1) >> xDecim) - (img->crop.left >> xDecim);
+
+        // sanity check on increments
+        ssize_t widthOffs =
+            (((img->width + (1 << xDecim) - 1) >> xDecim) - 1) * img->plane[ix].colInc;
+        ssize_t heightOffs =
+            (((img->height + (1 << yDecim) - 1) >> yDecim) - 1) * img->plane[ix].rowInc;
+        if (widthOffs < 0 || heightOffs < 0
+                || widthOffs + heightOffs >= (ssize_t)img->plane[ix].size) {
+            jniThrowException(
+                    env, "java/lang/IndexOutOfBoundsException", "plane exceeds bytearray");
+            delete img;
+            img = NULL;
+            return NULL;
+        }
+        xDecim = yDecim = 1;
+    }
+    return img;
+}
+
+extern "C" jint Java_android_media_cts_CodecUtils_getImageChecksum(JNIEnv *env,
+        jclass /*clazz*/, jobject image)
+{
+    NativeImage *img = getNativeImage(env, image);
+    if (img == NULL) {
+        return 0;
+    }
+
+    Adler32 adler;
+    for (size_t ix = 0; ix < img->numPlanes; ++ix) {
+        const uint8_t *row = img->plane[ix].buffer + img->plane[ix].cropOffs;
+        for (size_t y = img->plane[ix].cropHeight; y > 0; --y) {
+            const uint8_t *col = row;
+            ssize_t colInc = img->plane[ix].colInc;
+            for (size_t x = img->plane[ix].cropWidth; x > 0; --x) {
+                adler.update(*col);
+                col += colInc;
+            }
+            row += img->plane[ix].rowInc;
+        }
+    }
+    ALOGV("adler %zu/%u", adler.length(), adler.checksum());
+    return adler.checksum();
+}
+
+/* tiled copy that loops around source image boundary */
+extern "C" void Java_android_media_cts_CodecUtils_copyFlexYUVImage(JNIEnv *env,
+        jclass /*clazz*/, jobject target, jobject source)
+{
+    NativeImage *tgt = getNativeImage(env, target);
+    NativeImage *src = getNativeImage(env, source);
+    if (tgt != NULL && src != NULL) {
+        ALOGV("copyFlexYUVImage %dx%d (%d,%d..%d,%d) (%zux%zu) %+zd%+zd %+zd%+zd %+zd%+zd <= "
+                "%dx%d (%d, %d..%d, %d) (%zux%zu) %+zd%+zd %+zd%+zd %+zd%+zd",
+                tgt->width, tgt->height,
+                tgt->crop.left, tgt->crop.top, tgt->crop.right, tgt->crop.bottom,
+                tgt->plane[0].cropWidth, tgt->plane[0].cropHeight,
+                tgt->plane[0].rowInc, tgt->plane[0].colInc,
+                tgt->plane[1].rowInc, tgt->plane[1].colInc,
+                tgt->plane[2].rowInc, tgt->plane[2].colInc,
+                src->width, src->height,
+                src->crop.left, src->crop.top, src->crop.right, src->crop.bottom,
+                src->plane[0].cropWidth, src->plane[0].cropHeight,
+                src->plane[0].rowInc, src->plane[0].colInc,
+                src->plane[1].rowInc, src->plane[1].colInc,
+                src->plane[2].rowInc, src->plane[2].colInc);
+        for (size_t ix = 0; ix < tgt->numPlanes; ++ix) {
+            uint8_t *row = const_cast<uint8_t *>(tgt->plane[ix].buffer) + tgt->plane[ix].cropOffs;
+            for (size_t y = 0; y < tgt->plane[ix].cropHeight; ++y) {
+                uint8_t *col = row;
+                ssize_t colInc = tgt->plane[ix].colInc;
+                const uint8_t *srcRow = (src->plane[ix].buffer + src->plane[ix].cropOffs
+                        + src->plane[ix].rowInc * (y % src->plane[ix].cropHeight));
+                for (size_t x = 0; x < tgt->plane[ix].cropWidth; ++x) {
+                    *col = srcRow[src->plane[ix].colInc * (x % src->plane[ix].cropWidth)];
+                    col += colInc;
+                }
+                row += tgt->plane[ix].rowInc;
+            }
+        }
+    }
+}
diff --git a/tests/tests/media/res/raw/video_480x360_mp4_h264_871kbps_30fps.mp4 b/tests/tests/media/res/raw/video_480x360_mp4_h264_871kbps_30fps.mp4
new file mode 100644
index 0000000..55a83e7
--- /dev/null
+++ b/tests/tests/media/res/raw/video_480x360_mp4_h264_871kbps_30fps.mp4
Binary files differ
diff --git a/tests/tests/media/src/android/media/cts/CodecUtils.java b/tests/tests/media/src/android/media/cts/CodecUtils.java
new file mode 100644
index 0000000..3c3576f
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/CodecUtils.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2014 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.media.Image;
+import android.util.Log;
+
+public class CodecUtils  {
+    private static final String TAG = "CodecUtils";
+
+    /** Load jni on initialization */
+    static {
+        Log.i(TAG, "before loadlibrary");
+        System.loadLibrary("ctsmediacodec_jni");
+        Log.i(TAG, "after loadlibrary");
+    }
+
+    public native static int getImageChecksum(Image image);
+    public native static void copyFlexYUVImage(Image target, Image source);
+}
+
diff --git a/tests/tests/media/src/android/media/cts/OutputSurface.java b/tests/tests/media/src/android/media/cts/OutputSurface.java
index fc8ad9c..c87326d 100644
--- a/tests/tests/media/src/android/media/cts/OutputSurface.java
+++ b/tests/tests/media/src/android/media/cts/OutputSurface.java
@@ -70,7 +70,7 @@
         eglSetup(width, height);
         makeCurrent();
 
-        setup();
+        setup(this);
     }
 
     /**
@@ -78,14 +78,18 @@
      * new one).  Creates a Surface that can be passed to MediaCodec.configure().
      */
     public OutputSurface() {
-        setup();
+        setup(this);
+    }
+
+    public OutputSurface(final SurfaceTexture.OnFrameAvailableListener listener) {
+        setup(listener);
     }
 
     /**
      * Creates instances of TextureRender and SurfaceTexture, and a Surface associated
      * with the SurfaceTexture.
      */
-    private void setup() {
+    private void setup(SurfaceTexture.OnFrameAvailableListener listener) {
         mTextureRender = new TextureRender();
         mTextureRender.surfaceCreated();
 
@@ -107,7 +111,7 @@
         //
         // Java language note: passing "this" out of a constructor is generally unwise,
         // but we should be able to get away with it here.
-        mSurfaceTexture.setOnFrameAvailableListener(this);
+        mSurfaceTexture.setOnFrameAvailableListener(listener);
 
         mSurface = new Surface(mSurfaceTexture);
     }
@@ -285,6 +289,11 @@
         mTextureRender.drawFrame(mSurfaceTexture);
     }
 
+    public void latchImage() {
+        mTextureRender.checkGlError("before updateTexImage");
+        mSurfaceTexture.updateTexImage();
+    }
+
     @Override
     public void onFrameAvailable(SurfaceTexture st) {
         if (VERBOSE) Log.d(TAG, "new frame available");
diff --git a/tests/tests/media/src/android/media/cts/VideoEncoderTest.java b/tests/tests/media/src/android/media/cts/VideoEncoderTest.java
new file mode 100644
index 0000000..f78f5f8
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/VideoEncoderTest.java
@@ -0,0 +1,1247 @@
+/*
+ * Copyright 2014 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 com.android.cts.media.R;
+
+import android.media.cts.CodecUtils;
+
+import android.cts.util.MediaUtils;
+import android.graphics.ImageFormat;
+import android.graphics.SurfaceTexture;
+import android.media.Image;
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecInfo.VideoCapabilities;
+import android.media.MediaCodecList;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.net.Uri;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Range;
+import android.util.Size;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+
+public class VideoEncoderTest extends MediaPlayerTestBase {
+    private static final int MAX_SAMPLE_SIZE = 256 * 1024;
+    private static final String TAG = "VideoEncoderTest";
+    private static final long FRAME_TIMEOUT_MS = 1000;
+
+    private static final String SOURCE_URL =
+        "android.resource://com.android.cts.media/raw/video_480x360_mp4_h264_871kbps_30fps";
+
+    private final boolean DEBUG = false;
+
+    abstract class VideoProcessorBase extends MediaCodec.Callback {
+        private static final String TAG = "VideoProcessorBase";
+
+        private MediaExtractor mExtractor;
+        private ByteBuffer mBuffer = ByteBuffer.allocate(MAX_SAMPLE_SIZE);
+        private int mTrackIndex = -1;
+        private boolean mSignaledDecoderEOS;
+
+        protected boolean mCompleted;
+        protected final Object mCondition = new Object();
+
+        protected MediaFormat mDecFormat;
+        protected MediaCodec mDecoder, mEncoder;
+
+        protected void open(String path) throws IOException {
+            mExtractor = new MediaExtractor();
+            if (path.startsWith("android.resource://")) {
+                mExtractor.setDataSource(mContext, Uri.parse(path), null);
+            } else {
+                mExtractor.setDataSource(path);
+            }
+
+            for (int i = 0; i < mExtractor.getTrackCount(); i++) {
+                MediaFormat fmt = mExtractor.getTrackFormat(i);
+                String mime = fmt.getString(MediaFormat.KEY_MIME).toLowerCase();
+                if (mime.startsWith("video/")) {
+                    mTrackIndex = i;
+                    mDecFormat = fmt;
+                    mExtractor.selectTrack(i);
+                    break;
+                }
+            }
+            assertTrue("file " + path + " has no video", mTrackIndex >= 0);
+        }
+
+        // returns true if encoder supports the size
+        protected boolean initCodecsAndConfigureEncoder(
+                String videoEncName, String outMime, int width, int height, int colorFormat)
+                        throws IOException {
+            mDecFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
+
+            MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+            String videoDecName = mcl.findDecoderForFormat(mDecFormat);
+            Log.i(TAG, "decoder for " + mDecFormat + " is " + videoDecName);
+            mDecoder = MediaCodec.createByCodecName(videoDecName);
+            mEncoder = MediaCodec.createByCodecName(videoEncName);
+
+            mDecoder.setCallback(this);
+            mEncoder.setCallback(this);
+
+            MediaCodecInfo.VideoCapabilities encCaps =
+                mEncoder.getCodecInfo().getCapabilitiesForType(outMime).getVideoCapabilities();
+            if (!encCaps.isSizeSupported(width, height)) {
+                Log.i(TAG, videoEncName + " does not support size: " + width + "x" + height);
+                return false;
+            }
+
+            MediaFormat outFmt = MediaFormat.createVideoFormat(outMime, width, height);
+
+            {
+                int maxWidth = encCaps.getSupportedWidths().getUpper();
+                int maxHeight = encCaps.getSupportedHeightsFor(maxWidth).getUpper();
+                int maxRate =
+                    encCaps.getSupportedFrameRatesFor(maxWidth, maxHeight).getUpper().intValue();
+                outFmt.setInteger(MediaFormat.KEY_FRAME_RATE, Math.min(30, maxRate));
+                int bitRate = encCaps.getBitrateRange().clamp(
+                        (int)(encCaps.getBitrateRange().getUpper() /
+                                Math.sqrt(maxWidth * maxHeight / width / height)));
+                Log.d(TAG, "max rate = " + maxRate + ", bit rate = " + bitRate);
+                outFmt.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
+            }
+            outFmt.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
+            outFmt.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
+            mEncoder.configure(outFmt, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+            Log.i(TAG, "encoder input format " + mEncoder.getInputFormat() + " from " + outFmt);
+            return true;
+        }
+
+        protected void close() {
+            if (mDecoder != null) {
+                mDecoder.release();
+                mDecoder = null;
+            }
+            if (mEncoder != null) {
+                mEncoder.release();
+                mEncoder = null;
+            }
+            if (mExtractor != null) {
+                mExtractor.release();
+                mExtractor = null;
+            }
+        }
+
+        // returns true if filled buffer
+        protected boolean fillDecoderInputBuffer(int ix) {
+            if (DEBUG) Log.v(TAG, "decoder received input #" + ix);
+            while (!mSignaledDecoderEOS) {
+                int track = mExtractor.getSampleTrackIndex();
+                if (track >= 0 && track != mTrackIndex) {
+                    mExtractor.advance();
+                    continue;
+                }
+                int size = mExtractor.readSampleData(mBuffer, 0);
+                if (size < 0) {
+                    // queue decoder input EOS
+                    if (DEBUG) Log.v(TAG, "queuing decoder EOS");
+                    mDecoder.queueInputBuffer(
+                            ix, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+                    mSignaledDecoderEOS = true;
+                } else {
+                    mBuffer.limit(size);
+                    mBuffer.position(0);
+                    BufferInfo info = new BufferInfo();
+                    info.set(
+                            0, mBuffer.limit(), mExtractor.getSampleTime(),
+                            mExtractor.getSampleFlags());
+                    mDecoder.getInputBuffer(ix).put(mBuffer);
+                    if (DEBUG) Log.v(TAG, "queing input #" + ix + " for decoder with timestamp "
+                            + info.presentationTimeUs);
+                    mDecoder.queueInputBuffer(
+                            ix, 0, mBuffer.limit(), info.presentationTimeUs, 0);
+                }
+                mExtractor.advance();
+                return true;
+            }
+            return false;
+        }
+
+        protected void emptyEncoderOutputBuffer(int ix, BufferInfo info) {
+            if (DEBUG) Log.v(TAG, "encoder received output #" + ix
+                     + " (sz=" + info.size + ", f=" + info.flags
+                     + ", ts=" + info.presentationTimeUs + ")");
+            if (!mCompleted) {
+                mEncoder.releaseOutputBuffer(ix, false);
+                if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+                    Log.d(TAG, "encoder received output EOS");
+                    synchronized(mCondition) {
+                        mCompleted = true;
+                        mCondition.notifyAll(); // condition is always satisfied
+                    }
+                }
+            }
+        }
+
+        public abstract boolean processLoop(
+                String path, String outMime, String videoEncName,
+                int width, int height, boolean optional);
+    };
+
+    class VideoProcessor extends VideoProcessorBase {
+        private static final String TAG = "VideoProcessor";
+        private boolean mWorkInProgress;
+        private boolean mGotDecoderEOS;
+        private boolean mSignaledEncoderEOS;
+
+        private LinkedList<Pair<Integer, BufferInfo>> mBuffersToRender =
+            new LinkedList<Pair<Integer, BufferInfo>>();
+        private LinkedList<Integer> mEncInputBuffers = new LinkedList<Integer>();
+
+        private int mEncInputBufferSize = -1;
+
+        @Override
+        public boolean processLoop(
+                 String path, String outMime, String videoEncName,
+                 int width, int height, boolean optional) {
+            boolean skipped = true;
+            try {
+                open(path);
+                if (!initCodecsAndConfigureEncoder(
+                        videoEncName, outMime, width, height,
+                        CodecCapabilities.COLOR_FormatYUV420Flexible)) {
+                    assertTrue("could not configure encoder for supported size", optional);
+                    return !skipped;
+                }
+                skipped = false;
+
+                mDecoder.configure(mDecFormat, null /* surface */, null /* crypto */, 0);
+
+                mDecoder.start();
+                mEncoder.start();
+
+                // main loop - process GL ops as only main thread has GL context
+                while (!mCompleted) {
+                    Pair<Integer, BufferInfo> decBuffer = null;
+                    int encBuffer = -1;
+                    synchronized (mCondition) {
+                        try {
+                            // wait for an encoder input buffer and a decoder output buffer
+                            // Use a timeout to avoid stalling the test if it doesn't arrive.
+                            if (!haveBuffers() && !mCompleted) {
+                                mCondition.wait(FRAME_TIMEOUT_MS);
+                            }
+                        } catch (InterruptedException ie) {
+                            fail("wait interrupted");  // shouldn't happen
+                        }
+                        if (mCompleted) {
+                            break;
+                        }
+                        if (!haveBuffers()) {
+                            fail("timed out after " + mBuffersToRender.size()
+                                    + " decoder output and " + mEncInputBuffers.size()
+                                    + " encoder input buffers");
+                        }
+
+                        if (DEBUG) Log.v(TAG, "got image");
+                        decBuffer = mBuffersToRender.removeFirst();
+                        encBuffer = mEncInputBuffers.removeFirst();
+                        if (isEOSOnlyBuffer(decBuffer)) {
+                            queueEncoderEOS(decBuffer, encBuffer);
+                            continue;
+                        }
+                        mWorkInProgress = true;
+                    }
+
+                    if (mWorkInProgress) {
+                        renderDecodedBuffer(decBuffer, encBuffer);
+                        synchronized(mCondition) {
+                            mWorkInProgress = false;
+                        }
+                    }
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+                fail("received exception " + e);
+            } finally {
+                close();
+            }
+            return !skipped;
+        }
+
+        @Override
+        public void onInputBufferAvailable(MediaCodec mediaCodec, int ix) {
+            if (mediaCodec == mDecoder) {
+                // fill input buffer from extractor
+                fillDecoderInputBuffer(ix);
+            } else if (mediaCodec == mEncoder) {
+                synchronized(mCondition) {
+                    mEncInputBuffers.addLast(ix);
+                    tryToPropagateEOS();
+                    if (haveBuffers()) {
+                        mCondition.notifyAll();
+                    }
+                }
+            } else {
+                fail("received input buffer on " + mediaCodec.getName());
+            }
+        }
+
+        @Override
+        public void onOutputBufferAvailable(
+                MediaCodec mediaCodec, int ix, BufferInfo info) {
+            if (mediaCodec == mDecoder) {
+                if (DEBUG) Log.v(TAG, "decoder received output #" + ix
+                         + " (sz=" + info.size + ", f=" + info.flags
+                         + ", ts=" + info.presentationTimeUs + ")");
+                // render output buffer from decoder
+                if (!mGotDecoderEOS) {
+                    boolean eos = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+                    // can release empty buffers now
+                    if (info.size == 0) {
+                        mDecoder.releaseOutputBuffer(ix, false /* render */);
+                        ix = -1; // dummy index used by render to not render
+                    }
+                    synchronized(mCondition) {
+                        if (ix < 0 && eos && mBuffersToRender.size() > 0) {
+                            // move lone EOS flag to last buffer to be rendered
+                            mBuffersToRender.peekLast().second.flags |=
+                                MediaCodec.BUFFER_FLAG_END_OF_STREAM;
+                        } else if (ix >= 0 || eos) {
+                            mBuffersToRender.addLast(Pair.create(ix, info));
+                        }
+                        if (eos) {
+                            tryToPropagateEOS();
+                            mGotDecoderEOS = true;
+                        }
+                        if (haveBuffers()) {
+                            mCondition.notifyAll();
+                        }
+                    }
+                }
+            } else if (mediaCodec == mEncoder) {
+                emptyEncoderOutputBuffer(ix, info);
+            } else {
+                fail("received output buffer on " + mediaCodec.getName());
+            }
+        }
+
+        private void renderDecodedBuffer(Pair<Integer, BufferInfo> decBuffer, int encBuffer) {
+            // process heavyweight actions under instance lock
+            Image encImage = mEncoder.getInputImage(encBuffer);
+            Image decImage = mDecoder.getOutputImage(decBuffer.first);
+            assertNotNull("could not get encoder image for " + mEncoder.getInputFormat(), encImage);
+            assertNotNull("could not get decoder image for " + mDecoder.getInputFormat(), decImage);
+            assertEquals("incorrect decoder format",decImage.getFormat(), ImageFormat.YUV_420_888);
+            assertEquals("incorrect encoder format", encImage.getFormat(), ImageFormat.YUV_420_888);
+
+            CodecUtils.copyFlexYUVImage(encImage, decImage);
+
+            // TRICKY: need this for queueBuffer
+            if (mEncInputBufferSize < 0) {
+                mEncInputBufferSize = mEncoder.getInputBuffer(encBuffer).capacity();
+            }
+            Log.d(TAG, "queuing output #" + encBuffer + " for encoder (sz="
+                    + mEncInputBufferSize + ", f=" + decBuffer.second.flags
+                    + ", ts=" + decBuffer.second.presentationTimeUs + ")");
+            mEncoder.queueInputBuffer(
+                    encBuffer, 0, mEncInputBufferSize, decBuffer.second.presentationTimeUs,
+                    decBuffer.second.flags);
+            if ((decBuffer.second.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+                mSignaledEncoderEOS = true;
+            }
+            mDecoder.releaseOutputBuffer(decBuffer.first, false /* render */);
+        }
+
+        @Override
+        public void onError(MediaCodec mediaCodec, MediaCodec.CodecException e) {
+            fail("received error on " + mediaCodec.getName() + ": " + e);
+        }
+
+        @Override
+        public void onOutputFormatChanged(MediaCodec mediaCodec, MediaFormat mediaFormat) {
+            Log.i(TAG, mediaCodec.getName() + " got new output format " + mediaFormat);
+        }
+
+        // next methods are synchronized on mCondition
+        private boolean haveBuffers() {
+            return mEncInputBuffers.size() > 0 && mBuffersToRender.size() > 0
+                    && !mSignaledEncoderEOS;
+        }
+
+        private boolean isEOSOnlyBuffer(Pair<Integer, BufferInfo> decBuffer) {
+            return decBuffer.first < 0 || decBuffer.second.size == 0;
+        }
+
+        protected void tryToPropagateEOS() {
+            if (!mWorkInProgress && haveBuffers() && isEOSOnlyBuffer(mBuffersToRender.getFirst())) {
+                Pair<Integer, BufferInfo> decBuffer = mBuffersToRender.removeFirst();
+                int encBuffer = mEncInputBuffers.removeFirst();
+                queueEncoderEOS(decBuffer, encBuffer);
+            }
+        }
+
+        void queueEncoderEOS(Pair<Integer, BufferInfo> decBuffer, int encBuffer) {
+            Log.d(TAG, "signaling encoder EOS");
+            mEncoder.queueInputBuffer(encBuffer, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+            mSignaledEncoderEOS = true;
+            if (decBuffer.first >= 0) {
+                mDecoder.releaseOutputBuffer(decBuffer.first, false /* render */);
+            }
+        }
+    }
+
+
+    class SurfaceVideoProcessor extends VideoProcessorBase
+            implements SurfaceTexture.OnFrameAvailableListener {
+        private static final String TAG = "SurfaceVideoProcessor";
+        private boolean mFrameAvailable;
+        private boolean mGotDecoderEOS;
+        private boolean mSignaledEncoderEOS;
+
+        private InputSurface mEncSurface;
+        private OutputSurface mDecSurface;
+        private BufferInfo mInfoOnSurface;
+
+        private LinkedList<Pair<Integer, BufferInfo>> mBuffersToRender =
+            new LinkedList<Pair<Integer, BufferInfo>>();
+
+        @Override
+        public boolean processLoop(
+                String path, String outMime, String videoEncName,
+                int width, int height, boolean optional) {
+            boolean skipped = true;
+            try {
+                open(path);
+                if (!initCodecsAndConfigureEncoder(
+                        videoEncName, outMime, width, height,
+                        CodecCapabilities.COLOR_FormatSurface)) {
+                    assertTrue("could not configure encoder for supported size", optional);
+                    return !skipped;
+                }
+                skipped = false;
+
+                mEncSurface = new InputSurface(mEncoder.createInputSurface());
+                mEncSurface.makeCurrent();
+
+                mDecSurface = new OutputSurface(this);
+                //mDecSurface.changeFragmentShader(FRAGMENT_SHADER);
+                mDecoder.configure(mDecFormat, mDecSurface.getSurface(), null /* crypto */, 0);
+
+                mDecoder.start();
+                mEncoder.start();
+
+                // main loop - process GL ops as only main thread has GL context
+                while (!mCompleted) {
+                    BufferInfo info = null;
+                    synchronized (mCondition) {
+                        try {
+                            // wait for mFrameAvailable, which is set by onFrameAvailable().
+                            // Use a timeout to avoid stalling the test if it doesn't arrive.
+                            if (!mFrameAvailable && !mCompleted) {
+                                mCondition.wait(FRAME_TIMEOUT_MS);
+                            }
+                        } catch (InterruptedException ie) {
+                            fail("wait interrupted");  // shouldn't happen
+                        }
+                        if (mCompleted) {
+                            break;
+                        }
+                        assertTrue("still waiting for image", mFrameAvailable);
+                        if (DEBUG) Log.v(TAG, "got image");
+                        info = mInfoOnSurface;
+                    }
+                    if (info == null) {
+                        continue;
+                    }
+                    if (info.size > 0) {
+                        mDecSurface.latchImage();
+                        if (DEBUG) Log.v(TAG, "latched image");
+                        mFrameAvailable = false;
+
+                        mDecSurface.drawImage();
+                        Log.d(TAG, "encoding frame at " + info.presentationTimeUs * 1000);
+
+                        mEncSurface.setPresentationTime(info.presentationTimeUs * 1000);
+                        mEncSurface.swapBuffers();
+                    }
+                    if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+                        mSignaledEncoderEOS = true;
+                        Log.d(TAG, "signaling encoder EOS");
+                        mEncoder.signalEndOfInputStream();
+                    }
+
+                    synchronized (mCondition) {
+                        mInfoOnSurface = null;
+                        if (mBuffersToRender.size() > 0 && mInfoOnSurface == null) {
+                            if (DEBUG) Log.v(TAG, "handling postponed frame");
+                            Pair<Integer, BufferInfo> nextBuffer = mBuffersToRender.removeFirst();
+                            renderDecodedBuffer(nextBuffer.first, nextBuffer.second);
+                        }
+                    }
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+                fail("received exception " + e);
+            } finally {
+                close();
+                if (mEncSurface != null) {
+                    mEncSurface.release();
+                    mEncSurface = null;
+                }
+                if (mDecSurface != null) {
+                    mDecSurface.release();
+                    mDecSurface = null;
+                }
+            }
+            return !skipped;
+        }
+
+        @Override
+        public void onFrameAvailable(SurfaceTexture st) {
+            if (DEBUG) Log.v(TAG, "new frame available");
+            synchronized (mCondition) {
+                assertFalse("mFrameAvailable already set, frame could be dropped", mFrameAvailable);
+                mFrameAvailable = true;
+                mCondition.notifyAll();
+            }
+        }
+
+        @Override
+        public void onInputBufferAvailable(MediaCodec mediaCodec, int ix) {
+            if (mediaCodec == mDecoder) {
+                // fill input buffer from extractor
+                fillDecoderInputBuffer(ix);
+            } else {
+                fail("received input buffer on " + mediaCodec.getName());
+            }
+        }
+
+        @Override
+        public void onOutputBufferAvailable(
+                MediaCodec mediaCodec, int ix, BufferInfo info) {
+            if (mediaCodec == mDecoder) {
+                if (DEBUG) Log.v(TAG, "decoder received output #" + ix
+                         + " (sz=" + info.size + ", f=" + info.flags
+                         + ", ts=" + info.presentationTimeUs + ")");
+                // render output buffer from decoder
+                if (!mGotDecoderEOS) {
+                    boolean eos = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+                    if (eos) {
+                        mGotDecoderEOS = true;
+                    }
+                    // can release empty buffers now
+                    if (info.size == 0) {
+                        mDecoder.releaseOutputBuffer(ix, false /* render */);
+                        ix = -1; // dummy index used by render to not render
+                    }
+                    if (eos || info.size > 0) {
+                        synchronized(mCondition) {
+                            if (mInfoOnSurface != null || mBuffersToRender.size() > 0) {
+                                if (DEBUG) Log.v(TAG, "postponing render, surface busy");
+                                mBuffersToRender.addLast(Pair.create(ix, info));
+                            } else {
+                                renderDecodedBuffer(ix, info);
+                            }
+                        }
+                    }
+                }
+            } else if (mediaCodec == mEncoder) {
+                emptyEncoderOutputBuffer(ix, info);
+            } else {
+                fail("received output buffer on " + mediaCodec.getName());
+            }
+        }
+
+        private void renderDecodedBuffer(int ix, BufferInfo info) {
+            boolean eos = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+            mInfoOnSurface = info;
+            if (info.size > 0) {
+                Log.d(TAG, "rendering frame #" + ix + " at " + info.presentationTimeUs * 1000
+                        + (eos ? " with EOS" : ""));
+                mDecoder.releaseOutputBuffer(ix, info.presentationTimeUs * 1000);
+            }
+
+            if (eos && info.size == 0) {
+                if (DEBUG) Log.v(TAG, "decoder output EOS available");
+                mFrameAvailable = true;
+                mCondition.notifyAll();
+            }
+        }
+
+        @Override
+        public void onError(MediaCodec mediaCodec, MediaCodec.CodecException e) {
+            fail("received error on " + mediaCodec.getName() + ": " + e);
+        }
+
+        @Override
+        public void onOutputFormatChanged(MediaCodec mediaCodec, MediaFormat mediaFormat) {
+            Log.i(TAG, mediaCodec.getName() + " got new output format " + mediaFormat);
+        }
+    }
+
+    class Encoder {
+        final private String mName;
+        final private String mMime;
+        final private VideoCapabilities mCaps;
+
+        final private Map<Size, Set<Size>> mMinMax;     // extreme sizes
+        final private Map<Size, Set<Size>> mNearMinMax; // sizes near extreme
+        final private Set<Size> mArbitrary;             // arbitrary sizes in the middle
+        final private Set<Size> mSizes;                 // all non-specifically tested sizes
+
+        final private int xAlign;
+        final private int yAlign;
+
+        Encoder(String name, String mime, CodecCapabilities caps) {
+            mName = name;
+            mMime = mime;
+            mCaps = caps.getVideoCapabilities();
+
+            /* calculate min/max sizes */
+            mMinMax = new HashMap<Size, Set<Size>>();
+            mNearMinMax = new HashMap<Size, Set<Size>>();
+            mArbitrary = new HashSet<Size>();
+            mSizes = new HashSet<Size>();
+
+            xAlign = mCaps.getWidthAlignment();
+            yAlign = mCaps.getHeightAlignment();
+
+            initializeSizes();
+        }
+
+        private void initializeSizes() {
+            for (int x = 0; x < 2; ++x) {
+                for (int y = 0; y < 2; ++y) {
+                    addExtremeSizesFor(x, y);
+                }
+            }
+
+            // initialize arbitrary sizes
+            for (int i = 1; i <= 7; ++i) {
+                int j = ((7 * i) % 11) + 1;
+                int width = alignedPointInRange(i * 0.125, xAlign, mCaps.getSupportedWidths());
+                int height = alignedPointInRange(
+                        j * 0.077, yAlign, mCaps.getSupportedHeightsFor(width));
+                mArbitrary.add(new Size(width, height));
+
+                height = alignedPointInRange(i * 0.125, yAlign, mCaps.getSupportedHeights());
+                width = alignedPointInRange(j * 0.077, xAlign, mCaps.getSupportedWidthsFor(height));
+                mArbitrary.add(new Size(width, height));
+            }
+            mArbitrary.removeAll(mSizes);
+            mSizes.addAll(mArbitrary);
+            if (DEBUG) Log.i(TAG, "arbitrary=" + mArbitrary);
+        }
+
+        private void addExtremeSizesFor(int x, int y) {
+            Set<Size> minMax = new HashSet<Size>();
+            Set<Size> nearMinMax = new HashSet<Size>();
+
+            for (int dx = 0; dx <= xAlign; dx += xAlign) {
+                for (int dy = 0; dy <= yAlign; dy += yAlign) {
+                    Set<Size> bucket = (dx + dy == 0) ? minMax : nearMinMax;
+                    try {
+                        int width = getExtreme(mCaps.getSupportedWidths(), x, dx);
+                        int height = getExtreme(mCaps.getSupportedHeightsFor(width), y, dy);
+                        bucket.add(new Size(width, height));
+
+                        // try max max with more reasonable ratio if too skewed
+                        if (x + y == 2 && width >= 4 * height) {
+                            Size wideScreen = getLargestSizeForRatio(16, 9);
+                            width = getExtreme(
+                                    mCaps.getSupportedWidths()
+                                            .intersect(0, wideScreen.getWidth()), x, dx);
+                            height = getExtreme(mCaps.getSupportedHeightsFor(width), y, 0);
+                            bucket.add(new Size(width, height));
+                        }
+                    } catch (IllegalArgumentException e) {
+                    }
+
+                    try {
+                        int height = getExtreme(mCaps.getSupportedHeights(), y, dy);
+                        int width = getExtreme(mCaps.getSupportedWidthsFor(height), x, dx);
+                        bucket.add(new Size(width, height));
+
+                        // try max max with more reasonable ratio if too skewed
+                        if (x + y == 2 && height >= 4 * width) {
+                            Size wideScreen = getLargestSizeForRatio(9, 16);
+                            height = getExtreme(
+                                    mCaps.getSupportedHeights()
+                                            .intersect(0, wideScreen.getHeight()), y, dy);
+                            width = getExtreme(mCaps.getSupportedWidthsFor(height), x, dx);
+                            bucket.add(new Size(width, height));
+                        }
+                    } catch (IllegalArgumentException e) {
+                    }
+                }
+            }
+
+            // keep unique sizes
+            minMax.removeAll(mSizes);
+            mSizes.addAll(minMax);
+            nearMinMax.removeAll(mSizes);
+            mSizes.addAll(nearMinMax);
+
+            mMinMax.put(new Size(x, y), minMax);
+            mNearMinMax.put(new Size(x, y), nearMinMax);
+            if (DEBUG) Log.i(TAG, x + "x" + y + ": minMax=" + mMinMax + ", near=" + mNearMinMax);
+        }
+
+        private int alignInRange(double value, int align, Range<Integer> range) {
+            return range.clamp(align * (int)Math.round(value / align));
+        }
+
+        /* point should be between 0. and 1. */
+        private int alignedPointInRange(double point, int align, Range<Integer> range) {
+            return alignInRange(
+                    range.getLower() + point * (range.getUpper() - range.getLower()), align, range);
+        }
+
+        private int getExtreme(Range<Integer> range, int i, int delta) {
+            int dim = i == 1 ? range.getUpper() - delta : range.getLower() + delta;
+            if (delta == 0
+                    || (dim > range.getLower() && dim < range.getUpper())) {
+                return dim;
+            }
+            throw new IllegalArgumentException();
+        }
+
+        private Size getLargestSizeForRatio(int x, int y) {
+            Range<Integer> widthRange = mCaps.getSupportedWidths();
+            Range<Integer> heightRange = mCaps.getSupportedHeightsFor(widthRange.getUpper());
+            final int xAlign = mCaps.getWidthAlignment();
+            final int yAlign = mCaps.getHeightAlignment();
+
+            // scale by alignment
+            int width = alignInRange(
+                    Math.sqrt(widthRange.getUpper() * heightRange.getUpper() * (double)x / y),
+                    xAlign, widthRange);
+            int height = alignInRange(
+                    width * (double)y / x, yAlign, mCaps.getSupportedHeightsFor(width));
+            return new Size(width, height);
+        }
+
+
+        public boolean testExtreme(int x, int y, boolean flexYUV, boolean near) {
+            boolean skipped = true;
+            for (Size s : (near ? mNearMinMax : mMinMax).get(new Size(x, y))) {
+                if (test(s.getWidth(), s.getHeight(), false /* optional */, flexYUV)) {
+                    skipped = false;
+                }
+            }
+            return !skipped;
+        }
+
+        public boolean testArbitrary(boolean flexYUV) {
+            boolean skipped = true;
+            for (Size s : mArbitrary) {
+                if (test(s.getWidth(), s.getHeight(), false /* optional */, flexYUV)) {
+                    skipped = false;
+                }
+            }
+            return !skipped;
+        }
+
+        public boolean testSpecific(int width, int height, boolean flexYUV) {
+            // already tested by one of the min/max tests
+            if (mSizes.contains(new Size(width, height))) {
+                return false;
+            }
+            return test(width, height, true /* optional */, flexYUV);
+        }
+
+        private boolean test(int width, int height, boolean optional, boolean flexYUV) {
+            Log.i(TAG, "testing " + mMime + " on " + mName + " for " + width + "x" + height
+                    + (flexYUV ? " flexYUV" : " surface"));
+
+            VideoProcessorBase processor =
+                flexYUV ? new VideoProcessor() : new SurfaceVideoProcessor();
+
+            // We are using a resource URL as an example
+            return processor.processLoop(
+                    SOURCE_URL, mMime, mName, width, height, optional);
+        }
+
+    }
+
+    private Encoder[] googH265()  { return goog(MediaFormat.MIMETYPE_VIDEO_HEVC); }
+    private Encoder[] googH264()  { return goog(MediaFormat.MIMETYPE_VIDEO_AVC); }
+    private Encoder[] googH263()  { return goog(MediaFormat.MIMETYPE_VIDEO_H263); }
+    private Encoder[] googMpeg4() { return goog(MediaFormat.MIMETYPE_VIDEO_MPEG4); }
+    private Encoder[] googVP8()   { return goog(MediaFormat.MIMETYPE_VIDEO_VP8); }
+    private Encoder[] googVP9()   { return goog(MediaFormat.MIMETYPE_VIDEO_VP9); }
+
+    private Encoder[] otherH265()  { return other(MediaFormat.MIMETYPE_VIDEO_HEVC); }
+    private Encoder[] otherH264()  { return other(MediaFormat.MIMETYPE_VIDEO_AVC); }
+    private Encoder[] otherH263()  { return other(MediaFormat.MIMETYPE_VIDEO_H263); }
+    private Encoder[] otherMpeg4() { return other(MediaFormat.MIMETYPE_VIDEO_MPEG4); }
+    private Encoder[] otherVP8()   { return other(MediaFormat.MIMETYPE_VIDEO_VP8); }
+    private Encoder[] otherVP9()   { return other(MediaFormat.MIMETYPE_VIDEO_VP9); }
+
+    private Encoder[] goog(String mime) {
+        return encoders(mime, true /* goog */);
+    }
+
+    private Encoder[] other(String mime) {
+        return encoders(mime, false /* goog */);
+    }
+
+    private Encoder[] encoders(String mime, boolean goog) {
+        MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+        ArrayList<Encoder> result = new ArrayList<Encoder>();
+
+        for (MediaCodecInfo info : mcl.getCodecInfos()) {
+            if (!info.isEncoder()
+                    || info.getName().toLowerCase().startsWith("omx.google.") != goog) {
+                continue;
+            }
+            try {
+                CodecCapabilities caps = info.getCapabilitiesForType(mime);
+                result.add(new Encoder(info.getName(), mime, caps));
+            } catch (IllegalArgumentException e) { // mime is not supported
+            }
+        }
+        return result.toArray(new Encoder[result.size()]);
+    }
+
+    public void testGoogH265FlexMinMin()   { minmin(googH265(),   true /* flex */); }
+    public void testGoogH265SurfMinMin()   { minmin(googH265(),   false /* flex */); }
+    public void testGoogH264FlexMinMin()   { minmin(googH264(),   true /* flex */); }
+    public void testGoogH264SurfMinMin()   { minmin(googH264(),   false /* flex */); }
+    public void testGoogH263FlexMinMin()   { minmin(googH263(),   true /* flex */); }
+    public void testGoogH263SurfMinMin()   { minmin(googH263(),   false /* flex */); }
+    public void testGoogMpeg4FlexMinMin()  { minmin(googMpeg4(),  true /* flex */); }
+    public void testGoogMpeg4SurfMinMin()  { minmin(googMpeg4(),  false /* flex */); }
+    public void testGoogVP8FlexMinMin()    { minmin(googVP8(),    true /* flex */); }
+    public void testGoogVP8SurfMinMin()    { minmin(googVP8(),    false /* flex */); }
+    public void testGoogVP9FlexMinMin()    { minmin(googVP9(),    true /* flex */); }
+    public void testGoogVP9SurfMinMin()    { minmin(googVP9(),    false /* flex */); }
+
+    public void testOtherH265FlexMinMin()  { minmin(otherH265(),  true /* flex */); }
+    public void testOtherH265SurfMinMin()  { minmin(otherH265(),  false /* flex */); }
+    public void testOtherH264FlexMinMin()  { minmin(otherH264(),  true /* flex */); }
+    public void testOtherH264SurfMinMin()  { minmin(otherH264(),  false /* flex */); }
+    public void testOtherH263FlexMinMin()  { minmin(otherH263(),  true /* flex */); }
+    public void testOtherH263SurfMinMin()  { minmin(otherH263(),  false /* flex */); }
+    public void testOtherMpeg4FlexMinMin() { minmin(otherMpeg4(), true /* flex */); }
+    public void testOtherMpeg4SurfMinMin() { minmin(otherMpeg4(), false /* flex */); }
+    public void testOtherVP8FlexMinMin()   { minmin(otherVP8(),   true /* flex */); }
+    public void testOtherVP8SurfMinMin()   { minmin(otherVP8(),   false /* flex */); }
+    public void testOtherVP9FlexMinMin()   { minmin(otherVP9(),   true /* flex */); }
+    public void testOtherVP9SurfMinMin()   { minmin(otherVP9(),   false /* flex */); }
+
+    public void testGoogH265FlexMinMax()   { minmax(googH265(),   true /* flex */); }
+    public void testGoogH265SurfMinMax()   { minmax(googH265(),   false /* flex */); }
+    public void testGoogH264FlexMinMax()   { minmax(googH264(),   true /* flex */); }
+    public void testGoogH264SurfMinMax()   { minmax(googH264(),   false /* flex */); }
+    public void testGoogH263FlexMinMax()   { minmax(googH263(),   true /* flex */); }
+    public void testGoogH263SurfMinMax()   { minmax(googH263(),   false /* flex */); }
+    public void testGoogMpeg4FlexMinMax()  { minmax(googMpeg4(),  true /* flex */); }
+    public void testGoogMpeg4SurfMinMax()  { minmax(googMpeg4(),  false /* flex */); }
+    public void testGoogVP8FlexMinMax()    { minmax(googVP8(),    true /* flex */); }
+    public void testGoogVP8SurfMinMax()    { minmax(googVP8(),    false /* flex */); }
+    public void testGoogVP9FlexMinMax()    { minmax(googVP9(),    true /* flex */); }
+    public void testGoogVP9SurfMinMax()    { minmax(googVP9(),    false /* flex */); }
+
+    public void testOtherH265FlexMinMax()  { minmax(otherH265(),  true /* flex */); }
+    public void testOtherH265SurfMinMax()  { minmax(otherH265(),  false /* flex */); }
+    public void testOtherH264FlexMinMax()  { minmax(otherH264(),  true /* flex */); }
+    public void testOtherH264SurfMinMax()  { minmax(otherH264(),  false /* flex */); }
+    public void testOtherH263FlexMinMax()  { minmax(otherH263(),  true /* flex */); }
+    public void testOtherH263SurfMinMax()  { minmax(otherH263(),  false /* flex */); }
+    public void testOtherMpeg4FlexMinMax() { minmax(otherMpeg4(), true /* flex */); }
+    public void testOtherMpeg4SurfMinMax() { minmax(otherMpeg4(), false /* flex */); }
+    public void testOtherVP8FlexMinMax()   { minmax(otherVP8(),   true /* flex */); }
+    public void testOtherVP8SurfMinMax()   { minmax(otherVP8(),   false /* flex */); }
+    public void testOtherVP9FlexMinMax()   { minmax(otherVP9(),   true /* flex */); }
+    public void testOtherVP9SurfMinMax()   { minmax(otherVP9(),   false /* flex */); }
+
+    public void testGoogH265FlexMaxMin()   { maxmin(googH265(),   true /* flex */); }
+    public void testGoogH265SurfMaxMin()   { maxmin(googH265(),   false /* flex */); }
+    public void testGoogH264FlexMaxMin()   { maxmin(googH264(),   true /* flex */); }
+    public void testGoogH264SurfMaxMin()   { maxmin(googH264(),   false /* flex */); }
+    public void testGoogH263FlexMaxMin()   { maxmin(googH263(),   true /* flex */); }
+    public void testGoogH263SurfMaxMin()   { maxmin(googH263(),   false /* flex */); }
+    public void testGoogMpeg4FlexMaxMin()  { maxmin(googMpeg4(),  true /* flex */); }
+    public void testGoogMpeg4SurfMaxMin()  { maxmin(googMpeg4(),  false /* flex */); }
+    public void testGoogVP8FlexMaxMin()    { maxmin(googVP8(),    true /* flex */); }
+    public void testGoogVP8SurfMaxMin()    { maxmin(googVP8(),    false /* flex */); }
+    public void testGoogVP9FlexMaxMin()    { maxmin(googVP9(),    true /* flex */); }
+    public void testGoogVP9SurfMaxMin()    { maxmin(googVP9(),    false /* flex */); }
+
+    public void testOtherH265FlexMaxMin()  { maxmin(otherH265(),  true /* flex */); }
+    public void testOtherH265SurfMaxMin()  { maxmin(otherH265(),  false /* flex */); }
+    public void testOtherH264FlexMaxMin()  { maxmin(otherH264(),  true /* flex */); }
+    public void testOtherH264SurfMaxMin()  { maxmin(otherH264(),  false /* flex */); }
+    public void testOtherH263FlexMaxMin()  { maxmin(otherH263(),  true /* flex */); }
+    public void testOtherH263SurfMaxMin()  { maxmin(otherH263(),  false /* flex */); }
+    public void testOtherMpeg4FlexMaxMin() { maxmin(otherMpeg4(), true /* flex */); }
+    public void testOtherMpeg4SurfMaxMin() { maxmin(otherMpeg4(), false /* flex */); }
+    public void testOtherVP8FlexMaxMin()   { maxmin(otherVP8(),   true /* flex */); }
+    public void testOtherVP8SurfMaxMin()   { maxmin(otherVP8(),   false /* flex */); }
+    public void testOtherVP9FlexMaxMin()   { maxmin(otherVP9(),   true /* flex */); }
+    public void testOtherVP9SurfMaxMin()   { maxmin(otherVP9(),   false /* flex */); }
+
+    public void testGoogH265FlexMaxMax()   { maxmax(googH265(),   true /* flex */); }
+    public void testGoogH265SurfMaxMax()   { maxmax(googH265(),   false /* flex */); }
+    public void testGoogH264FlexMaxMax()   { maxmax(googH264(),   true /* flex */); }
+    public void testGoogH264SurfMaxMax()   { maxmax(googH264(),   false /* flex */); }
+    public void testGoogH263FlexMaxMax()   { maxmax(googH263(),   true /* flex */); }
+    public void testGoogH263SurfMaxMax()   { maxmax(googH263(),   false /* flex */); }
+    public void testGoogMpeg4FlexMaxMax()  { maxmax(googMpeg4(),  true /* flex */); }
+    public void testGoogMpeg4SurfMaxMax()  { maxmax(googMpeg4(),  false /* flex */); }
+    public void testGoogVP8FlexMaxMax()    { maxmax(googVP8(),    true /* flex */); }
+    public void testGoogVP8SurfMaxMax()    { maxmax(googVP8(),    false /* flex */); }
+    public void testGoogVP9FlexMaxMax()    { maxmax(googVP9(),    true /* flex */); }
+    public void testGoogVP9SurfMaxMax()    { maxmax(googVP9(),    false /* flex */); }
+
+    public void testOtherH265FlexMaxMax()  { maxmax(otherH265(),  true /* flex */); }
+    public void testOtherH265SurfMaxMax()  { maxmax(otherH265(),  false /* flex */); }
+    public void testOtherH264FlexMaxMax()  { maxmax(otherH264(),  true /* flex */); }
+    public void testOtherH264SurfMaxMax()  { maxmax(otherH264(),  false /* flex */); }
+    public void testOtherH263FlexMaxMax()  { maxmax(otherH263(),  true /* flex */); }
+    public void testOtherH263SurfMaxMax()  { maxmax(otherH263(),  false /* flex */); }
+    public void testOtherMpeg4FlexMaxMax() { maxmax(otherMpeg4(), true /* flex */); }
+    public void testOtherMpeg4SurfMaxMax() { maxmax(otherMpeg4(), false /* flex */); }
+    public void testOtherVP8FlexMaxMax()   { maxmax(otherVP8(),   true /* flex */); }
+    public void testOtherVP8SurfMaxMax()   { maxmax(otherVP8(),   false /* flex */); }
+    public void testOtherVP9FlexMaxMax()   { maxmax(otherVP9(),   true /* flex */); }
+    public void testOtherVP9SurfMaxMax()   { maxmax(otherVP9(),   false /* flex */); }
+
+    public void testGoogH265FlexNearMinMin()   { nearminmin(googH265(),   true /* flex */); }
+    public void testGoogH265SurfNearMinMin()   { nearminmin(googH265(),   false /* flex */); }
+    public void testGoogH264FlexNearMinMin()   { nearminmin(googH264(),   true /* flex */); }
+    public void testGoogH264SurfNearMinMin()   { nearminmin(googH264(),   false /* flex */); }
+    public void testGoogH263FlexNearMinMin()   { nearminmin(googH263(),   true /* flex */); }
+    public void testGoogH263SurfNearMinMin()   { nearminmin(googH263(),   false /* flex */); }
+    public void testGoogMpeg4FlexNearMinMin()  { nearminmin(googMpeg4(),  true /* flex */); }
+    public void testGoogMpeg4SurfNearMinMin()  { nearminmin(googMpeg4(),  false /* flex */); }
+    public void testGoogVP8FlexNearMinMin()    { nearminmin(googVP8(),    true /* flex */); }
+    public void testGoogVP8SurfNearMinMin()    { nearminmin(googVP8(),    false /* flex */); }
+    public void testGoogVP9FlexNearMinMin()    { nearminmin(googVP9(),    true /* flex */); }
+    public void testGoogVP9SurfNearMinMin()    { nearminmin(googVP9(),    false /* flex */); }
+
+    public void testOtherH265FlexNearMinMin()  { nearminmin(otherH265(),  true /* flex */); }
+    public void testOtherH265SurfNearMinMin()  { nearminmin(otherH265(),  false /* flex */); }
+    public void testOtherH264FlexNearMinMin()  { nearminmin(otherH264(),  true /* flex */); }
+    public void testOtherH264SurfNearMinMin()  { nearminmin(otherH264(),  false /* flex */); }
+    public void testOtherH263FlexNearMinMin()  { nearminmin(otherH263(),  true /* flex */); }
+    public void testOtherH263SurfNearMinMin()  { nearminmin(otherH263(),  false /* flex */); }
+    public void testOtherMpeg4FlexNearMinMin() { nearminmin(otherMpeg4(), true /* flex */); }
+    public void testOtherMpeg4SurfNearMinMin() { nearminmin(otherMpeg4(), false /* flex */); }
+    public void testOtherVP8FlexNearMinMin()   { nearminmin(otherVP8(),   true /* flex */); }
+    public void testOtherVP8SurfNearMinMin()   { nearminmin(otherVP8(),   false /* flex */); }
+    public void testOtherVP9FlexNearMinMin()   { nearminmin(otherVP9(),   true /* flex */); }
+    public void testOtherVP9SurfNearMinMin()   { nearminmin(otherVP9(),   false /* flex */); }
+
+    public void testGoogH265FlexNearMinMax()   { nearminmax(googH265(),   true /* flex */); }
+    public void testGoogH265SurfNearMinMax()   { nearminmax(googH265(),   false /* flex */); }
+    public void testGoogH264FlexNearMinMax()   { nearminmax(googH264(),   true /* flex */); }
+    public void testGoogH264SurfNearMinMax()   { nearminmax(googH264(),   false /* flex */); }
+    public void testGoogH263FlexNearMinMax()   { nearminmax(googH263(),   true /* flex */); }
+    public void testGoogH263SurfNearMinMax()   { nearminmax(googH263(),   false /* flex */); }
+    public void testGoogMpeg4FlexNearMinMax()  { nearminmax(googMpeg4(),  true /* flex */); }
+    public void testGoogMpeg4SurfNearMinMax()  { nearminmax(googMpeg4(),  false /* flex */); }
+    public void testGoogVP8FlexNearMinMax()    { nearminmax(googVP8(),    true /* flex */); }
+    public void testGoogVP8SurfNearMinMax()    { nearminmax(googVP8(),    false /* flex */); }
+    public void testGoogVP9FlexNearMinMax()    { nearminmax(googVP9(),    true /* flex */); }
+    public void testGoogVP9SurfNearMinMax()    { nearminmax(googVP9(),    false /* flex */); }
+
+    public void testOtherH265FlexNearMinMax()  { nearminmax(otherH265(),  true /* flex */); }
+    public void testOtherH265SurfNearMinMax()  { nearminmax(otherH265(),  false /* flex */); }
+    public void testOtherH264FlexNearMinMax()  { nearminmax(otherH264(),  true /* flex */); }
+    public void testOtherH264SurfNearMinMax()  { nearminmax(otherH264(),  false /* flex */); }
+    public void testOtherH263FlexNearMinMax()  { nearminmax(otherH263(),  true /* flex */); }
+    public void testOtherH263SurfNearMinMax()  { nearminmax(otherH263(),  false /* flex */); }
+    public void testOtherMpeg4FlexNearMinMax() { nearminmax(otherMpeg4(), true /* flex */); }
+    public void testOtherMpeg4SurfNearMinMax() { nearminmax(otherMpeg4(), false /* flex */); }
+    public void testOtherVP8FlexNearMinMax()   { nearminmax(otherVP8(),   true /* flex */); }
+    public void testOtherVP8SurfNearMinMax()   { nearminmax(otherVP8(),   false /* flex */); }
+    public void testOtherVP9FlexNearMinMax()   { nearminmax(otherVP9(),   true /* flex */); }
+    public void testOtherVP9SurfNearMinMax()   { nearminmax(otherVP9(),   false /* flex */); }
+
+    public void testGoogH265FlexNearMaxMin()   { nearmaxmin(googH265(),   true /* flex */); }
+    public void testGoogH265SurfNearMaxMin()   { nearmaxmin(googH265(),   false /* flex */); }
+    public void testGoogH264FlexNearMaxMin()   { nearmaxmin(googH264(),   true /* flex */); }
+    public void testGoogH264SurfNearMaxMin()   { nearmaxmin(googH264(),   false /* flex */); }
+    public void testGoogH263FlexNearMaxMin()   { nearmaxmin(googH263(),   true /* flex */); }
+    public void testGoogH263SurfNearMaxMin()   { nearmaxmin(googH263(),   false /* flex */); }
+    public void testGoogMpeg4FlexNearMaxMin()  { nearmaxmin(googMpeg4(),  true /* flex */); }
+    public void testGoogMpeg4SurfNearMaxMin()  { nearmaxmin(googMpeg4(),  false /* flex */); }
+    public void testGoogVP8FlexNearMaxMin()    { nearmaxmin(googVP8(),    true /* flex */); }
+    public void testGoogVP8SurfNearMaxMin()    { nearmaxmin(googVP8(),    false /* flex */); }
+    public void testGoogVP9FlexNearMaxMin()    { nearmaxmin(googVP9(),    true /* flex */); }
+    public void testGoogVP9SurfNearMaxMin()    { nearmaxmin(googVP9(),    false /* flex */); }
+
+    public void testOtherH265FlexNearMaxMin()  { nearmaxmin(otherH265(),  true /* flex */); }
+    public void testOtherH265SurfNearMaxMin()  { nearmaxmin(otherH265(),  false /* flex */); }
+    public void testOtherH264FlexNearMaxMin()  { nearmaxmin(otherH264(),  true /* flex */); }
+    public void testOtherH264SurfNearMaxMin()  { nearmaxmin(otherH264(),  false /* flex */); }
+    public void testOtherH263FlexNearMaxMin()  { nearmaxmin(otherH263(),  true /* flex */); }
+    public void testOtherH263SurfNearMaxMin()  { nearmaxmin(otherH263(),  false /* flex */); }
+    public void testOtherMpeg4FlexNearMaxMin() { nearmaxmin(otherMpeg4(), true /* flex */); }
+    public void testOtherMpeg4SurfNearMaxMin() { nearmaxmin(otherMpeg4(), false /* flex */); }
+    public void testOtherVP8FlexNearMaxMin()   { nearmaxmin(otherVP8(),   true /* flex */); }
+    public void testOtherVP8SurfNearMaxMin()   { nearmaxmin(otherVP8(),   false /* flex */); }
+    public void testOtherVP9FlexNearMaxMin()   { nearmaxmin(otherVP9(),   true /* flex */); }
+    public void testOtherVP9SurfNearMaxMin()   { nearmaxmin(otherVP9(),   false /* flex */); }
+
+    public void testGoogH265FlexNearMaxMax()   { nearmaxmax(googH265(),   true /* flex */); }
+    public void testGoogH265SurfNearMaxMax()   { nearmaxmax(googH265(),   false /* flex */); }
+    public void testGoogH264FlexNearMaxMax()   { nearmaxmax(googH264(),   true /* flex */); }
+    public void testGoogH264SurfNearMaxMax()   { nearmaxmax(googH264(),   false /* flex */); }
+    public void testGoogH263FlexNearMaxMax()   { nearmaxmax(googH263(),   true /* flex */); }
+    public void testGoogH263SurfNearMaxMax()   { nearmaxmax(googH263(),   false /* flex */); }
+    public void testGoogMpeg4FlexNearMaxMax()  { nearmaxmax(googMpeg4(),  true /* flex */); }
+    public void testGoogMpeg4SurfNearMaxMax()  { nearmaxmax(googMpeg4(),  false /* flex */); }
+    public void testGoogVP8FlexNearMaxMax()    { nearmaxmax(googVP8(),    true /* flex */); }
+    public void testGoogVP8SurfNearMaxMax()    { nearmaxmax(googVP8(),    false /* flex */); }
+    public void testGoogVP9FlexNearMaxMax()    { nearmaxmax(googVP9(),    true /* flex */); }
+    public void testGoogVP9SurfNearMaxMax()    { nearmaxmax(googVP9(),    false /* flex */); }
+
+    public void testOtherH265FlexNearMaxMax()  { nearmaxmax(otherH265(),  true /* flex */); }
+    public void testOtherH265SurfNearMaxMax()  { nearmaxmax(otherH265(),  false /* flex */); }
+    public void testOtherH264FlexNearMaxMax()  { nearmaxmax(otherH264(),  true /* flex */); }
+    public void testOtherH264SurfNearMaxMax()  { nearmaxmax(otherH264(),  false /* flex */); }
+    public void testOtherH263FlexNearMaxMax()  { nearmaxmax(otherH263(),  true /* flex */); }
+    public void testOtherH263SurfNearMaxMax()  { nearmaxmax(otherH263(),  false /* flex */); }
+    public void testOtherMpeg4FlexNearMaxMax() { nearmaxmax(otherMpeg4(), true /* flex */); }
+    public void testOtherMpeg4SurfNearMaxMax() { nearmaxmax(otherMpeg4(), false /* flex */); }
+    public void testOtherVP8FlexNearMaxMax()   { nearmaxmax(otherVP8(),   true /* flex */); }
+    public void testOtherVP8SurfNearMaxMax()   { nearmaxmax(otherVP8(),   false /* flex */); }
+    public void testOtherVP9FlexNearMaxMax()   { nearmaxmax(otherVP9(),   true /* flex */); }
+    public void testOtherVP9SurfNearMaxMax()   { nearmaxmax(otherVP9(),   false /* flex */); }
+
+    public void testGoogH265FlexArbitrary()   { arbitrary(googH265(),   true /* flex */); }
+    public void testGoogH265SurfArbitrary()   { arbitrary(googH265(),   false /* flex */); }
+    public void testGoogH264FlexArbitrary()   { arbitrary(googH264(),   true /* flex */); }
+    public void testGoogH264SurfArbitrary()   { arbitrary(googH264(),   false /* flex */); }
+    public void testGoogH263FlexArbitrary()   { arbitrary(googH263(),   true /* flex */); }
+    public void testGoogH263SurfArbitrary()   { arbitrary(googH263(),   false /* flex */); }
+    public void testGoogMpeg4FlexArbitrary()  { arbitrary(googMpeg4(),  true /* flex */); }
+    public void testGoogMpeg4SurfArbitrary()  { arbitrary(googMpeg4(),  false /* flex */); }
+    public void testGoogVP8FlexArbitrary()    { arbitrary(googVP8(),    true /* flex */); }
+    public void testGoogVP8SurfArbitrary()    { arbitrary(googVP8(),    false /* flex */); }
+    public void testGoogVP9FlexArbitrary()    { arbitrary(googVP9(),    true /* flex */); }
+    public void testGoogVP9SurfArbitrary()    { arbitrary(googVP9(),    false /* flex */); }
+
+    public void testOtherH265FlexArbitrary()  { arbitrary(otherH265(),  true /* flex */); }
+    public void testOtherH265SurfArbitrary()  { arbitrary(otherH265(),  false /* flex */); }
+    public void testOtherH264FlexArbitrary()  { arbitrary(otherH264(),  true /* flex */); }
+    public void testOtherH264SurfArbitrary()  { arbitrary(otherH264(),  false /* flex */); }
+    public void testOtherH263FlexArbitrary()  { arbitrary(otherH263(),  true /* flex */); }
+    public void testOtherH263SurfArbitrary()  { arbitrary(otherH263(),  false /* flex */); }
+    public void testOtherMpeg4FlexArbitrary() { arbitrary(otherMpeg4(), true /* flex */); }
+    public void testOtherMpeg4SurfArbitrary() { arbitrary(otherMpeg4(), false /* flex */); }
+    public void testOtherVP8FlexArbitrary()   { arbitrary(otherVP8(),   true /* flex */); }
+    public void testOtherVP8SurfArbitrary()   { arbitrary(otherVP8(),   false /* flex */); }
+    public void testOtherVP9FlexArbitrary()   { arbitrary(otherVP9(),   true /* flex */); }
+    public void testOtherVP9SurfArbitrary()   { arbitrary(otherVP9(),   false /* flex */); }
+
+    public void testGoogH265FlexQCIF()   { specific(googH265(),   176, 144, true /* flex */); }
+    public void testGoogH265SurfQCIF()   { specific(googH265(),   176, 144, false /* flex */); }
+    public void testGoogH264FlexQCIF()   { specific(googH264(),   176, 144, true /* flex */); }
+    public void testGoogH264SurfQCIF()   { specific(googH264(),   176, 144, false /* flex */); }
+    public void testGoogH263FlexQCIF()   { specific(googH263(),   176, 144, true /* flex */); }
+    public void testGoogH263SurfQCIF()   { specific(googH263(),   176, 144, false /* flex */); }
+    public void testGoogMpeg4FlexQCIF()  { specific(googMpeg4(),  176, 144, true /* flex */); }
+    public void testGoogMpeg4SurfQCIF()  { specific(googMpeg4(),  176, 144, false /* flex */); }
+    public void testGoogVP8FlexQCIF()    { specific(googVP8(),    176, 144, true /* flex */); }
+    public void testGoogVP8SurfQCIF()    { specific(googVP8(),    176, 144, false /* flex */); }
+    public void testGoogVP9FlexQCIF()    { specific(googVP9(),    176, 144, true /* flex */); }
+    public void testGoogVP9SurfQCIF()    { specific(googVP9(),    176, 144, false /* flex */); }
+
+    public void testOtherH265FlexQCIF()  { specific(otherH265(),  176, 144, true /* flex */); }
+    public void testOtherH265SurfQCIF()  { specific(otherH265(),  176, 144, false /* flex */); }
+    public void testOtherH264FlexQCIF()  { specific(otherH264(),  176, 144, true /* flex */); }
+    public void testOtherH264SurfQCIF()  { specific(otherH264(),  176, 144, false /* flex */); }
+    public void testOtherH263FlexQCIF()  { specific(otherH263(),  176, 144, true /* flex */); }
+    public void testOtherH263SurfQCIF()  { specific(otherH263(),  176, 144, false /* flex */); }
+    public void testOtherMpeg4FlexQCIF() { specific(otherMpeg4(), 176, 144, true /* flex */); }
+    public void testOtherMpeg4SurfQCIF() { specific(otherMpeg4(), 176, 144, false /* flex */); }
+    public void testOtherVP8FlexQCIF()   { specific(otherVP8(),   176, 144, true /* flex */); }
+    public void testOtherVP8SurfQCIF()   { specific(otherVP8(),   176, 144, false /* flex */); }
+    public void testOtherVP9FlexQCIF()   { specific(otherVP9(),   176, 144, true /* flex */); }
+    public void testOtherVP9SurfQCIF()   { specific(otherVP9(),   176, 144, false /* flex */); }
+
+    public void testGoogH265Flex480p()   { specific(googH265(),   720, 480, true /* flex */); }
+    public void testGoogH265Surf480p()   { specific(googH265(),   720, 480, false /* flex */); }
+    public void testGoogH264Flex480p()   { specific(googH264(),   720, 480, true /* flex */); }
+    public void testGoogH264Surf480p()   { specific(googH264(),   720, 480, false /* flex */); }
+    public void testGoogH263Flex480p()   { specific(googH263(),   720, 480, true /* flex */); }
+    public void testGoogH263Surf480p()   { specific(googH263(),   720, 480, false /* flex */); }
+    public void testGoogMpeg4Flex480p()  { specific(googMpeg4(),  720, 480, true /* flex */); }
+    public void testGoogMpeg4Surf480p()  { specific(googMpeg4(),  720, 480, false /* flex */); }
+    public void testGoogVP8Flex480p()    { specific(googVP8(),    720, 480, true /* flex */); }
+    public void testGoogVP8Surf480p()    { specific(googVP8(),    720, 480, false /* flex */); }
+    public void testGoogVP9Flex480p()    { specific(googVP9(),    720, 480, true /* flex */); }
+    public void testGoogVP9Surf480p()    { specific(googVP9(),    720, 480, false /* flex */); }
+
+    public void testOtherH265Flex480p()  { specific(otherH265(),  720, 480, true /* flex */); }
+    public void testOtherH265Surf480p()  { specific(otherH265(),  720, 480, false /* flex */); }
+    public void testOtherH264Flex480p()  { specific(otherH264(),  720, 480, true /* flex */); }
+    public void testOtherH264Surf480p()  { specific(otherH264(),  720, 480, false /* flex */); }
+    public void testOtherH263Flex480p()  { specific(otherH263(),  720, 480, true /* flex */); }
+    public void testOtherH263Surf480p()  { specific(otherH263(),  720, 480, false /* flex */); }
+    public void testOtherMpeg4Flex480p() { specific(otherMpeg4(), 720, 480, true /* flex */); }
+    public void testOtherMpeg4Surf480p() { specific(otherMpeg4(), 720, 480, false /* flex */); }
+    public void testOtherVP8Flex480p()   { specific(otherVP8(),   720, 480, true /* flex */); }
+    public void testOtherVP8Surf480p()   { specific(otherVP8(),   720, 480, false /* flex */); }
+    public void testOtherVP9Flex480p()   { specific(otherVP9(),   720, 480, true /* flex */); }
+    public void testOtherVP9Surf480p()   { specific(otherVP9(),   720, 480, false /* flex */); }
+
+    // even though H.263 and MPEG-4 are not defined for 720p or 1080p
+    // test for it, in case device claims support for it.
+
+    public void testGoogH265Flex720p()   { specific(googH265(),   1280, 720, true /* flex */); }
+    public void testGoogH265Surf720p()   { specific(googH265(),   1280, 720, false /* flex */); }
+    public void testGoogH264Flex720p()   { specific(googH264(),   1280, 720, true /* flex */); }
+    public void testGoogH264Surf720p()   { specific(googH264(),   1280, 720, false /* flex */); }
+    public void testGoogH263Flex720p()   { specific(googH263(),   1280, 720, true /* flex */); }
+    public void testGoogH263Surf720p()   { specific(googH263(),   1280, 720, false /* flex */); }
+    public void testGoogMpeg4Flex720p()  { specific(googMpeg4(),  1280, 720, true /* flex */); }
+    public void testGoogMpeg4Surf720p()  { specific(googMpeg4(),  1280, 720, false /* flex */); }
+    public void testGoogVP8Flex720p()    { specific(googVP8(),    1280, 720, true /* flex */); }
+    public void testGoogVP8Surf720p()    { specific(googVP8(),    1280, 720, false /* flex */); }
+    public void testGoogVP9Flex720p()    { specific(googVP9(),    1280, 720, true /* flex */); }
+    public void testGoogVP9Surf720p()    { specific(googVP9(),    1280, 720, false /* flex */); }
+
+    public void testOtherH265Flex720p()  { specific(otherH265(),  1280, 720, true /* flex */); }
+    public void testOtherH265Surf720p()  { specific(otherH265(),  1280, 720, false /* flex */); }
+    public void testOtherH264Flex720p()  { specific(otherH264(),  1280, 720, true /* flex */); }
+    public void testOtherH264Surf720p()  { specific(otherH264(),  1280, 720, false /* flex */); }
+    public void testOtherH263Flex720p()  { specific(otherH263(),  1280, 720, true /* flex */); }
+    public void testOtherH263Surf720p()  { specific(otherH263(),  1280, 720, false /* flex */); }
+    public void testOtherMpeg4Flex720p() { specific(otherMpeg4(), 1280, 720, true /* flex */); }
+    public void testOtherMpeg4Surf720p() { specific(otherMpeg4(), 1280, 720, false /* flex */); }
+    public void testOtherVP8Flex720p()   { specific(otherVP8(),   1280, 720, true /* flex */); }
+    public void testOtherVP8Surf720p()   { specific(otherVP8(),   1280, 720, false /* flex */); }
+    public void testOtherVP9Flex720p()   { specific(otherVP9(),   1280, 720, true /* flex */); }
+    public void testOtherVP9Surf720p()   { specific(otherVP9(),   1280, 720, false /* flex */); }
+
+    public void testGoogH265Flex1080p()   { specific(googH265(),   1920, 1080, true /* flex */); }
+    public void testGoogH265Surf1080p()   { specific(googH265(),   1920, 1080, false /* flex */); }
+    public void testGoogH264Flex1080p()   { specific(googH264(),   1920, 1080, true /* flex */); }
+    public void testGoogH264Surf1080p()   { specific(googH264(),   1920, 1080, false /* flex */); }
+    public void testGoogH263Flex1080p()   { specific(googH263(),   1920, 1080, true /* flex */); }
+    public void testGoogH263Surf1080p()   { specific(googH263(),   1920, 1080, false /* flex */); }
+    public void testGoogMpeg4Flex1080p()  { specific(googMpeg4(),  1920, 1080, true /* flex */); }
+    public void testGoogMpeg4Surf1080p()  { specific(googMpeg4(),  1920, 1080, false /* flex */); }
+    public void testGoogVP8Flex1080p()    { specific(googVP8(),    1920, 1080, true /* flex */); }
+    public void testGoogVP8Surf1080p()    { specific(googVP8(),    1920, 1080, false /* flex */); }
+    public void testGoogVP9Flex1080p()    { specific(googVP9(),    1920, 1080, true /* flex */); }
+    public void testGoogVP9Surf1080p()    { specific(googVP9(),    1920, 1080, false /* flex */); }
+
+    public void testOtherH265Flex1080p()  { specific(otherH265(),  1920, 1080, true /* flex */); }
+    public void testOtherH265Surf1080p()  { specific(otherH265(),  1920, 1080, false /* flex */); }
+    public void testOtherH264Flex1080p()  { specific(otherH264(),  1920, 1080, true /* flex */); }
+    public void testOtherH264Surf1080p()  { specific(otherH264(),  1920, 1080, false /* flex */); }
+    public void testOtherH263Flex1080p()  { specific(otherH263(),  1920, 1080, true /* flex */); }
+    public void testOtherH263Surf1080p()  { specific(otherH263(),  1920, 1080, false /* flex */); }
+    public void testOtherMpeg4Flex1080p() { specific(otherMpeg4(), 1920, 1080, true /* flex */); }
+    public void testOtherMpeg4Surf1080p() { specific(otherMpeg4(), 1920, 1080, false /* flex */); }
+    public void testOtherVP8Flex1080p()   { specific(otherVP8(),   1920, 1080, true /* flex */); }
+    public void testOtherVP8Surf1080p()   { specific(otherVP8(),   1920, 1080, false /* flex */); }
+    public void testOtherVP9Flex1080p()   { specific(otherVP9(),   1920, 1080, true /* flex */); }
+    public void testOtherVP9Surf1080p()   { specific(otherVP9(),   1920, 1080, false /* flex */); }
+
+    private void minmin(Encoder[] encoders, boolean flexYUV) {
+        extreme(encoders, 0 /* x */, 0 /* y */, flexYUV, false /* near */);
+    }
+
+    private void minmax(Encoder[] encoders, boolean flexYUV) {
+        extreme(encoders, 0 /* x */, 1 /* y */, flexYUV, false /* near */);
+    }
+
+    private void maxmin(Encoder[] encoders, boolean flexYUV) {
+        extreme(encoders, 1 /* x */, 0 /* y */, flexYUV, false /* near */);
+    }
+
+    private void maxmax(Encoder[] encoders, boolean flexYUV) {
+        extreme(encoders, 1 /* x */, 1 /* y */, flexYUV, false /* near */);
+    }
+
+    private void nearminmin(Encoder[] encoders, boolean flexYUV) {
+        extreme(encoders, 0 /* x */, 0 /* y */, flexYUV, true /* near */);
+    }
+
+    private void nearminmax(Encoder[] encoders, boolean flexYUV) {
+        extreme(encoders, 0 /* x */, 1 /* y */, flexYUV, true /* near */);
+    }
+
+    private void nearmaxmin(Encoder[] encoders, boolean flexYUV) {
+        extreme(encoders, 1 /* x */, 0 /* y */, flexYUV, true /* near */);
+    }
+
+    private void nearmaxmax(Encoder[] encoders, boolean flexYUV) {
+        extreme(encoders, 1 /* x */, 1 /* y */, flexYUV, true /* near */);
+    }
+
+    private void extreme(Encoder[] encoders, int x, int y, boolean flexYUV, boolean near) {
+        boolean skipped = true;
+        if (encoders.length == 0) {
+            MediaUtils.skipTest("no such encoder present");
+            return;
+        }
+        for (Encoder encoder: encoders) {
+            if (encoder.testExtreme(x, y, flexYUV, near)) {
+                skipped = false;
+            }
+        }
+        if (skipped) {
+            MediaUtils.skipTest("duplicate resolution extreme");
+        }
+    }
+
+    private void arbitrary(Encoder[] encoders, boolean flexYUV) {
+        boolean skipped = true;
+        if (encoders.length == 0) {
+            MediaUtils.skipTest("no such encoder present");
+            return;
+        }
+        for (Encoder encoder: encoders) {
+            if (encoder.testArbitrary(flexYUV)) {
+                skipped = false;
+            }
+        }
+        if (skipped) {
+            MediaUtils.skipTest("duplicate resolution");
+        }
+    }
+
+    /* test specific size */
+    private void specific(Encoder[] encoders, int width, int height, boolean flexYUV) {
+        boolean skipped = true;
+        if (encoders.length == 0) {
+            MediaUtils.skipTest("no such encoder present");
+            return;
+        }
+        for (Encoder encoder : encoders) {
+            if (encoder.testSpecific(width, height, flexYUV)) {
+                skipped = false;
+            }
+        }
+        if (skipped) {
+            MediaUtils.skipTest("duplicate or unsupported resolution");
+        }
+    }
+}