blob: 6ba9168ee1776f230e40d92c5e752f0bd8552291 [file] [log] [blame]
/*
* Copyright 2016 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.webrtc;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.graphics.Bitmap;
import android.graphics.SurfaceTexture;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
// EmptyActivity is needed for the surface.
@RunWith(BaseJUnit4ClassRunner.class)
public class EglRendererTest {
private final static String TAG = "EglRendererTest";
private final static int RENDER_WAIT_MS = 1000;
private final static int SURFACE_WAIT_MS = 1000;
private final static int TEST_FRAME_WIDTH = 4;
private final static int TEST_FRAME_HEIGHT = 4;
private final static int REMOVE_FRAME_LISTENER_RACY_NUM_TESTS = 10;
// Some arbitrary frames.
private final static byte[][][] TEST_FRAMES_DATA = {
{
new byte[] {
11, -12, 13, -14, -15, 16, -17, 18, 19, -110, 111, -112, -113, 114, -115, 116},
new byte[] {117, 118, 119, 120}, new byte[] {121, 122, 123, 124},
},
{
new byte[] {-11, -12, -13, -14, -15, -16, -17, -18, -19, -110, -111, -112, -113, -114,
-115, -116},
new byte[] {-121, -122, -123, -124}, new byte[] {-117, -118, -119, -120},
},
{
new byte[] {-11, -12, -13, -14, -15, -16, -17, -18, -19, -110, -111, -112, -113, -114,
-115, -116},
new byte[] {117, 118, 119, 120}, new byte[] {121, 122, 123, 124},
},
};
private final static ByteBuffer[][] TEST_FRAMES =
copyTestDataToDirectByteBuffers(TEST_FRAMES_DATA);
private static class TestFrameListener implements EglRenderer.FrameListener {
final private ArrayList<Bitmap> bitmaps = new ArrayList<Bitmap>();
boolean bitmapReceived;
Bitmap storedBitmap;
@Override
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
@SuppressWarnings("NoSynchronizedMethodCheck")
public synchronized void onFrame(Bitmap bitmap) {
if (bitmapReceived) {
fail("Unexpected bitmap was received.");
}
bitmapReceived = true;
storedBitmap = bitmap;
notify();
}
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
@SuppressWarnings("NoSynchronizedMethodCheck")
public synchronized boolean waitForBitmap(int timeoutMs) throws InterruptedException {
final long endTimeMs = System.currentTimeMillis() + timeoutMs;
while (!bitmapReceived) {
final long waitTimeMs = endTimeMs - System.currentTimeMillis();
if (waitTimeMs < 0) {
return false;
}
wait(timeoutMs);
}
return true;
}
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
@SuppressWarnings("NoSynchronizedMethodCheck")
public synchronized Bitmap resetAndGetBitmap() {
bitmapReceived = false;
return storedBitmap;
}
}
final TestFrameListener testFrameListener = new TestFrameListener();
EglRenderer eglRenderer;
CountDownLatch surfaceReadyLatch = new CountDownLatch(1);
int oesTextureId;
SurfaceTexture surfaceTexture;
@Before
public void setUp() throws Exception {
PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions
.builder(InstrumentationRegistry.getTargetContext())
.createInitializationOptions());
eglRenderer = new EglRenderer("TestRenderer: ");
eglRenderer.init(null /* sharedContext */, EglBase.CONFIG_RGBA, new GlRectDrawer());
oesTextureId = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
surfaceTexture = new SurfaceTexture(oesTextureId);
surfaceTexture.setDefaultBufferSize(1 /* width */, 1 /* height */);
eglRenderer.createEglSurface(surfaceTexture);
}
@After
public void tearDown() {
surfaceTexture.release();
GLES20.glDeleteTextures(1 /* n */, new int[] {oesTextureId}, 0 /* offset */);
eglRenderer.release();
}
/** Checks the bitmap is not null and the correct size. */
private static void checkBitmap(Bitmap bitmap, float scale) {
assertNotNull(bitmap);
assertEquals((int) (TEST_FRAME_WIDTH * scale), bitmap.getWidth());
assertEquals((int) (TEST_FRAME_HEIGHT * scale), bitmap.getHeight());
}
/**
* Does linear sampling on U/V plane of test data.
*
* @param data Plane data to be sampled from.
* @param planeWidth Width of the plane data. This is also assumed to be the stride.
* @param planeHeight Height of the plane data.
* @param x X-coordinate in range [0, 1].
* @param y Y-coordinate in range [0, 1].
*/
private static float linearSample(
ByteBuffer plane, int planeWidth, int planeHeight, float x, float y) {
final int stride = planeWidth;
final float coordX = x * planeWidth;
final float coordY = y * planeHeight;
int lowIndexX = (int) Math.floor(coordX - 0.5f);
int lowIndexY = (int) Math.floor(coordY - 0.5f);
int highIndexX = lowIndexX + 1;
int highIndexY = lowIndexY + 1;
final float highWeightX = coordX - lowIndexX - 0.5f;
final float highWeightY = coordY - lowIndexY - 0.5f;
final float lowWeightX = 1f - highWeightX;
final float lowWeightY = 1f - highWeightY;
// Clamp on the edges.
lowIndexX = Math.max(0, lowIndexX);
lowIndexY = Math.max(0, lowIndexY);
highIndexX = Math.min(planeWidth - 1, highIndexX);
highIndexY = Math.min(planeHeight - 1, highIndexY);
float lowYValue = (plane.get(lowIndexY * stride + lowIndexX) & 0xFF) * lowWeightX
+ (plane.get(lowIndexY * stride + highIndexX) & 0xFF) * highWeightX;
float highYValue = (plane.get(highIndexY * stride + lowIndexX) & 0xFF) * lowWeightX
+ (plane.get(highIndexY * stride + highIndexX) & 0xFF) * highWeightX;
return (lowWeightY * lowYValue + highWeightY * highYValue) / 255f;
}
private static byte saturatedFloatToByte(float c) {
return (byte) Math.round(255f * Math.max(0f, Math.min(1f, c)));
}
/**
* Converts test data YUV frame to expected RGBA frame. Tries to match the behavior of OpenGL
* YUV drawer shader. Does linear sampling on the U- and V-planes.
*
* @param yuvFrame Array of size 3 containing Y-, U-, V-planes for image of size
* (TEST_FRAME_WIDTH, TEST_FRAME_HEIGHT). U- and V-planes should be half the size
* of the Y-plane.
*/
private static byte[] convertYUVFrameToRGBA(ByteBuffer[] yuvFrame) {
final byte[] argbFrame = new byte[TEST_FRAME_WIDTH * TEST_FRAME_HEIGHT * 4];
final int argbStride = TEST_FRAME_WIDTH * 4;
final int yStride = TEST_FRAME_WIDTH;
final int vStride = TEST_FRAME_WIDTH / 2;
for (int y = 0; y < TEST_FRAME_HEIGHT; y++) {
for (int x = 0; x < TEST_FRAME_WIDTH; x++) {
final int x2 = x / 2;
final int y2 = y / 2;
final float yC = (yuvFrame[0].get(y * yStride + x) & 0xFF) / 255f;
final float uC = linearSample(yuvFrame[1], TEST_FRAME_WIDTH / 2, TEST_FRAME_HEIGHT / 2,
(x + 0.5f) / TEST_FRAME_WIDTH, (y + 0.5f) / TEST_FRAME_HEIGHT)
- 0.5f;
final float vC = linearSample(yuvFrame[2], TEST_FRAME_WIDTH / 2, TEST_FRAME_HEIGHT / 2,
(x + 0.5f) / TEST_FRAME_WIDTH, (y + 0.5f) / TEST_FRAME_HEIGHT)
- 0.5f;
final float rC = yC + 1.403f * vC;
final float gC = yC - 0.344f * uC - 0.714f * vC;
final float bC = yC + 1.77f * uC;
argbFrame[y * argbStride + x * 4 + 0] = saturatedFloatToByte(rC);
argbFrame[y * argbStride + x * 4 + 1] = saturatedFloatToByte(gC);
argbFrame[y * argbStride + x * 4 + 2] = saturatedFloatToByte(bC);
argbFrame[y * argbStride + x * 4 + 3] = (byte) 255;
}
}
return argbFrame;
}
/** Checks that the bitmap content matches the test frame with the given index. */
private static void checkBitmapContent(Bitmap bitmap, int frame) {
checkBitmap(bitmap, 1f);
byte[] expectedRGBA = convertYUVFrameToRGBA(TEST_FRAMES[frame]);
ByteBuffer bitmapBuffer = ByteBuffer.allocateDirect(bitmap.getByteCount());
bitmap.copyPixelsToBuffer(bitmapBuffer);
for (int i = 0; i < expectedRGBA.length; i++) {
int expected = expectedRGBA[i] & 0xFF;
int value = bitmapBuffer.get(i) & 0xFF;
// Due to unknown conversion differences check value matches +-1.
if (Math.abs(value - expected) > 1) {
Logging.d(TAG, "Expected bitmap content: " + Arrays.toString(expectedRGBA));
Logging.d(TAG, "Bitmap content: " + Arrays.toString(bitmapBuffer.array()));
fail("Frame doesn't match original frame on byte " + i + ". Expected: " + expected
+ " Result: " + value);
}
}
}
/** Tells eglRenderer to render test frame with given index. */
private void feedFrame(int i) {
eglRenderer.renderFrame(new VideoRenderer.I420Frame(TEST_FRAME_WIDTH, TEST_FRAME_HEIGHT, 0,
new int[] {TEST_FRAME_WIDTH, TEST_FRAME_WIDTH / 2, TEST_FRAME_WIDTH / 2}, TEST_FRAMES[i],
0));
}
@Test
@SmallTest
public void testAddFrameListener() throws Exception {
eglRenderer.addFrameListener(testFrameListener, 0f /* scaleFactor */);
feedFrame(0);
assertTrue(testFrameListener.waitForBitmap(RENDER_WAIT_MS));
assertNull(testFrameListener.resetAndGetBitmap());
eglRenderer.addFrameListener(testFrameListener, 0f /* scaleFactor */);
feedFrame(1);
assertTrue(testFrameListener.waitForBitmap(RENDER_WAIT_MS));
assertNull(testFrameListener.resetAndGetBitmap());
feedFrame(2);
// Check we get no more bitmaps than two.
assertFalse(testFrameListener.waitForBitmap(RENDER_WAIT_MS));
}
@Test
@SmallTest
public void testAddFrameListenerBitmap() throws Exception {
eglRenderer.addFrameListener(testFrameListener, 1f /* scaleFactor */);
feedFrame(0);
assertTrue(testFrameListener.waitForBitmap(RENDER_WAIT_MS));
checkBitmapContent(testFrameListener.resetAndGetBitmap(), 0);
eglRenderer.addFrameListener(testFrameListener, 1f /* scaleFactor */);
feedFrame(1);
assertTrue(testFrameListener.waitForBitmap(RENDER_WAIT_MS));
checkBitmapContent(testFrameListener.resetAndGetBitmap(), 1);
}
@Test
@SmallTest
public void testAddFrameListenerBitmapScale() throws Exception {
for (int i = 0; i < 3; ++i) {
float scale = i * 0.5f + 0.5f;
eglRenderer.addFrameListener(testFrameListener, scale);
feedFrame(i);
assertTrue(testFrameListener.waitForBitmap(RENDER_WAIT_MS));
checkBitmap(testFrameListener.resetAndGetBitmap(), scale);
}
}
/**
* Checks that the frame listener will not be called with a frame that was delivered before the
* frame listener was added.
*/
@Test
@SmallTest
public void testFrameListenerNotCalledWithOldFrames() throws Exception {
feedFrame(0);
eglRenderer.addFrameListener(testFrameListener, 0f);
// Check the old frame does not trigger frame listener.
assertFalse(testFrameListener.waitForBitmap(RENDER_WAIT_MS));
}
/** Checks that the frame listener will not be called after it is removed. */
@Test
@SmallTest
public void testRemoveFrameListenerNotRacy() throws Exception {
for (int i = 0; i < REMOVE_FRAME_LISTENER_RACY_NUM_TESTS; i++) {
feedFrame(0);
eglRenderer.addFrameListener(testFrameListener, 0f);
eglRenderer.removeFrameListener(testFrameListener);
feedFrame(1);
}
// Check the frame listener hasn't triggered.
assertFalse(testFrameListener.waitForBitmap(RENDER_WAIT_MS));
}
@Test
@SmallTest
public void testFrameListenersFpsReduction() throws Exception {
// Test that normal frame listeners receive frames while the renderer is paused.
eglRenderer.pauseVideo();
eglRenderer.addFrameListener(testFrameListener, 1f /* scaleFactor */);
feedFrame(0);
assertTrue(testFrameListener.waitForBitmap(RENDER_WAIT_MS));
checkBitmapContent(testFrameListener.resetAndGetBitmap(), 0);
// Test that frame listeners with FPS reduction applied receive frames while the renderer is not
// paused.
eglRenderer.disableFpsReduction();
eglRenderer.addFrameListener(
testFrameListener, 1f /* scaleFactor */, null, true /* applyFpsReduction */);
feedFrame(1);
assertTrue(testFrameListener.waitForBitmap(RENDER_WAIT_MS));
checkBitmapContent(testFrameListener.resetAndGetBitmap(), 1);
// Test that frame listeners with FPS reduction applied will not receive frames while the
// renderer is paused.
eglRenderer.pauseVideo();
eglRenderer.addFrameListener(
testFrameListener, 1f /* scaleFactor */, null, true /* applyFpsReduction */);
feedFrame(1);
assertFalse(testFrameListener.waitForBitmap(RENDER_WAIT_MS));
}
private static ByteBuffer[][] copyTestDataToDirectByteBuffers(byte[][][] testData) {
final ByteBuffer[][] result = new ByteBuffer[testData.length][];
for (int i = 0; i < testData.length; i++) {
result[i] = new ByteBuffer[testData[i].length];
for (int j = 0; j < testData[i].length; j++) {
result[i][j] = ByteBuffer.allocateDirect(testData[i][j].length);
result[i][j].put(testData[i][j]);
result[i][j].rewind();
}
}
return result;
}
}