• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
3  *
4  *  Use of this source code is governed by a BSD-style license
5  *  that can be found in the LICENSE file in the root of the source
6  *  tree. An additional intellectual property rights grant can be found
7  *  in the file PATENTS.  All contributing project authors may
8  *  be found in the AUTHORS file in the root of the source tree.
9  */
10 
11 package org.webrtc;
12 
13 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
14 import static org.hamcrest.Matchers.lessThanOrEqualTo;
15 import static org.junit.Assert.assertEquals;
16 import static org.junit.Assert.assertThat;
17 
18 import android.graphics.Matrix;
19 import android.opengl.GLES20;
20 import android.os.Handler;
21 import android.os.HandlerThread;
22 import androidx.test.filters.SmallTest;
23 import java.nio.ByteBuffer;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.List;
29 import org.junit.BeforeClass;
30 import org.junit.Test;
31 import org.junit.runner.RunWith;
32 import org.junit.runners.Parameterized;
33 import org.junit.runners.Parameterized.Parameters;
34 import org.webrtc.VideoFrame;
35 
36 /**
37  * Test VideoFrame buffers of different kind of formats: I420, RGB, OES, NV12, NV21, and verify
38  * toI420() and cropAndScale() behavior. Creating RGB/OES textures involves VideoFrameDrawer and
39  * GlRectDrawer and we are testing the full chain I420 -> OES/RGB texture -> I420, with and without
40  * cropping in the middle. Reading textures back to I420 also exercises the YuvConverter code.
41  */
42 @RunWith(Parameterized.class)
43 public class VideoFrameBufferTest {
44   /**
45    * These tests are parameterized on this enum which represents the different VideoFrame.Buffers.
46    */
47   private static enum BufferType { I420_JAVA, I420_NATIVE, RGB_TEXTURE, OES_TEXTURE, NV21, NV12 }
48 
49   @Parameters(name = "{0}")
parameters()50   public static Collection<BufferType> parameters() {
51     return Arrays.asList(BufferType.values());
52   }
53 
54   @BeforeClass
setUp()55   public static void setUp() {
56     // Needed for JniCommon.nativeAllocateByteBuffer() to work, which is used from JavaI420Buffer.
57     System.loadLibrary(TestConstants.NATIVE_LIBRARY);
58   }
59 
60   private final BufferType bufferType;
61 
VideoFrameBufferTest(BufferType bufferType)62   public VideoFrameBufferTest(BufferType bufferType) {
63     this.bufferType = bufferType;
64   }
65 
66   /**
67    * Create a VideoFrame.Buffer of the given type with the same pixel content as the given I420
68    * buffer.
69    */
createBufferWithType( BufferType bufferType, VideoFrame.I420Buffer i420Buffer)70   private static VideoFrame.Buffer createBufferWithType(
71       BufferType bufferType, VideoFrame.I420Buffer i420Buffer) {
72     VideoFrame.Buffer buffer;
73     switch (bufferType) {
74       case I420_JAVA:
75         buffer = i420Buffer;
76         buffer.retain();
77         assertEquals(VideoFrameBufferType.I420, buffer.getBufferType());
78         assertEquals(VideoFrameBufferType.I420, nativeGetBufferType(buffer));
79         return buffer;
80       case I420_NATIVE:
81         buffer = nativeGetNativeI420Buffer(i420Buffer);
82         assertEquals(VideoFrameBufferType.I420, buffer.getBufferType());
83         assertEquals(VideoFrameBufferType.I420, nativeGetBufferType(buffer));
84         return buffer;
85       case RGB_TEXTURE:
86         buffer = createRgbTextureBuffer(/* eglContext= */ null, i420Buffer);
87         assertEquals(VideoFrameBufferType.NATIVE, buffer.getBufferType());
88         assertEquals(VideoFrameBufferType.NATIVE, nativeGetBufferType(buffer));
89         return buffer;
90       case OES_TEXTURE:
91         buffer = createOesTextureBuffer(/* eglContext= */ null, i420Buffer);
92         assertEquals(VideoFrameBufferType.NATIVE, buffer.getBufferType());
93         assertEquals(VideoFrameBufferType.NATIVE, nativeGetBufferType(buffer));
94         return buffer;
95       case NV21:
96         buffer = createNV21Buffer(i420Buffer);
97         assertEquals(VideoFrameBufferType.NATIVE, buffer.getBufferType());
98         assertEquals(VideoFrameBufferType.NATIVE, nativeGetBufferType(buffer));
99         return buffer;
100       case NV12:
101         buffer = createNV12Buffer(i420Buffer);
102         assertEquals(VideoFrameBufferType.NATIVE, buffer.getBufferType());
103         assertEquals(VideoFrameBufferType.NATIVE, nativeGetBufferType(buffer));
104         return buffer;
105       default:
106         throw new IllegalArgumentException("Unknown buffer type: " + bufferType);
107     }
108   }
109 
createBufferToTest(VideoFrame.I420Buffer i420Buffer)110   private VideoFrame.Buffer createBufferToTest(VideoFrame.I420Buffer i420Buffer) {
111     return createBufferWithType(this.bufferType, i420Buffer);
112   }
113 
114   /**
115    * Creates a 16x16 I420 buffer that varies smoothly and spans all RGB values.
116    */
createTestI420Buffer()117   public static VideoFrame.I420Buffer createTestI420Buffer() {
118     final int width = 16;
119     final int height = 16;
120     final int[] yData = new int[] {156, 162, 167, 172, 177, 182, 187, 193, 199, 203, 209, 214, 219,
121         224, 229, 235, 147, 152, 157, 162, 168, 173, 178, 183, 188, 193, 199, 205, 210, 215, 220,
122         225, 138, 143, 148, 153, 158, 163, 168, 174, 180, 184, 190, 195, 200, 205, 211, 216, 128,
123         133, 138, 144, 149, 154, 159, 165, 170, 175, 181, 186, 191, 196, 201, 206, 119, 124, 129,
124         134, 140, 145, 150, 156, 161, 166, 171, 176, 181, 187, 192, 197, 109, 114, 119, 126, 130,
125         136, 141, 146, 151, 156, 162, 167, 172, 177, 182, 187, 101, 105, 111, 116, 121, 126, 132,
126         137, 142, 147, 152, 157, 162, 168, 173, 178, 90, 96, 101, 107, 112, 117, 122, 127, 132, 138,
127         143, 148, 153, 158, 163, 168, 82, 87, 92, 97, 102, 107, 113, 118, 123, 128, 133, 138, 144,
128         149, 154, 159, 72, 77, 83, 88, 93, 98, 103, 108, 113, 119, 124, 129, 134, 139, 144, 150, 63,
129         68, 73, 78, 83, 89, 94, 99, 104, 109, 114, 119, 125, 130, 135, 140, 53, 58, 64, 69, 74, 79,
130         84, 89, 95, 100, 105, 110, 115, 120, 126, 131, 44, 49, 54, 59, 64, 70, 75, 80, 85, 90, 95,
131         101, 106, 111, 116, 121, 34, 40, 45, 50, 55, 60, 65, 71, 76, 81, 86, 91, 96, 101, 107, 113,
132         25, 30, 35, 40, 46, 51, 56, 61, 66, 71, 77, 82, 87, 92, 98, 103, 16, 21, 26, 31, 36, 41, 46,
133         52, 57, 62, 67, 72, 77, 83, 89, 94};
134     final int[] uData = new int[] {110, 113, 116, 118, 120, 123, 125, 128, 113, 116, 118, 120, 123,
135         125, 128, 130, 116, 118, 120, 123, 125, 128, 130, 132, 118, 120, 123, 125, 128, 130, 132,
136         135, 120, 123, 125, 128, 130, 132, 135, 138, 123, 125, 128, 130, 132, 135, 138, 139, 125,
137         128, 130, 132, 135, 138, 139, 142, 128, 130, 132, 135, 138, 139, 142, 145};
138     final int[] vData = new int[] {31, 45, 59, 73, 87, 100, 114, 127, 45, 59, 73, 87, 100, 114, 128,
139         141, 59, 73, 87, 100, 114, 127, 141, 155, 73, 87, 100, 114, 127, 141, 155, 168, 87, 100,
140         114, 128, 141, 155, 168, 182, 100, 114, 128, 141, 155, 168, 182, 197, 114, 127, 141, 155,
141         168, 182, 196, 210, 127, 141, 155, 168, 182, 196, 210, 224};
142     return JavaI420Buffer.wrap(width, height, toByteBuffer(yData),
143         /* strideY= */ width, toByteBuffer(uData), /* strideU= */ width / 2, toByteBuffer(vData),
144         /* strideV= */ width / 2,
145         /* releaseCallback= */ null);
146   }
147 
148   /**
149    * Create an RGB texture buffer available in `eglContext` with the same pixel content as the given
150    * I420 buffer.
151    */
createRgbTextureBuffer( EglBase.Context eglContext, VideoFrame.I420Buffer i420Buffer)152   public static VideoFrame.TextureBuffer createRgbTextureBuffer(
153       EglBase.Context eglContext, VideoFrame.I420Buffer i420Buffer) {
154     final int width = i420Buffer.getWidth();
155     final int height = i420Buffer.getHeight();
156 
157     final HandlerThread renderThread = new HandlerThread("RGB texture thread");
158     renderThread.start();
159     final Handler renderThreadHandler = new Handler(renderThread.getLooper());
160     return ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, () -> {
161       // Create EGL base with a pixel buffer as display output.
162       final EglBase eglBase = EglBase.create(eglContext, EglBase.CONFIG_PIXEL_BUFFER);
163       eglBase.createDummyPbufferSurface();
164       eglBase.makeCurrent();
165 
166       final GlTextureFrameBuffer textureFrameBuffer = new GlTextureFrameBuffer(GLES20.GL_RGBA);
167       textureFrameBuffer.setSize(width, height);
168 
169       GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, textureFrameBuffer.getFrameBufferId());
170       drawI420Buffer(i420Buffer);
171       GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
172 
173       final YuvConverter yuvConverter = new YuvConverter();
174       return new TextureBufferImpl(width, height, VideoFrame.TextureBuffer.Type.RGB,
175           textureFrameBuffer.getTextureId(),
176           /* transformMatrix= */ new Matrix(), renderThreadHandler, yuvConverter,
177           /* releaseCallback= */ () -> renderThreadHandler.post(() -> {
178             textureFrameBuffer.release();
179             yuvConverter.release();
180             eglBase.release();
181             renderThread.quit();
182           }));
183     });
184   }
185 
186   /**
187    * Create an OES texture buffer available in `eglContext` with the same pixel content as the given
188    * I420 buffer.
189    */
190   public static VideoFrame.TextureBuffer createOesTextureBuffer(
191       EglBase.Context eglContext, VideoFrame.I420Buffer i420Buffer) {
192     final int width = i420Buffer.getWidth();
193     final int height = i420Buffer.getHeight();
194 
195     // Create resources for generating OES textures.
196     final SurfaceTextureHelper surfaceTextureHelper =
197         SurfaceTextureHelper.create("SurfaceTextureHelper test", eglContext);
198     surfaceTextureHelper.setTextureSize(width, height);
199 
200     final HandlerThread renderThread = new HandlerThread("OES texture thread");
201     renderThread.start();
202     final Handler renderThreadHandler = new Handler(renderThread.getLooper());
203     final VideoFrame.TextureBuffer oesBuffer =
204         ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, () -> {
205           // Create EGL base with the SurfaceTexture as display output.
206           final EglBase eglBase = EglBase.create(eglContext, EglBase.CONFIG_PLAIN);
207           eglBase.createSurface(surfaceTextureHelper.getSurfaceTexture());
208           eglBase.makeCurrent();
209           assertEquals(width, eglBase.surfaceWidth());
210           assertEquals(height, eglBase.surfaceHeight());
211 
212           final SurfaceTextureHelperTest.MockTextureListener listener =
213               new SurfaceTextureHelperTest.MockTextureListener();
214           surfaceTextureHelper.startListening(listener);
215 
216           // Draw the frame and block until an OES texture is delivered.
217           drawI420Buffer(i420Buffer);
218           eglBase.swapBuffers();
219           final VideoFrame.TextureBuffer textureBuffer = listener.waitForTextureBuffer();
220           surfaceTextureHelper.stopListening();
221           surfaceTextureHelper.dispose();
222 
223           return textureBuffer;
224         });
225     renderThread.quit();
226 
227     return oesBuffer;
228   }
229 
230   /** Create an NV21Buffer with the same pixel content as the given I420 buffer. */
231   public static NV21Buffer createNV21Buffer(VideoFrame.I420Buffer i420Buffer) {
232     final int width = i420Buffer.getWidth();
233     final int height = i420Buffer.getHeight();
234     final int chromaStride = width;
235     final int chromaWidth = (width + 1) / 2;
236     final int chromaHeight = (height + 1) / 2;
237     final int ySize = width * height;
238 
239     final ByteBuffer nv21Buffer = ByteBuffer.allocateDirect(ySize + chromaStride * chromaHeight);
240     // We don't care what the array offset is since we only want an array that is direct.
241     @SuppressWarnings("ByteBufferBackingArray") final byte[] nv21Data = nv21Buffer.array();
242 
243     for (int y = 0; y < height; ++y) {
244       for (int x = 0; x < width; ++x) {
245         final byte yValue = i420Buffer.getDataY().get(y * i420Buffer.getStrideY() + x);
246         nv21Data[y * width + x] = yValue;
247       }
248     }
249     for (int y = 0; y < chromaHeight; ++y) {
250       for (int x = 0; x < chromaWidth; ++x) {
251         final byte uValue = i420Buffer.getDataU().get(y * i420Buffer.getStrideU() + x);
252         final byte vValue = i420Buffer.getDataV().get(y * i420Buffer.getStrideV() + x);
253         nv21Data[ySize + y * chromaStride + 2 * x + 0] = vValue;
254         nv21Data[ySize + y * chromaStride + 2 * x + 1] = uValue;
255       }
256     }
257     return new NV21Buffer(nv21Data, width, height, /* releaseCallback= */ null);
258   }
259 
260   /** Create an NV12Buffer with the same pixel content as the given I420 buffer. */
261   public static NV12Buffer createNV12Buffer(VideoFrame.I420Buffer i420Buffer) {
262     final int width = i420Buffer.getWidth();
263     final int height = i420Buffer.getHeight();
264     final int chromaStride = width;
265     final int chromaWidth = (width + 1) / 2;
266     final int chromaHeight = (height + 1) / 2;
267     final int ySize = width * height;
268 
269     final ByteBuffer nv12Buffer = ByteBuffer.allocateDirect(ySize + chromaStride * chromaHeight);
270 
271     for (int y = 0; y < height; ++y) {
272       for (int x = 0; x < width; ++x) {
273         final byte yValue = i420Buffer.getDataY().get(y * i420Buffer.getStrideY() + x);
274         nv12Buffer.put(y * width + x, yValue);
275       }
276     }
277     for (int y = 0; y < chromaHeight; ++y) {
278       for (int x = 0; x < chromaWidth; ++x) {
279         final byte uValue = i420Buffer.getDataU().get(y * i420Buffer.getStrideU() + x);
280         final byte vValue = i420Buffer.getDataV().get(y * i420Buffer.getStrideV() + x);
281         nv12Buffer.put(ySize + y * chromaStride + 2 * x + 0, uValue);
282         nv12Buffer.put(ySize + y * chromaStride + 2 * x + 1, vValue);
283       }
284     }
285     return new NV12Buffer(width, height, /* stride= */ width, /* sliceHeight= */ height, nv12Buffer,
286         /* releaseCallback */ null);
287   }
288 
289   /** Print the ByteBuffer plane to the StringBuilder. */
290   private static void printPlane(
291       StringBuilder stringBuilder, int width, int height, ByteBuffer plane, int stride) {
292     for (int y = 0; y < height; ++y) {
293       for (int x = 0; x < width; ++x) {
294         final int value = plane.get(y * stride + x) & 0xFF;
295         if (x != 0) {
296           stringBuilder.append(", ");
297         }
298         stringBuilder.append(value);
299       }
300       stringBuilder.append("\n");
301     }
302   }
303 
304   /** Convert the pixel content of an I420 buffer to a string representation. */
305   private static String i420BufferToString(VideoFrame.I420Buffer buffer) {
306     final StringBuilder stringBuilder = new StringBuilder();
307     stringBuilder.append(
308         "I420 buffer with size: " + buffer.getWidth() + "x" + buffer.getHeight() + ".\n");
309     stringBuilder.append("Y-plane:\n");
310     printPlane(stringBuilder, buffer.getWidth(), buffer.getHeight(), buffer.getDataY(),
311         buffer.getStrideY());
312     final int chromaWidth = (buffer.getWidth() + 1) / 2;
313     final int chromaHeight = (buffer.getHeight() + 1) / 2;
314     stringBuilder.append("U-plane:\n");
315     printPlane(stringBuilder, chromaWidth, chromaHeight, buffer.getDataU(), buffer.getStrideU());
316     stringBuilder.append("V-plane:\n");
317     printPlane(stringBuilder, chromaWidth, chromaHeight, buffer.getDataV(), buffer.getStrideV());
318     return stringBuilder.toString();
319   }
320 
321   /**
322    * Assert that the given I420 buffers are almost identical, allowing for some difference due to
323    * numerical errors. It has limits for both overall PSNR and maximum individual pixel difference.
324    */
325   public static void assertAlmostEqualI420Buffers(
326       VideoFrame.I420Buffer bufferA, VideoFrame.I420Buffer bufferB) {
327     final int diff = maxDiff(bufferA, bufferB);
328     assertThat("Pixel difference too high: " + diff + "."
329             + "\nBuffer A: " + i420BufferToString(bufferA)
330             + "Buffer B: " + i420BufferToString(bufferB),
331         diff, lessThanOrEqualTo(4));
332     final double psnr = calculatePsnr(bufferA, bufferB);
333     assertThat("PSNR too low: " + psnr + "."
334             + "\nBuffer A: " + i420BufferToString(bufferA)
335             + "Buffer B: " + i420BufferToString(bufferB),
336         psnr, greaterThanOrEqualTo(50.0));
337   }
338 
339   /** Returns a flattened list of pixel differences for two ByteBuffer planes. */
340   private static List<Integer> getPixelDiffs(
341       int width, int height, ByteBuffer planeA, int strideA, ByteBuffer planeB, int strideB) {
342     List<Integer> res = new ArrayList<>();
343     for (int y = 0; y < height; ++y) {
344       for (int x = 0; x < width; ++x) {
345         final int valueA = planeA.get(y * strideA + x) & 0xFF;
346         final int valueB = planeB.get(y * strideB + x) & 0xFF;
347         res.add(Math.abs(valueA - valueB));
348       }
349     }
350     return res;
351   }
352 
353   /** Returns a flattened list of pixel differences for two I420 buffers. */
354   private static List<Integer> getPixelDiffs(
355       VideoFrame.I420Buffer bufferA, VideoFrame.I420Buffer bufferB) {
356     assertEquals(bufferA.getWidth(), bufferB.getWidth());
357     assertEquals(bufferA.getHeight(), bufferB.getHeight());
358     final int width = bufferA.getWidth();
359     final int height = bufferA.getHeight();
360     final int chromaWidth = (width + 1) / 2;
361     final int chromaHeight = (height + 1) / 2;
362     final List<Integer> diffs = getPixelDiffs(width, height, bufferA.getDataY(),
363         bufferA.getStrideY(), bufferB.getDataY(), bufferB.getStrideY());
364     diffs.addAll(getPixelDiffs(chromaWidth, chromaHeight, bufferA.getDataU(), bufferA.getStrideU(),
365         bufferB.getDataU(), bufferB.getStrideU()));
366     diffs.addAll(getPixelDiffs(chromaWidth, chromaHeight, bufferA.getDataV(), bufferA.getStrideV(),
367         bufferB.getDataV(), bufferB.getStrideV()));
368     return diffs;
369   }
370 
371   /** Returns the maximum pixel difference from any of the Y/U/V planes in the given buffers. */
372   private static int maxDiff(VideoFrame.I420Buffer bufferA, VideoFrame.I420Buffer bufferB) {
373     return Collections.max(getPixelDiffs(bufferA, bufferB));
374   }
375 
376   /**
377    * Returns the PSNR given a sum of squared error and the number of measurements that were added.
378    */
379   private static double sseToPsnr(long sse, int count) {
380     if (sse == 0) {
381       return Double.POSITIVE_INFINITY;
382     }
383     final double meanSquaredError = (double) sse / (double) count;
384     final double maxPixelValue = 255.0;
385     return 10.0 * Math.log10(maxPixelValue * maxPixelValue / meanSquaredError);
386   }
387 
388   /** Returns the PSNR of the given I420 buffers. */
389   private static double calculatePsnr(
390       VideoFrame.I420Buffer bufferA, VideoFrame.I420Buffer bufferB) {
391     final List<Integer> pixelDiffs = getPixelDiffs(bufferA, bufferB);
392     long sse = 0;
393     for (int pixelDiff : pixelDiffs) {
394       sse += pixelDiff * pixelDiff;
395     }
396     return sseToPsnr(sse, pixelDiffs.size());
397   }
398 
399   /**
400    * Convert an int array to a byte array and make sure the values are within the range [0, 255].
401    */
402   private static byte[] toByteArray(int[] array) {
403     final byte[] res = new byte[array.length];
404     for (int i = 0; i < array.length; ++i) {
405       final int value = array[i];
406       assertThat(value, greaterThanOrEqualTo(0));
407       assertThat(value, lessThanOrEqualTo(255));
408       res[i] = (byte) value;
409     }
410     return res;
411   }
412 
413   /** Convert a byte array to a direct ByteBuffer. */
414   private static ByteBuffer toByteBuffer(int[] array) {
415     final ByteBuffer buffer = ByteBuffer.allocateDirect(array.length);
416     buffer.put(toByteArray(array));
417     buffer.rewind();
418     return buffer;
419   }
420 
421   /**
422    * Draw an I420 buffer on the currently bound frame buffer, allocating and releasing any
423    * resources necessary.
424    */
425   private static void drawI420Buffer(VideoFrame.I420Buffer i420Buffer) {
426     final GlRectDrawer drawer = new GlRectDrawer();
427     final VideoFrameDrawer videoFrameDrawer = new VideoFrameDrawer();
428     videoFrameDrawer.drawFrame(
429         new VideoFrame(i420Buffer, /* rotation= */ 0, /* timestampNs= */ 0), drawer);
430     videoFrameDrawer.release();
431     drawer.release();
432   }
433 
434   /**
435    * Helper function that tests cropAndScale() with the given cropping and scaling parameters, and
436    * compares the pixel content against a reference I420 buffer.
437    */
438   private void testCropAndScale(
439       int cropX, int cropY, int cropWidth, int cropHeight, int scaleWidth, int scaleHeight) {
440     final VideoFrame.I420Buffer referenceI420Buffer = createTestI420Buffer();
441     final VideoFrame.Buffer bufferToTest = createBufferToTest(referenceI420Buffer);
442 
443     final VideoFrame.Buffer croppedReferenceBuffer = referenceI420Buffer.cropAndScale(
444         cropX, cropY, cropWidth, cropHeight, scaleWidth, scaleHeight);
445     referenceI420Buffer.release();
446     final VideoFrame.I420Buffer croppedReferenceI420Buffer = croppedReferenceBuffer.toI420();
447     croppedReferenceBuffer.release();
448 
449     final VideoFrame.Buffer croppedBufferToTest =
450         bufferToTest.cropAndScale(cropX, cropY, cropWidth, cropHeight, scaleWidth, scaleHeight);
451     bufferToTest.release();
452 
453     final VideoFrame.I420Buffer croppedOutputI420Buffer = croppedBufferToTest.toI420();
454     croppedBufferToTest.release();
455 
456     assertAlmostEqualI420Buffers(croppedReferenceI420Buffer, croppedOutputI420Buffer);
457     croppedReferenceI420Buffer.release();
458     croppedOutputI420Buffer.release();
459   }
460 
461   @Test
462   @SmallTest
463   /** Test calling toI420() and comparing pixel content against I420 reference. */
464   public void testToI420() {
465     final VideoFrame.I420Buffer referenceI420Buffer = createTestI420Buffer();
466     final VideoFrame.Buffer bufferToTest = createBufferToTest(referenceI420Buffer);
467 
468     final VideoFrame.I420Buffer outputI420Buffer = bufferToTest.toI420();
469     bufferToTest.release();
470 
471     assertEquals(VideoFrameBufferType.I420, nativeGetBufferType(outputI420Buffer));
472     assertAlmostEqualI420Buffers(referenceI420Buffer, outputI420Buffer);
473     referenceI420Buffer.release();
474     outputI420Buffer.release();
475   }
476 
477   @Test
478   @SmallTest
479   /** Pure 2x scaling with no cropping. */
480   public void testScale2x() {
481     testCropAndScale(0 /* cropX= */, 0 /* cropY= */, /* cropWidth= */ 16, /* cropHeight= */ 16,
482         /* scaleWidth= */ 8, /* scaleHeight= */ 8);
483   }
484 
485   @Test
486   @SmallTest
487   /** Test cropping only X direction, with no scaling. */
488   public void testCropX() {
489     testCropAndScale(8 /* cropX= */, 0 /* cropY= */, /* cropWidth= */ 8, /* cropHeight= */ 16,
490         /* scaleWidth= */ 8, /* scaleHeight= */ 16);
491   }
492 
493   @Test
494   @SmallTest
495   /** Test cropping only Y direction, with no scaling. */
496   public void testCropY() {
497     testCropAndScale(0 /* cropX= */, 8 /* cropY= */, /* cropWidth= */ 16, /* cropHeight= */ 8,
498         /* scaleWidth= */ 16, /* scaleHeight= */ 8);
499   }
500 
501   @Test
502   @SmallTest
503   /** Test center crop, with no scaling. */
504   public void testCenterCrop() {
505     testCropAndScale(4 /* cropX= */, 4 /* cropY= */, /* cropWidth= */ 8, /* cropHeight= */ 8,
506         /* scaleWidth= */ 8, /* scaleHeight= */ 8);
507   }
508 
509   @Test
510   @SmallTest
511   /** Test non-center crop for right bottom corner, with no scaling. */
512   public void testRightBottomCornerCrop() {
513     testCropAndScale(8 /* cropX= */, 8 /* cropY= */, /* cropWidth= */ 8, /* cropHeight= */ 8,
514         /* scaleWidth= */ 8, /* scaleHeight= */ 8);
515   }
516 
517   @Test
518   @SmallTest
519   /** Test combined cropping and scaling. */
520   public void testCropAndScale() {
521     testCropAndScale(4 /* cropX= */, 4 /* cropY= */, /* cropWidth= */ 12, /* cropHeight= */ 12,
522         /* scaleWidth= */ 8, /* scaleHeight= */ 8);
523   }
524 
525   @VideoFrameBufferType private static native int nativeGetBufferType(VideoFrame.Buffer buffer);
526 
527   /** Returns the copy of I420Buffer using WrappedNativeI420Buffer. */
528   private static native VideoFrame.Buffer nativeGetNativeI420Buffer(
529       VideoFrame.I420Buffer i420Buffer);
530 }
531