/* * 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.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyLong; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.robolectric.Shadows.shadowOf; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import org.chromium.testing.local.LocalRobolectricTestRunner; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowSystemClock; import org.webrtc.EglBase.Context; import org.webrtc.EncodedImage; import org.webrtc.EncodedImage.FrameType; import org.webrtc.FakeMediaCodecWrapper.State; import org.webrtc.VideoCodecStatus; import org.webrtc.VideoEncoder; import org.webrtc.VideoEncoder.CodecSpecificInfo; import org.webrtc.VideoEncoder.EncodeInfo; import org.webrtc.VideoEncoder.Settings; import org.webrtc.VideoFrame; import org.webrtc.VideoFrame.Buffer; import org.webrtc.VideoFrame.I420Buffer; @RunWith(LocalRobolectricTestRunner.class) @Config(manifest = Config.NONE) public class HardwareVideoEncoderTest { private static final VideoEncoder.Settings TEST_ENCODER_SETTINGS = new Settings( /* numberOfCores= */ 1, /* width= */ 640, /* height= */ 480, /* startBitrate= */ 10000, /* maxFramerate= */ 30, /* numberOfSimulcastStreams= */ 1, /* automaticResizeOn= */ true); private static final long POLL_DELAY_MS = 10; private static final long DELIVER_ENCODED_IMAGE_DELAY_MS = 10; private static class TestEncoder extends HardwareVideoEncoder { private final Object deliverEncodedImageLock = new Object(); private boolean deliverEncodedImageDone = true; TestEncoder(MediaCodecWrapperFactory mediaCodecWrapperFactory, String codecName, VideoCodecType codecType, Integer surfaceColorFormat, Integer yuvColorFormat, Map params, int keyFrameIntervalSec, int forceKeyFrameIntervalMs, BitrateAdjuster bitrateAdjuster, EglBase14.Context sharedContext) { super(mediaCodecWrapperFactory, codecName, codecType, surfaceColorFormat, yuvColorFormat, params, keyFrameIntervalSec, forceKeyFrameIntervalMs, bitrateAdjuster, sharedContext); } public void waitDeliverEncodedImage() throws InterruptedException { synchronized (deliverEncodedImageLock) { deliverEncodedImageDone = false; deliverEncodedImageLock.notifyAll(); while (!deliverEncodedImageDone) { deliverEncodedImageLock.wait(); } } } @SuppressWarnings("WaitNotInLoop") // This method is called inside a loop. @Override protected void deliverEncodedImage() { synchronized (deliverEncodedImageLock) { if (deliverEncodedImageDone) { try { deliverEncodedImageLock.wait(DELIVER_ENCODED_IMAGE_DELAY_MS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } if (deliverEncodedImageDone) { return; } super.deliverEncodedImage(); deliverEncodedImageDone = true; deliverEncodedImageLock.notifyAll(); } } @Override protected void fillInputBuffer(ByteBuffer buffer, Buffer videoFrameBuffer) { I420Buffer i420Buffer = videoFrameBuffer.toI420(); buffer.put(i420Buffer.getDataY()); buffer.put(i420Buffer.getDataU()); buffer.put(i420Buffer.getDataV()); buffer.flip(); i420Buffer.release(); } } private class TestEncoderBuilder { private VideoCodecType codecType = VideoCodecType.VP8; public TestEncoderBuilder setCodecType(VideoCodecType codecType) { this.codecType = codecType; return this; } public TestEncoder build() { return new TestEncoder((String name) -> fakeMediaCodecWrapper, "org.webrtc.testencoder", codecType, /* surfaceColorFormat= */ null, /* yuvColorFormat= */ MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar, /* params= */ new HashMap<>(), /* keyFrameIntervalSec= */ 0, /* forceKeyFrameIntervalMs= */ 0, /* bitrateAdjuster= */ new BaseBitrateAdjuster(), /* sharedContext= */ null); } } @Mock VideoEncoder.Callback mockEncoderCallback; private FakeMediaCodecWrapper fakeMediaCodecWrapper; @Before public void setUp() { MockitoAnnotations.initMocks(this); MediaFormat outputFormat = new MediaFormat(); // TODO(sakal): Add more details to output format as needed. fakeMediaCodecWrapper = spy(new FakeMediaCodecWrapper(outputFormat)); } @Test public void testInit() { // Set-up. HardwareVideoEncoder encoder = new TestEncoderBuilder().setCodecType(VideoCodecType.VP8).build(); // Test. assertThat(encoder.initEncode(TEST_ENCODER_SETTINGS, mockEncoderCallback)) .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_ENCODER_SETTINGS.width); assertThat(mediaFormat.getInteger(MediaFormat.KEY_HEIGHT)) .isEqualTo(TEST_ENCODER_SETTINGS.height); assertThat(mediaFormat.getString(MediaFormat.KEY_MIME)) .isEqualTo(VideoCodecType.VP8.mimeType()); assertThat(fakeMediaCodecWrapper.getConfiguredFlags()) .isEqualTo(MediaCodec.CONFIGURE_FLAG_ENCODE); } @Test public void testEncodeByteBuffer() { // Set-up. HardwareVideoEncoder encoder = new TestEncoderBuilder().build(); encoder.initEncode(TEST_ENCODER_SETTINGS, mockEncoderCallback); // Test. byte[] i420 = CodecTestHelper.generateRandomData( TEST_ENCODER_SETTINGS.width * TEST_ENCODER_SETTINGS.height * 3 / 2); final VideoFrame.I420Buffer testBuffer = CodecTestHelper.wrapI420(TEST_ENCODER_SETTINGS.width, TEST_ENCODER_SETTINGS.height, i420); final VideoFrame testFrame = new VideoFrame(testBuffer, /* rotation= */ 0, /* timestampNs= */ 0); assertThat(encoder.encode(testFrame, new EncodeInfo(new FrameType[] {FrameType.VideoFrameKey}))) .isEqualTo(VideoCodecStatus.OK); // Verify. ArgumentCaptor indexCaptor = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor offsetCaptor = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor sizeCaptor = ArgumentCaptor.forClass(Integer.class); verify(fakeMediaCodecWrapper) .queueInputBuffer(indexCaptor.capture(), offsetCaptor.capture(), sizeCaptor.capture(), anyLong(), anyInt()); ByteBuffer buffer = fakeMediaCodecWrapper.getInputBuffers()[indexCaptor.getValue()]; CodecTestHelper.assertEqualContents( i420, buffer, offsetCaptor.getValue(), sizeCaptor.getValue()); } @Test public void testDeliversOutputData() throws InterruptedException { final int outputDataLength = 100; // Set-up. TestEncoder encoder = new TestEncoderBuilder().build(); encoder.initEncode(TEST_ENCODER_SETTINGS, mockEncoderCallback); byte[] i420 = CodecTestHelper.generateRandomData( TEST_ENCODER_SETTINGS.width * TEST_ENCODER_SETTINGS.height * 3 / 2); final VideoFrame.I420Buffer testBuffer = CodecTestHelper.wrapI420(TEST_ENCODER_SETTINGS.width, TEST_ENCODER_SETTINGS.height, i420); final VideoFrame testFrame = new VideoFrame(testBuffer, /* rotation= */ 0, /* timestampNs= */ 42); encoder.encode(testFrame, new EncodeInfo(new FrameType[] {FrameType.VideoFrameKey})); // Test. byte[] outputData = CodecTestHelper.generateRandomData(outputDataLength); fakeMediaCodecWrapper.addOutputData(outputData, /* presentationTimestampUs= */ 0, /* flags= */ MediaCodec.BUFFER_FLAG_SYNC_FRAME); encoder.waitDeliverEncodedImage(); // Verify. ArgumentCaptor videoFrameCaptor = ArgumentCaptor.forClass(EncodedImage.class); verify(mockEncoderCallback) .onEncodedFrame(videoFrameCaptor.capture(), any(CodecSpecificInfo.class)); EncodedImage videoFrame = videoFrameCaptor.getValue(); assertThat(videoFrame).isNotNull(); assertThat(videoFrame.encodedWidth).isEqualTo(TEST_ENCODER_SETTINGS.width); assertThat(videoFrame.encodedHeight).isEqualTo(TEST_ENCODER_SETTINGS.height); assertThat(videoFrame.rotation).isEqualTo(0); assertThat(videoFrame.captureTimeNs).isEqualTo(42); assertThat(videoFrame.completeFrame).isTrue(); assertThat(videoFrame.frameType).isEqualTo(FrameType.VideoFrameKey); CodecTestHelper.assertEqualContents( outputData, videoFrame.buffer, /* offset= */ 0, videoFrame.buffer.capacity()); } @Test public void testRelease() { // Set-up. HardwareVideoEncoder encoder = new TestEncoderBuilder().build(); encoder.initEncode(TEST_ENCODER_SETTINGS, mockEncoderCallback); // Test. assertThat(encoder.release()).isEqualTo(VideoCodecStatus.OK); // Verify. assertThat(fakeMediaCodecWrapper.getState()).isEqualTo(State.RELEASED); } @Test public void testReleaseMultipleTimes() { // Set-up. HardwareVideoEncoder encoder = new TestEncoderBuilder().build(); encoder.initEncode(TEST_ENCODER_SETTINGS, mockEncoderCallback); // Test. assertThat(encoder.release()).isEqualTo(VideoCodecStatus.OK); assertThat(encoder.release()).isEqualTo(VideoCodecStatus.OK); // Verify. assertThat(fakeMediaCodecWrapper.getState()).isEqualTo(State.RELEASED); } }