blob: c1e8f6c3b34f794ff30517b0f94787b51360df63 [file] [log] [blame]
/*
* Copyright 2018 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 com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.graphics.Matrix;
import android.graphics.SurfaceTexture;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaFormat;
import android.os.Handler;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.chromium.testing.local.LocalRobolectricTestRunner;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.webrtc.EncodedImage.FrameType;
import org.webrtc.FakeMediaCodecWrapper.State;
import org.webrtc.VideoDecoder.DecodeInfo;
import org.webrtc.VideoFrame.I420Buffer;
import org.webrtc.VideoFrame.TextureBuffer.Type;
@RunWith(LocalRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class AndroidVideoDecoderTest {
private static final VideoDecoder.Settings TEST_DECODER_SETTINGS =
new VideoDecoder.Settings(/* numberOfCores= */ 1, /* width= */ 640, /* height= */ 480);
private static final int COLOR_FORMAT = CodecCapabilities.COLOR_FormatYUV420Planar;
private static final long POLL_DELAY_MS = 10;
private static final long DELIVER_DECODED_IMAGE_DELAY_MS = 10;
private static final byte[] ENCODED_TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
private class TestDecoder extends AndroidVideoDecoder {
private final Object deliverDecodedFrameLock = new Object();
private boolean deliverDecodedFrameDone = true;
public TestDecoder(MediaCodecWrapperFactory mediaCodecFactory, String codecName,
VideoCodecMimeType codecType, int colorFormat, EglBase.Context sharedContext) {
super(mediaCodecFactory, codecName, codecType, colorFormat, sharedContext);
}
public void waitDeliverDecodedFrame() throws InterruptedException {
synchronized (deliverDecodedFrameLock) {
deliverDecodedFrameDone = false;
deliverDecodedFrameLock.notifyAll();
while (!deliverDecodedFrameDone) {
deliverDecodedFrameLock.wait();
}
}
}
@SuppressWarnings("WaitNotInLoop") // This method is called inside a loop.
@Override
protected void deliverDecodedFrame() {
synchronized (deliverDecodedFrameLock) {
if (deliverDecodedFrameDone) {
try {
deliverDecodedFrameLock.wait(DELIVER_DECODED_IMAGE_DELAY_MS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
if (deliverDecodedFrameDone) {
return;
}
super.deliverDecodedFrame();
deliverDecodedFrameDone = true;
deliverDecodedFrameLock.notifyAll();
}
}
@Override
protected SurfaceTextureHelper createSurfaceTextureHelper() {
return mockSurfaceTextureHelper;
}
@Override
protected void releaseSurface() {}
@Override
protected VideoFrame.I420Buffer allocateI420Buffer(int width, int height) {
int chromaHeight = (height + 1) / 2;
int strideUV = (width + 1) / 2;
int yPos = 0;
int uPos = yPos + width * height;
int vPos = uPos + strideUV * chromaHeight;
ByteBuffer buffer = ByteBuffer.allocateDirect(width * height + 2 * strideUV * chromaHeight);
buffer.position(yPos);
buffer.limit(uPos);
ByteBuffer dataY = buffer.slice();
buffer.position(uPos);
buffer.limit(vPos);
ByteBuffer dataU = buffer.slice();
buffer.position(vPos);
buffer.limit(vPos + strideUV * chromaHeight);
ByteBuffer dataV = buffer.slice();
return JavaI420Buffer.wrap(width, height, dataY, width, dataU, strideUV, dataV, strideUV,
/* releaseCallback= */ null);
}
@Override
protected void copyPlane(
ByteBuffer src, int srcStride, ByteBuffer dst, int dstStride, int width, int height) {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
dst.put(y * dstStride + x, src.get(y * srcStride + x));
}
}
}
}
private class TestDecoderBuilder {
private VideoCodecMimeType codecType = VideoCodecMimeType.VP8;
private boolean useSurface = true;
public TestDecoderBuilder setCodecType(VideoCodecMimeType codecType) {
this.codecType = codecType;
return this;
}
public TestDecoderBuilder setUseSurface(boolean useSurface) {
this.useSurface = useSurface;
return this;
}
public TestDecoder build() {
return new TestDecoder((String name)
-> fakeMediaCodecWrapper,
/* codecName= */ "org.webrtc.testdecoder", codecType, COLOR_FORMAT,
useSurface ? mockEglBaseContext : null);
}
}
private static class FakeDecoderCallback implements VideoDecoder.Callback {
public final List<VideoFrame> decodedFrames;
public FakeDecoderCallback() {
decodedFrames = new ArrayList<>();
}
@Override
public void onDecodedFrame(VideoFrame frame, Integer decodeTimeMs, Integer qp) {
frame.retain();
decodedFrames.add(frame);
}
public void release() {
for (VideoFrame frame : decodedFrames) frame.release();
decodedFrames.clear();
}
}
private EncodedImage createTestEncodedImage() {
return EncodedImage.builder()
.setBuffer(ByteBuffer.wrap(ENCODED_TEST_DATA), null)
.setFrameType(FrameType.VideoFrameKey)
.setCompleteFrame(true)
.createEncodedImage();
}
@Mock private EglBase.Context mockEglBaseContext;
@Mock private SurfaceTextureHelper mockSurfaceTextureHelper;
@Mock private VideoDecoder.Callback mockDecoderCallback;
private FakeMediaCodecWrapper fakeMediaCodecWrapper;
private FakeDecoderCallback fakeDecoderCallback;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(mockSurfaceTextureHelper.getSurfaceTexture())
.thenReturn(new SurfaceTexture(/*texName=*/0));
MediaFormat outputFormat = new MediaFormat();
// TODO(sakal): Add more details to output format as needed.
fakeMediaCodecWrapper = spy(new FakeMediaCodecWrapper(outputFormat));
fakeDecoderCallback = new FakeDecoderCallback();
}
@After
public void cleanUp() {
fakeDecoderCallback.release();
}
@Test
public void testInit() {
// Set-up.
AndroidVideoDecoder decoder =
new TestDecoderBuilder().setCodecType(VideoCodecMimeType.VP8).build();
// Test.
assertThat(decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback))
.isEqualTo(VideoCodecStatus.OK);
// Verify.
assertThat(fakeMediaCodecWrapper.getState()).isEqualTo(State.EXECUTING_RUNNING);
MediaFormat mediaFormat = fakeMediaCodecWrapper.getConfiguredFormat();
assertThat(mediaFormat).isNotNull();
assertThat(mediaFormat.getInteger(MediaFormat.KEY_WIDTH))
.isEqualTo(TEST_DECODER_SETTINGS.width);
assertThat(mediaFormat.getInteger(MediaFormat.KEY_HEIGHT))
.isEqualTo(TEST_DECODER_SETTINGS.height);
assertThat(mediaFormat.getString(MediaFormat.KEY_MIME))
.isEqualTo(VideoCodecMimeType.VP8.mimeType());
}
@Test
public void testRelease() {
// Set-up.
AndroidVideoDecoder decoder = new TestDecoderBuilder().build();
decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback);
// Test.
assertThat(decoder.release()).isEqualTo(VideoCodecStatus.OK);
// Verify.
assertThat(fakeMediaCodecWrapper.getState()).isEqualTo(State.RELEASED);
}
@Test
public void testReleaseMultipleTimes() {
// Set-up.
AndroidVideoDecoder decoder = new TestDecoderBuilder().build();
decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback);
// Test.
assertThat(decoder.release()).isEqualTo(VideoCodecStatus.OK);
assertThat(decoder.release()).isEqualTo(VideoCodecStatus.OK);
// Verify.
assertThat(fakeMediaCodecWrapper.getState()).isEqualTo(State.RELEASED);
}
@Test
public void testDecodeQueuesData() {
// Set-up.
AndroidVideoDecoder decoder = new TestDecoderBuilder().build();
decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback);
// Test.
assertThat(decoder.decode(createTestEncodedImage(),
new DecodeInfo(/* isMissingFrames= */ false, /* renderTimeMs= */ 0)))
.isEqualTo(VideoCodecStatus.OK);
// Verify.
ArgumentCaptor<Integer> indexCaptor = ArgumentCaptor.forClass(Integer.class);
ArgumentCaptor<Integer> offsetCaptor = ArgumentCaptor.forClass(Integer.class);
ArgumentCaptor<Integer> sizeCaptor = ArgumentCaptor.forClass(Integer.class);
verify(fakeMediaCodecWrapper)
.queueInputBuffer(indexCaptor.capture(), offsetCaptor.capture(), sizeCaptor.capture(),
/* presentationTimeUs= */ anyLong(),
/* flags= */ eq(0));
ByteBuffer inputBuffer = fakeMediaCodecWrapper.getInputBuffers()[indexCaptor.getValue()];
CodecTestHelper.assertEqualContents(
ENCODED_TEST_DATA, inputBuffer, offsetCaptor.getValue(), sizeCaptor.getValue());
}
@Test
public void testDeliversOutputByteBuffers() throws InterruptedException {
final byte[] testOutputData = CodecTestHelper.generateRandomData(
TEST_DECODER_SETTINGS.width * TEST_DECODER_SETTINGS.height * 3 / 2);
final I420Buffer expectedDeliveredBuffer = CodecTestHelper.wrapI420(
TEST_DECODER_SETTINGS.width, TEST_DECODER_SETTINGS.height, testOutputData);
// Set-up.
TestDecoder decoder = new TestDecoderBuilder().setUseSurface(/* useSurface = */ false).build();
decoder.initDecode(TEST_DECODER_SETTINGS, fakeDecoderCallback);
decoder.decode(createTestEncodedImage(),
new DecodeInfo(/* isMissingFrames= */ false, /* renderTimeMs= */ 0));
fakeMediaCodecWrapper.addOutputData(
testOutputData, /* presentationTimestampUs= */ 0, /* flags= */ 0);
// Test.
decoder.waitDeliverDecodedFrame();
// Verify.
assertThat(fakeDecoderCallback.decodedFrames).hasSize(1);
VideoFrame videoFrame = fakeDecoderCallback.decodedFrames.get(0);
assertThat(videoFrame).isNotNull();
assertThat(videoFrame.getRotatedWidth()).isEqualTo(TEST_DECODER_SETTINGS.width);
assertThat(videoFrame.getRotatedHeight()).isEqualTo(TEST_DECODER_SETTINGS.height);
assertThat(videoFrame.getRotation()).isEqualTo(0);
I420Buffer deliveredBuffer = videoFrame.getBuffer().toI420();
assertThat(deliveredBuffer.getDataY()).isEqualTo(expectedDeliveredBuffer.getDataY());
assertThat(deliveredBuffer.getDataU()).isEqualTo(expectedDeliveredBuffer.getDataU());
assertThat(deliveredBuffer.getDataV()).isEqualTo(expectedDeliveredBuffer.getDataV());
}
@Test
public void testRendersOutputTexture() throws InterruptedException {
// Set-up.
TestDecoder decoder = new TestDecoderBuilder().build();
decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback);
decoder.decode(createTestEncodedImage(),
new DecodeInfo(/* isMissingFrames= */ false, /* renderTimeMs= */ 0));
int bufferIndex =
fakeMediaCodecWrapper.addOutputTexture(/* presentationTimestampUs= */ 0, /* flags= */ 0);
// Test.
decoder.waitDeliverDecodedFrame();
// Verify.
verify(fakeMediaCodecWrapper).releaseOutputBuffer(bufferIndex, /* render= */ true);
}
@Test
public void testSurfaceTextureStall_FramesDropped() throws InterruptedException {
final int numFrames = 10;
// Maximum number of frame the decoder can keep queued on the output side.
final int maxQueuedBuffers = 3;
// Set-up.
TestDecoder decoder = new TestDecoderBuilder().build();
decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback);
// Test.
int[] bufferIndices = new int[numFrames];
for (int i = 0; i < 10; i++) {
decoder.decode(createTestEncodedImage(),
new DecodeInfo(/* isMissingFrames= */ false, /* renderTimeMs= */ 0));
bufferIndices[i] =
fakeMediaCodecWrapper.addOutputTexture(/* presentationTimestampUs= */ 0, /* flags= */ 0);
decoder.waitDeliverDecodedFrame();
}
// Verify.
InOrder releaseOrder = inOrder(fakeMediaCodecWrapper);
releaseOrder.verify(fakeMediaCodecWrapper)
.releaseOutputBuffer(bufferIndices[0], /* render= */ true);
for (int i = 1; i < numFrames - maxQueuedBuffers; i++) {
releaseOrder.verify(fakeMediaCodecWrapper)
.releaseOutputBuffer(bufferIndices[i], /* render= */ false);
}
}
@Test
public void testDeliversRenderedBuffers() throws InterruptedException {
// Set-up.
TestDecoder decoder = new TestDecoderBuilder().build();
decoder.initDecode(TEST_DECODER_SETTINGS, fakeDecoderCallback);
decoder.decode(createTestEncodedImage(),
new DecodeInfo(/* isMissingFrames= */ false, /* renderTimeMs= */ 0));
fakeMediaCodecWrapper.addOutputTexture(/* presentationTimestampUs= */ 0, /* flags= */ 0);
// Render the output buffer.
decoder.waitDeliverDecodedFrame();
ArgumentCaptor<VideoSink> videoSinkCaptor = ArgumentCaptor.forClass(VideoSink.class);
verify(mockSurfaceTextureHelper).startListening(videoSinkCaptor.capture());
// Test.
Runnable releaseCallback = mock(Runnable.class);
VideoFrame.TextureBuffer outputTextureBuffer =
new TextureBufferImpl(TEST_DECODER_SETTINGS.width, TEST_DECODER_SETTINGS.height, Type.OES,
/* id= */ 0,
/* transformMatrix= */ new Matrix(),
/* toI420Handler= */ new Handler(), new YuvConverter(), releaseCallback);
VideoFrame outputVideoFrame =
new VideoFrame(outputTextureBuffer, /* rotation= */ 0, /* timestampNs= */ 0);
videoSinkCaptor.getValue().onFrame(outputVideoFrame);
outputVideoFrame.release();
// Verify.
assertThat(fakeDecoderCallback.decodedFrames).hasSize(1);
VideoFrame videoFrame = fakeDecoderCallback.decodedFrames.get(0);
assertThat(videoFrame).isNotNull();
assertThat(videoFrame.getBuffer()).isEqualTo(outputTextureBuffer);
fakeDecoderCallback.release();
verify(releaseCallback).run();
}
@Test
public void testConfigureExceptionTriggerSWFallback() {
// Set-up.
doThrow(new IllegalStateException("Fake error"))
.when(fakeMediaCodecWrapper)
.configure(any(), any(), any(), anyInt());
AndroidVideoDecoder decoder = new TestDecoderBuilder().build();
// Test.
assertThat(decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback))
.isEqualTo(VideoCodecStatus.FALLBACK_SOFTWARE);
}
@Test
public void testStartExceptionTriggerSWFallback() {
// Set-up.
doThrow(new IllegalStateException("Fake error")).when(fakeMediaCodecWrapper).start();
AndroidVideoDecoder decoder = new TestDecoderBuilder().build();
// Test.
assertThat(decoder.initDecode(TEST_DECODER_SETTINGS, mockDecoderCallback))
.isEqualTo(VideoCodecStatus.FALLBACK_SOFTWARE);
}
}