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