• 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 com.google.common.truth.Truth.assertThat;
14 import static org.mockito.ArgumentMatchers.any;
15 import static org.mockito.ArgumentMatchers.anyInt;
16 import static org.mockito.ArgumentMatchers.anyLong;
17 import static org.mockito.ArgumentMatchers.eq;
18 import static org.mockito.Mockito.doThrow;
19 import static org.mockito.Mockito.inOrder;
20 import static org.mockito.Mockito.mock;
21 import static org.mockito.Mockito.spy;
22 import static org.mockito.Mockito.verify;
23 import static org.mockito.Mockito.when;
24 
25 import android.graphics.Matrix;
26 import android.graphics.SurfaceTexture;
27 import android.media.MediaCodecInfo.CodecCapabilities;
28 import android.media.MediaFormat;
29 import android.os.Handler;
30 import androidx.test.runner.AndroidJUnit4;
31 import java.nio.ByteBuffer;
32 import java.util.ArrayList;
33 import java.util.List;
34 import org.junit.After;
35 import org.junit.Before;
36 import org.junit.Test;
37 import org.junit.runner.RunWith;
38 import org.mockito.ArgumentCaptor;
39 import org.mockito.InOrder;
40 import org.mockito.Mock;
41 import org.mockito.MockitoAnnotations;
42 import org.robolectric.annotation.Config;
43 import org.webrtc.EncodedImage.FrameType;
44 import org.webrtc.FakeMediaCodecWrapper.State;
45 import org.webrtc.VideoDecoder.DecodeInfo;
46 import org.webrtc.VideoFrame.I420Buffer;
47 import org.webrtc.VideoFrame.TextureBuffer.Type;
48 
49 @RunWith(AndroidJUnit4.class)
50 @Config(manifest = Config.NONE)
51 public class AndroidVideoDecoderTest {
52   private static final VideoDecoder.Settings TEST_DECODER_SETTINGS =
53       new VideoDecoder.Settings(/* numberOfCores= */ 1, /* width= */ 640, /* height= */ 480);
54   private static final int COLOR_FORMAT = CodecCapabilities.COLOR_FormatYUV420Planar;
55   private static final long POLL_DELAY_MS = 10;
56   private static final long DELIVER_DECODED_IMAGE_DELAY_MS = 10;
57 
58   private static final byte[] ENCODED_TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
59 
60   private class TestDecoder extends AndroidVideoDecoder {
61     private final Object deliverDecodedFrameLock = new Object();
62     private boolean deliverDecodedFrameDone = true;
63 
TestDecoder(MediaCodecWrapperFactory mediaCodecFactory, String codecName, VideoCodecMimeType codecType, int colorFormat, EglBase.Context sharedContext)64     public TestDecoder(MediaCodecWrapperFactory mediaCodecFactory, String codecName,
65         VideoCodecMimeType codecType, int colorFormat, EglBase.Context sharedContext) {
66       super(mediaCodecFactory, codecName, codecType, colorFormat, sharedContext);
67     }
68 
waitDeliverDecodedFrame()69     public void waitDeliverDecodedFrame() throws InterruptedException {
70       synchronized (deliverDecodedFrameLock) {
71         deliverDecodedFrameDone = false;
72         deliverDecodedFrameLock.notifyAll();
73         while (!deliverDecodedFrameDone) {
74           deliverDecodedFrameLock.wait();
75         }
76       }
77     }
78 
79     @SuppressWarnings("WaitNotInLoop") // This method is called inside a loop.
80     @Override
deliverDecodedFrame()81     protected void deliverDecodedFrame() {
82       synchronized (deliverDecodedFrameLock) {
83         if (deliverDecodedFrameDone) {
84           try {
85             deliverDecodedFrameLock.wait(DELIVER_DECODED_IMAGE_DELAY_MS);
86           } catch (InterruptedException e) {
87             Thread.currentThread().interrupt();
88             return;
89           }
90         }
91         if (deliverDecodedFrameDone) {
92           return;
93         }
94         super.deliverDecodedFrame();
95         deliverDecodedFrameDone = true;
96         deliverDecodedFrameLock.notifyAll();
97       }
98     }
99 
100     @Override
createSurfaceTextureHelper()101     protected SurfaceTextureHelper createSurfaceTextureHelper() {
102       return mockSurfaceTextureHelper;
103     }
104 
105     @Override
releaseSurface()106     protected void releaseSurface() {}
107 
108     @Override
allocateI420Buffer(int width, int height)109     protected VideoFrame.I420Buffer allocateI420Buffer(int width, int height) {
110       int chromaHeight = (height + 1) / 2;
111       int strideUV = (width + 1) / 2;
112       int yPos = 0;
113       int uPos = yPos + width * height;
114       int vPos = uPos + strideUV * chromaHeight;
115 
116       ByteBuffer buffer = ByteBuffer.allocateDirect(width * height + 2 * strideUV * chromaHeight);
117 
118       buffer.position(yPos);
119       buffer.limit(uPos);
120       ByteBuffer dataY = buffer.slice();
121 
122       buffer.position(uPos);
123       buffer.limit(vPos);
124       ByteBuffer dataU = buffer.slice();
125 
126       buffer.position(vPos);
127       buffer.limit(vPos + strideUV * chromaHeight);
128       ByteBuffer dataV = buffer.slice();
129 
130       return JavaI420Buffer.wrap(width, height, dataY, width, dataU, strideUV, dataV, strideUV,
131           /* releaseCallback= */ null);
132     }
133 
134     @Override
copyPlane( ByteBuffer src, int srcStride, ByteBuffer dst, int dstStride, int width, int height)135     protected void copyPlane(
136         ByteBuffer src, int srcStride, ByteBuffer dst, int dstStride, int width, int height) {
137       for (int y = 0; y < height; y++) {
138         for (int x = 0; x < width; x++) {
139           dst.put(y * dstStride + x, src.get(y * srcStride + x));
140         }
141       }
142     }
143   }
144 
145   private class TestDecoderBuilder {
146     private VideoCodecMimeType codecType = VideoCodecMimeType.VP8;
147     private boolean useSurface = true;
148 
setCodecType(VideoCodecMimeType codecType)149     public TestDecoderBuilder setCodecType(VideoCodecMimeType codecType) {
150       this.codecType = codecType;
151       return this;
152     }
153 
setUseSurface(boolean useSurface)154     public TestDecoderBuilder setUseSurface(boolean useSurface) {
155       this.useSurface = useSurface;
156       return this;
157     }
158 
build()159     public TestDecoder build() {
160       return new TestDecoder((String name)
161                                  -> fakeMediaCodecWrapper,
162           /* codecName= */ "org.webrtc.testdecoder", codecType, COLOR_FORMAT,
163           useSurface ? mockEglBaseContext : null);
164     }
165   }
166 
167   private static class FakeDecoderCallback implements VideoDecoder.Callback {
168     public final List<VideoFrame> decodedFrames;
169 
FakeDecoderCallback()170     public FakeDecoderCallback() {
171       decodedFrames = new ArrayList<>();
172     }
173 
174     @Override
onDecodedFrame(VideoFrame frame, Integer decodeTimeMs, Integer qp)175     public void onDecodedFrame(VideoFrame frame, Integer decodeTimeMs, Integer qp) {
176       frame.retain();
177       decodedFrames.add(frame);
178     }
179 
release()180     public void release() {
181       for (VideoFrame frame : decodedFrames) frame.release();
182       decodedFrames.clear();
183     }
184   }
185 
createTestEncodedImage()186   private EncodedImage createTestEncodedImage() {
187     return EncodedImage.builder()
188         .setBuffer(ByteBuffer.wrap(ENCODED_TEST_DATA), null)
189         .setFrameType(FrameType.VideoFrameKey)
190         .createEncodedImage();
191   }
192 
193   @Mock private EglBase.Context mockEglBaseContext;
194   @Mock private SurfaceTextureHelper mockSurfaceTextureHelper;
195   @Mock private VideoDecoder.Callback mockDecoderCallback;
196   private FakeMediaCodecWrapper fakeMediaCodecWrapper;
197   private FakeDecoderCallback fakeDecoderCallback;
198 
199   @Before
setUp()200   public void setUp() {
201     MockitoAnnotations.initMocks(this);
202     when(mockSurfaceTextureHelper.getSurfaceTexture())
203         .thenReturn(new SurfaceTexture(/*texName=*/0));
204     MediaFormat inputFormat = new MediaFormat();
205     MediaFormat outputFormat = new MediaFormat();
206     // TODO(sakal): Add more details to output format as needed.
207     fakeMediaCodecWrapper = spy(new FakeMediaCodecWrapper(inputFormat, outputFormat));
208     fakeDecoderCallback = new FakeDecoderCallback();
209   }
210 
211   @After
cleanUp()212   public void cleanUp() {
213     fakeDecoderCallback.release();
214   }
215 
216   @Test
testInit()217   public void testInit() {
218     // Set-up.
219     AndroidVideoDecoder decoder =
220         new TestDecoderBuilder().setCodecType(VideoCodecMimeType.VP8).build();
221 
222     // Test.
223     assertThat(decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback))
224         .isEqualTo(VideoCodecStatus.OK);
225 
226     // Verify.
227     assertThat(fakeMediaCodecWrapper.getState()).isEqualTo(State.EXECUTING_RUNNING);
228 
229     MediaFormat mediaFormat = fakeMediaCodecWrapper.getConfiguredFormat();
230     assertThat(mediaFormat).isNotNull();
231     assertThat(mediaFormat.getInteger(MediaFormat.KEY_WIDTH))
232         .isEqualTo(TEST_DECODER_SETTINGS.width);
233     assertThat(mediaFormat.getInteger(MediaFormat.KEY_HEIGHT))
234         .isEqualTo(TEST_DECODER_SETTINGS.height);
235     assertThat(mediaFormat.getString(MediaFormat.KEY_MIME))
236         .isEqualTo(VideoCodecMimeType.VP8.mimeType());
237   }
238 
239   @Test
testRelease()240   public void testRelease() {
241     // Set-up.
242     AndroidVideoDecoder decoder = new TestDecoderBuilder().build();
243     decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback);
244 
245     // Test.
246     assertThat(decoder.release()).isEqualTo(VideoCodecStatus.OK);
247 
248     // Verify.
249     assertThat(fakeMediaCodecWrapper.getState()).isEqualTo(State.RELEASED);
250   }
251 
252   @Test
testReleaseMultipleTimes()253   public void testReleaseMultipleTimes() {
254     // Set-up.
255     AndroidVideoDecoder decoder = new TestDecoderBuilder().build();
256     decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback);
257 
258     // Test.
259     assertThat(decoder.release()).isEqualTo(VideoCodecStatus.OK);
260     assertThat(decoder.release()).isEqualTo(VideoCodecStatus.OK);
261 
262     // Verify.
263     assertThat(fakeMediaCodecWrapper.getState()).isEqualTo(State.RELEASED);
264   }
265 
266   @Test
testDecodeQueuesData()267   public void testDecodeQueuesData() {
268     // Set-up.
269     AndroidVideoDecoder decoder = new TestDecoderBuilder().build();
270     decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback);
271 
272     // Test.
273     assertThat(decoder.decode(createTestEncodedImage(),
274                    new DecodeInfo(/* isMissingFrames= */ false, /* renderTimeMs= */ 0)))
275         .isEqualTo(VideoCodecStatus.OK);
276 
277     // Verify.
278     ArgumentCaptor<Integer> indexCaptor = ArgumentCaptor.forClass(Integer.class);
279     ArgumentCaptor<Integer> offsetCaptor = ArgumentCaptor.forClass(Integer.class);
280     ArgumentCaptor<Integer> sizeCaptor = ArgumentCaptor.forClass(Integer.class);
281     verify(fakeMediaCodecWrapper)
282         .queueInputBuffer(indexCaptor.capture(), offsetCaptor.capture(), sizeCaptor.capture(),
283             /* presentationTimeUs= */ anyLong(),
284             /* flags= */ eq(0));
285 
286     ByteBuffer inputBuffer = fakeMediaCodecWrapper.getInputBuffer(indexCaptor.getValue());
287     CodecTestHelper.assertEqualContents(
288         ENCODED_TEST_DATA, inputBuffer, offsetCaptor.getValue(), sizeCaptor.getValue());
289   }
290 
291   @Test
testDeliversOutputByteBuffers()292   public void testDeliversOutputByteBuffers() throws InterruptedException {
293     final byte[] testOutputData = CodecTestHelper.generateRandomData(
294         TEST_DECODER_SETTINGS.width * TEST_DECODER_SETTINGS.height * 3 / 2);
295     final I420Buffer expectedDeliveredBuffer = CodecTestHelper.wrapI420(
296         TEST_DECODER_SETTINGS.width, TEST_DECODER_SETTINGS.height, testOutputData);
297 
298     // Set-up.
299     TestDecoder decoder = new TestDecoderBuilder().setUseSurface(/* useSurface = */ false).build();
300     decoder.initDecode(TEST_DECODER_SETTINGS, fakeDecoderCallback);
301     decoder.decode(createTestEncodedImage(),
302         new DecodeInfo(/* isMissingFrames= */ false, /* renderTimeMs= */ 0));
303     fakeMediaCodecWrapper.addOutputData(
304         testOutputData, /* presentationTimestampUs= */ 0, /* flags= */ 0);
305 
306     // Test.
307     decoder.waitDeliverDecodedFrame();
308 
309     // Verify.
310     assertThat(fakeDecoderCallback.decodedFrames).hasSize(1);
311     VideoFrame videoFrame = fakeDecoderCallback.decodedFrames.get(0);
312     assertThat(videoFrame).isNotNull();
313     assertThat(videoFrame.getRotatedWidth()).isEqualTo(TEST_DECODER_SETTINGS.width);
314     assertThat(videoFrame.getRotatedHeight()).isEqualTo(TEST_DECODER_SETTINGS.height);
315     assertThat(videoFrame.getRotation()).isEqualTo(0);
316     I420Buffer deliveredBuffer = videoFrame.getBuffer().toI420();
317     assertThat(deliveredBuffer.getDataY()).isEqualTo(expectedDeliveredBuffer.getDataY());
318     assertThat(deliveredBuffer.getDataU()).isEqualTo(expectedDeliveredBuffer.getDataU());
319     assertThat(deliveredBuffer.getDataV()).isEqualTo(expectedDeliveredBuffer.getDataV());
320   }
321 
322   @Test
testRendersOutputTexture()323   public void testRendersOutputTexture() throws InterruptedException {
324     // Set-up.
325     TestDecoder decoder = new TestDecoderBuilder().build();
326     decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback);
327     decoder.decode(createTestEncodedImage(),
328         new DecodeInfo(/* isMissingFrames= */ false, /* renderTimeMs= */ 0));
329     int bufferIndex =
330         fakeMediaCodecWrapper.addOutputTexture(/* presentationTimestampUs= */ 0, /* flags= */ 0);
331 
332     // Test.
333     decoder.waitDeliverDecodedFrame();
334 
335     // Verify.
336     verify(fakeMediaCodecWrapper).releaseOutputBuffer(bufferIndex, /* render= */ true);
337   }
338 
339   @Test
testSurfaceTextureStall_FramesDropped()340   public void testSurfaceTextureStall_FramesDropped() throws InterruptedException {
341     final int numFrames = 10;
342     // Maximum number of frame the decoder can keep queued on the output side.
343     final int maxQueuedBuffers = 3;
344 
345     // Set-up.
346     TestDecoder decoder = new TestDecoderBuilder().build();
347     decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback);
348 
349     // Test.
350     int[] bufferIndices = new int[numFrames];
351     for (int i = 0; i < 10; i++) {
352       decoder.decode(createTestEncodedImage(),
353           new DecodeInfo(/* isMissingFrames= */ false, /* renderTimeMs= */ 0));
354       bufferIndices[i] =
355           fakeMediaCodecWrapper.addOutputTexture(/* presentationTimestampUs= */ 0, /* flags= */ 0);
356       decoder.waitDeliverDecodedFrame();
357     }
358 
359     // Verify.
360     InOrder releaseOrder = inOrder(fakeMediaCodecWrapper);
361     releaseOrder.verify(fakeMediaCodecWrapper)
362         .releaseOutputBuffer(bufferIndices[0], /* render= */ true);
363     for (int i = 1; i < numFrames - maxQueuedBuffers; i++) {
364       releaseOrder.verify(fakeMediaCodecWrapper)
365           .releaseOutputBuffer(bufferIndices[i], /* render= */ false);
366     }
367   }
368 
369   @Test
testDeliversRenderedBuffers()370   public void testDeliversRenderedBuffers() throws InterruptedException {
371     // Set-up.
372     TestDecoder decoder = new TestDecoderBuilder().build();
373     decoder.initDecode(TEST_DECODER_SETTINGS, fakeDecoderCallback);
374     decoder.decode(createTestEncodedImage(),
375         new DecodeInfo(/* isMissingFrames= */ false, /* renderTimeMs= */ 0));
376     fakeMediaCodecWrapper.addOutputTexture(/* presentationTimestampUs= */ 0, /* flags= */ 0);
377 
378     // Render the output buffer.
379     decoder.waitDeliverDecodedFrame();
380 
381     ArgumentCaptor<VideoSink> videoSinkCaptor = ArgumentCaptor.forClass(VideoSink.class);
382     verify(mockSurfaceTextureHelper).startListening(videoSinkCaptor.capture());
383 
384     // Test.
385     Runnable releaseCallback = mock(Runnable.class);
386     VideoFrame.TextureBuffer outputTextureBuffer =
387         new TextureBufferImpl(TEST_DECODER_SETTINGS.width, TEST_DECODER_SETTINGS.height, Type.OES,
388             /* id= */ 0,
389             /* transformMatrix= */ new Matrix(),
390             /* toI420Handler= */ new Handler(), new YuvConverter(), releaseCallback);
391     VideoFrame outputVideoFrame =
392         new VideoFrame(outputTextureBuffer, /* rotation= */ 0, /* timestampNs= */ 0);
393     videoSinkCaptor.getValue().onFrame(outputVideoFrame);
394     outputVideoFrame.release();
395 
396     // Verify.
397     assertThat(fakeDecoderCallback.decodedFrames).hasSize(1);
398     VideoFrame videoFrame = fakeDecoderCallback.decodedFrames.get(0);
399     assertThat(videoFrame).isNotNull();
400     assertThat(videoFrame.getBuffer()).isEqualTo(outputTextureBuffer);
401 
402     fakeDecoderCallback.release();
403 
404     verify(releaseCallback).run();
405   }
406 
407   @Test
testConfigureExceptionTriggerSWFallback()408   public void testConfigureExceptionTriggerSWFallback() {
409     // Set-up.
410     doThrow(new IllegalStateException("Fake error"))
411         .when(fakeMediaCodecWrapper)
412         .configure(any(), any(), any(), anyInt());
413 
414     AndroidVideoDecoder decoder = new TestDecoderBuilder().build();
415 
416     // Test.
417     assertThat(decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback))
418         .isEqualTo(VideoCodecStatus.FALLBACK_SOFTWARE);
419   }
420 
421   @Test
testStartExceptionTriggerSWFallback()422   public void testStartExceptionTriggerSWFallback() {
423     // Set-up.
424     doThrow(new IllegalStateException("Fake error")).when(fakeMediaCodecWrapper).start();
425 
426     AndroidVideoDecoder decoder = new TestDecoderBuilder().build();
427 
428     // Test.
429     assertThat(decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback))
430         .isEqualTo(VideoCodecStatus.FALLBACK_SOFTWARE);
431   }
432 }
433