diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /third_party/libwebrtc/sdk/android/tests/src | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/libwebrtc/sdk/android/tests/src')
12 files changed, 1781 insertions, 0 deletions
diff --git a/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/AndroidVideoDecoderTest.java b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/AndroidVideoDecoderTest.java new file mode 100644 index 0000000000..535187e99e --- /dev/null +++ b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/AndroidVideoDecoderTest.java @@ -0,0 +1,432 @@ +/* + * 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 androidx.test.runner.AndroidJUnit4; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +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(AndroidJUnit4.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) + .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 inputFormat = new MediaFormat(); + MediaFormat outputFormat = new MediaFormat(); + // TODO(sakal): Add more details to output format as needed. + fakeMediaCodecWrapper = spy(new FakeMediaCodecWrapper(inputFormat, 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.getInputBuffer(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); + } +} diff --git a/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/CameraEnumerationTest.java b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/CameraEnumerationTest.java new file mode 100644 index 0000000000..2c33992f99 --- /dev/null +++ b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/CameraEnumerationTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016 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 org.junit.Assert.assertEquals; +import static org.webrtc.CameraEnumerationAndroid.getClosestSupportedFramerateRange; + +import androidx.test.runner.AndroidJUnit4; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; +import org.webrtc.CameraEnumerationAndroid.CaptureFormat.FramerateRange; + +/** + * Tests for CameraEnumerationAndroid. + */ +@RunWith(AndroidJUnit4.class) +@Config(manifest = Config.NONE) +public class CameraEnumerationTest { + @Test + public void testGetClosestSupportedFramerateRange() { + assertEquals(new FramerateRange(10000, 30000), + getClosestSupportedFramerateRange( + Arrays.asList(new FramerateRange(10000, 30000), new FramerateRange(30000, 30000)), + 30 /* requestedFps */)); + + assertEquals(new FramerateRange(10000, 20000), + getClosestSupportedFramerateRange( + Arrays.asList(new FramerateRange(0, 30000), new FramerateRange(10000, 20000), + new FramerateRange(14000, 16000), new FramerateRange(15000, 15000)), + 15 /* requestedFps */)); + + assertEquals(new FramerateRange(10000, 20000), + getClosestSupportedFramerateRange( + Arrays.asList(new FramerateRange(15000, 15000), new FramerateRange(10000, 20000), + new FramerateRange(10000, 30000)), + 10 /* requestedFps */)); + } +} diff --git a/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/CodecTestHelper.java b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/CodecTestHelper.java new file mode 100644 index 0000000000..08a10707f8 --- /dev/null +++ b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/CodecTestHelper.java @@ -0,0 +1,62 @@ +/* + * 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 com.google.common.truth.Truth.assertWithMessage; + +import java.nio.ByteBuffer; +import java.util.Random; + +/** + * Helper methods for {@link HardwareVideoEncoderTest} and {@link AndroidVideoDecoderTest}. + */ +class CodecTestHelper { + static void assertEqualContents(byte[] expected, ByteBuffer actual, int offset, int size) { + assertThat(size).isEqualTo(expected.length); + assertThat(actual.capacity()).isAtLeast(offset + size); + for (int i = 0; i < expected.length; i++) { + assertWithMessage("At index: " + i).that(actual.get(offset + i)).isEqualTo(expected[i]); + } + } + + static byte[] generateRandomData(int length) { + Random random = new Random(); + byte[] data = new byte[length]; + random.nextBytes(data); + return data; + } + + static VideoFrame.I420Buffer wrapI420(int width, int height, byte[] data) { + final int posY = 0; + final int posU = width * height; + final int posV = posU + width * height / 4; + final int endV = posV + width * height / 4; + + ByteBuffer buffer = ByteBuffer.allocateDirect(data.length); + buffer.put(data); + + buffer.limit(posU); + buffer.position(posY); + ByteBuffer dataY = buffer.slice(); + + buffer.limit(posV); + buffer.position(posU); + ByteBuffer dataU = buffer.slice(); + + buffer.limit(endV); + buffer.position(posV); + ByteBuffer dataV = buffer.slice(); + + return JavaI420Buffer.wrap(width, height, dataY, width, dataU, width / 2, dataV, width / 2, + /* releaseCallback= */ null); + } +} diff --git a/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/CryptoOptionsTest.java b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/CryptoOptionsTest.java new file mode 100644 index 0000000000..c6cd2c2008 --- /dev/null +++ b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/CryptoOptionsTest.java @@ -0,0 +1,74 @@ +/* + * 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 androidx.test.runner.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; +import org.webrtc.CryptoOptions; + +@RunWith(AndroidJUnit4.class) +@Config(manifest = Config.NONE) +public class CryptoOptionsTest { + // Validates the builder builds by default all false options. + @Test + public void testBuilderDefaultsAreFalse() { + CryptoOptions cryptoOptions = CryptoOptions.builder().createCryptoOptions(); + assertThat(cryptoOptions.getSrtp().getEnableGcmCryptoSuites()).isFalse(); + assertThat(cryptoOptions.getSrtp().getEnableAes128Sha1_32CryptoCipher()).isFalse(); + assertThat(cryptoOptions.getSrtp().getEnableEncryptedRtpHeaderExtensions()).isFalse(); + assertThat(cryptoOptions.getSFrame().getRequireFrameEncryption()).isFalse(); + } + + // Validates the builder sets the correct parameters. + @Test + public void testBuilderCorrectlyInitializingGcmCrypto() { + CryptoOptions cryptoOptions = + CryptoOptions.builder().setEnableGcmCryptoSuites(true).createCryptoOptions(); + assertThat(cryptoOptions.getSrtp().getEnableGcmCryptoSuites()).isTrue(); + assertThat(cryptoOptions.getSrtp().getEnableAes128Sha1_32CryptoCipher()).isFalse(); + assertThat(cryptoOptions.getSrtp().getEnableEncryptedRtpHeaderExtensions()).isFalse(); + assertThat(cryptoOptions.getSFrame().getRequireFrameEncryption()).isFalse(); + } + + @Test + public void testBuilderCorrectlyInitializingAes128Sha1_32CryptoCipher() { + CryptoOptions cryptoOptions = + CryptoOptions.builder().setEnableAes128Sha1_32CryptoCipher(true).createCryptoOptions(); + assertThat(cryptoOptions.getSrtp().getEnableGcmCryptoSuites()).isFalse(); + assertThat(cryptoOptions.getSrtp().getEnableAes128Sha1_32CryptoCipher()).isTrue(); + assertThat(cryptoOptions.getSrtp().getEnableEncryptedRtpHeaderExtensions()).isFalse(); + assertThat(cryptoOptions.getSFrame().getRequireFrameEncryption()).isFalse(); + } + + @Test + public void testBuilderCorrectlyInitializingEncryptedRtpHeaderExtensions() { + CryptoOptions cryptoOptions = + CryptoOptions.builder().setEnableEncryptedRtpHeaderExtensions(true).createCryptoOptions(); + assertThat(cryptoOptions.getSrtp().getEnableGcmCryptoSuites()).isFalse(); + assertThat(cryptoOptions.getSrtp().getEnableAes128Sha1_32CryptoCipher()).isFalse(); + assertThat(cryptoOptions.getSrtp().getEnableEncryptedRtpHeaderExtensions()).isTrue(); + assertThat(cryptoOptions.getSFrame().getRequireFrameEncryption()).isFalse(); + } + + @Test + public void testBuilderCorrectlyInitializingRequireFrameEncryption() { + CryptoOptions cryptoOptions = + CryptoOptions.builder().setRequireFrameEncryption(true).createCryptoOptions(); + assertThat(cryptoOptions.getSrtp().getEnableGcmCryptoSuites()).isFalse(); + assertThat(cryptoOptions.getSrtp().getEnableAes128Sha1_32CryptoCipher()).isFalse(); + assertThat(cryptoOptions.getSrtp().getEnableEncryptedRtpHeaderExtensions()).isFalse(); + assertThat(cryptoOptions.getSFrame().getRequireFrameEncryption()).isTrue(); + } +} diff --git a/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/FakeMediaCodecWrapper.java b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/FakeMediaCodecWrapper.java new file mode 100644 index 0000000000..fb7aba4700 --- /dev/null +++ b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/FakeMediaCodecWrapper.java @@ -0,0 +1,321 @@ +/* + * 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 android.graphics.SurfaceTexture; +import android.media.MediaCodec; +import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaCrypto; +import android.media.MediaFormat; +import android.os.Bundle; +import android.view.Surface; +import androidx.annotation.Nullable; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * Fake MediaCodec that implements the basic state machine. + * + * @note This class is only intended for single-threaded tests and is not thread-safe. + */ +public class FakeMediaCodecWrapper implements MediaCodecWrapper { + private static final int NUM_INPUT_BUFFERS = 10; + private static final int NUM_OUTPUT_BUFFERS = 10; + private static final int MAX_ENCODED_DATA_SIZE_BYTES = 1_000; + + /** + * MediaCodec state as defined by: + * https://developer.android.com/reference/android/media/MediaCodec.html + */ + public enum State { + STOPPED_CONFIGURED(Primary.STOPPED), + STOPPED_UNINITIALIZED(Primary.STOPPED), + STOPPED_ERROR(Primary.STOPPED), + EXECUTING_FLUSHED(Primary.EXECUTING), + EXECUTING_RUNNING(Primary.EXECUTING), + EXECUTING_END_OF_STREAM(Primary.EXECUTING), + RELEASED(Primary.RELEASED); + + public enum Primary { STOPPED, EXECUTING, RELEASED } + + private final Primary primary; + + State(Primary primary) { + this.primary = primary; + } + + public Primary getPrimary() { + return primary; + } + } + + /** Represents an output buffer that will be returned by dequeueOutputBuffer. */ + public static class QueuedOutputBufferInfo { + private int index; + private int offset; + private int size; + private long presentationTimeUs; + private int flags; + + private QueuedOutputBufferInfo( + int index, int offset, int size, long presentationTimeUs, int flags) { + this.index = index; + this.offset = offset; + this.size = size; + this.presentationTimeUs = presentationTimeUs; + this.flags = flags; + } + + public static QueuedOutputBufferInfo create( + int index, int offset, int size, long presentationTimeUs, int flags) { + return new QueuedOutputBufferInfo(index, offset, size, presentationTimeUs, flags); + } + + public int getIndex() { + return index; + } + + public int getOffset() { + return offset; + } + + public int getSize() { + return size; + } + + public long getPresentationTimeUs() { + return presentationTimeUs; + } + + public int getFlags() { + return flags; + } + } + + private State state = State.STOPPED_UNINITIALIZED; + private @Nullable MediaFormat configuredFormat; + private int configuredFlags; + private final MediaFormat inputFormat; + private final MediaFormat outputFormat; + private final ByteBuffer[] inputBuffers = new ByteBuffer[NUM_INPUT_BUFFERS]; + private final ByteBuffer[] outputBuffers = new ByteBuffer[NUM_OUTPUT_BUFFERS]; + private final boolean[] inputBufferReserved = new boolean[NUM_INPUT_BUFFERS]; + private final boolean[] outputBufferReserved = new boolean[NUM_OUTPUT_BUFFERS]; + private final List<QueuedOutputBufferInfo> queuedOutputBuffers = new ArrayList<>(); + + public FakeMediaCodecWrapper(MediaFormat inputFormat, MediaFormat outputFormat) { + this.inputFormat = inputFormat; + this.outputFormat = outputFormat; + } + + /** Returns the current simulated state of MediaCodec. */ + public State getState() { + return state; + } + + /** Gets the last configured media format passed to configure. */ + public @Nullable MediaFormat getConfiguredFormat() { + return configuredFormat; + } + + /** Returns the last flags passed to configure. */ + public int getConfiguredFlags() { + return configuredFlags; + } + + /** + * Adds a texture buffer that will be returned by dequeueOutputBuffer. Returns index of the + * buffer. + */ + public int addOutputTexture(long presentationTimestampUs, int flags) { + int index = getFreeOutputBuffer(); + queuedOutputBuffers.add(QueuedOutputBufferInfo.create( + index, /* offset= */ 0, /* size= */ 0, presentationTimestampUs, flags)); + return index; + } + + /** + * Adds a byte buffer buffer that will be returned by dequeueOutputBuffer. Returns index of the + * buffer. + */ + public int addOutputData(byte[] data, long presentationTimestampUs, int flags) { + int index = getFreeOutputBuffer(); + ByteBuffer outputBuffer = outputBuffers[index]; + + outputBuffer.clear(); + outputBuffer.put(data); + outputBuffer.rewind(); + + queuedOutputBuffers.add(QueuedOutputBufferInfo.create( + index, /* offset= */ 0, data.length, presentationTimestampUs, flags)); + return index; + } + + /** + * Returns the first output buffer that is not reserved and reserves it. It will be stay reserved + * until released with releaseOutputBuffer. + */ + private int getFreeOutputBuffer() { + for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) { + if (!outputBufferReserved[i]) { + outputBufferReserved[i] = true; + return i; + } + } + throw new RuntimeException("All output buffers reserved!"); + } + + @Override + public void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags) { + if (state != State.STOPPED_UNINITIALIZED) { + throw new IllegalStateException("Expected state STOPPED_UNINITIALIZED but was " + state); + } + state = State.STOPPED_CONFIGURED; + configuredFormat = format; + configuredFlags = flags; + + final int width = configuredFormat.getInteger(MediaFormat.KEY_WIDTH); + final int height = configuredFormat.getInteger(MediaFormat.KEY_HEIGHT); + final int yuvSize = width * height * 3 / 2; + final int inputBufferSize; + final int outputBufferSize; + + if ((flags & MediaCodec.CONFIGURE_FLAG_ENCODE) != 0) { + final int colorFormat = configuredFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT); + + inputBufferSize = colorFormat == CodecCapabilities.COLOR_FormatSurface ? 0 : yuvSize; + outputBufferSize = MAX_ENCODED_DATA_SIZE_BYTES; + } else { + inputBufferSize = MAX_ENCODED_DATA_SIZE_BYTES; + outputBufferSize = surface != null ? 0 : yuvSize; + } + + for (int i = 0; i < inputBuffers.length; i++) { + inputBuffers[i] = ByteBuffer.allocateDirect(inputBufferSize); + } + for (int i = 0; i < outputBuffers.length; i++) { + outputBuffers[i] = ByteBuffer.allocateDirect(outputBufferSize); + } + } + + @Override + public void start() { + if (state != State.STOPPED_CONFIGURED) { + throw new IllegalStateException("Expected state STOPPED_CONFIGURED but was " + state); + } + state = State.EXECUTING_RUNNING; + } + + @Override + public void flush() { + if (state.getPrimary() != State.Primary.EXECUTING) { + throw new IllegalStateException("Expected state EXECUTING but was " + state); + } + state = State.EXECUTING_FLUSHED; + } + + @Override + public void stop() { + if (state.getPrimary() != State.Primary.EXECUTING) { + throw new IllegalStateException("Expected state EXECUTING but was " + state); + } + state = State.STOPPED_UNINITIALIZED; + } + + @Override + public void release() { + state = State.RELEASED; + } + + @Override + public int dequeueInputBuffer(long timeoutUs) { + if (state != State.EXECUTING_FLUSHED && state != State.EXECUTING_RUNNING) { + throw new IllegalStateException( + "Expected state EXECUTING_FLUSHED or EXECUTING_RUNNING but was " + state); + } + state = State.EXECUTING_RUNNING; + + for (int i = 0; i < NUM_INPUT_BUFFERS; i++) { + if (!inputBufferReserved[i]) { + inputBufferReserved[i] = true; + return i; + } + } + return MediaCodec.INFO_TRY_AGAIN_LATER; + } + + @Override + public void queueInputBuffer( + int index, int offset, int size, long presentationTimeUs, int flags) { + if (state.getPrimary() != State.Primary.EXECUTING) { + throw new IllegalStateException("Expected state EXECUTING but was " + state); + } + if (flags != 0) { + throw new UnsupportedOperationException( + "Flags are not implemented in FakeMediaCodecWrapper."); + } + } + + @Override + public int dequeueOutputBuffer(MediaCodec.BufferInfo info, long timeoutUs) { + if (state.getPrimary() != State.Primary.EXECUTING) { + throw new IllegalStateException("Expected state EXECUTING but was " + state); + } + + if (queuedOutputBuffers.isEmpty()) { + return MediaCodec.INFO_TRY_AGAIN_LATER; + } + QueuedOutputBufferInfo outputBufferInfo = queuedOutputBuffers.remove(/* index= */ 0); + info.set(outputBufferInfo.getOffset(), outputBufferInfo.getSize(), + outputBufferInfo.getPresentationTimeUs(), outputBufferInfo.getFlags()); + return outputBufferInfo.getIndex(); + } + + @Override + public void releaseOutputBuffer(int index, boolean render) { + if (state.getPrimary() != State.Primary.EXECUTING) { + throw new IllegalStateException("Expected state EXECUTING but was " + state); + } + if (!outputBufferReserved[index]) { + throw new RuntimeException("Released output buffer was not in use."); + } + outputBufferReserved[index] = false; + } + + @Override + public ByteBuffer getInputBuffer(int index) { + return inputBuffers[index]; + } + + @Override + public ByteBuffer getOutputBuffer(int index) { + return outputBuffers[index]; + } + + @Override + public MediaFormat getInputFormat() { + return inputFormat; + } + + @Override + public MediaFormat getOutputFormat() { + return outputFormat; + } + + @Override + public Surface createInputSurface() { + return new Surface(new SurfaceTexture(/* texName= */ 0)); + } + + @Override + public void setParameters(Bundle params) {} +} diff --git a/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/FramerateBitrateAdjusterTest.java b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/FramerateBitrateAdjusterTest.java new file mode 100644 index 0000000000..5fcf9c87d6 --- /dev/null +++ b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/FramerateBitrateAdjusterTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2021 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 androidx.test.runner.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; +import org.webrtc.VideoEncoder.ScalingSettings; + +@RunWith(AndroidJUnit4.class) +@Config(manifest = Config.NONE) +public class FramerateBitrateAdjusterTest { + @Test + public void getAdjustedFramerate_alwaysReturnsDefault() { + FramerateBitrateAdjuster bitrateAdjuster = new FramerateBitrateAdjuster(); + bitrateAdjuster.setTargets(1000, 15); + assertThat(bitrateAdjuster.getAdjustedFramerateFps()).isEqualTo(30.0); + } + + @Test + public void getAdjustedBitrate_defaultFramerate_returnsTargetBitrate() { + FramerateBitrateAdjuster bitrateAdjuster = new FramerateBitrateAdjuster(); + bitrateAdjuster.setTargets(1000, 30); + assertThat(bitrateAdjuster.getAdjustedBitrateBps()).isEqualTo(1000); + } + + @Test + public void getAdjustedBitrate_nonDefaultFramerate_returnsAdjustedBitrate() { + FramerateBitrateAdjuster bitrateAdjuster = new FramerateBitrateAdjuster(); + bitrateAdjuster.setTargets(1000, 7.5); + // Target frame frame is x4 times smaller than the adjusted one (30fps). Adjusted bitrate should + // be x4 times larger then the target one. + assertThat(bitrateAdjuster.getAdjustedBitrateBps()).isEqualTo(4000); + } +} diff --git a/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/GlGenericDrawerTest.java b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/GlGenericDrawerTest.java new file mode 100644 index 0000000000..fdb8f4bf08 --- /dev/null +++ b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/GlGenericDrawerTest.java @@ -0,0 +1,160 @@ +/* + * 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 org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import androidx.test.runner.AndroidJUnit4; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; +import org.webrtc.GlShader; + +@RunWith(AndroidJUnit4.class) +@Config(manifest = Config.NONE) +public class GlGenericDrawerTest { + // Simplest possible valid generic fragment shader. + private static final String FRAGMENT_SHADER = "void main() {\n" + + " gl_FragColor = sample(tc);\n" + + "}\n"; + private static final int TEXTURE_ID = 3; + private static final float[] TEX_MATRIX = + new float[] {1, 2, 3, 4, -1, -2, -3, -4, 0, 0, 1, 0, 0, 0, 0, 1}; + private static final int FRAME_WIDTH = 640; + private static final int FRAME_HEIGHT = 480; + private static final int VIEWPORT_X = 3; + private static final int VIEWPORT_Y = 5; + private static final int VIEWPORT_WIDTH = 500; + private static final int VIEWPORT_HEIGHT = 500; + + // Replace OpenGLES GlShader dependency with a mock. + private class GlGenericDrawerForTest extends GlGenericDrawer { + public GlGenericDrawerForTest(String genericFragmentSource, ShaderCallbacks shaderCallbacks) { + super(genericFragmentSource, shaderCallbacks); + } + + @Override + GlShader createShader(ShaderType shaderType) { + return mockedShader; + } + } + + private GlShader mockedShader; + private GlGenericDrawer glGenericDrawer; + private GlGenericDrawer.ShaderCallbacks mockedCallbacks; + + @Before + public void setUp() { + mockedShader = mock(GlShader.class); + mockedCallbacks = mock(GlGenericDrawer.ShaderCallbacks.class); + glGenericDrawer = new GlGenericDrawerForTest(FRAGMENT_SHADER, mockedCallbacks); + } + + @After + public void tearDown() { + verifyNoMoreInteractions(mockedCallbacks); + } + + @Test + public void testOesFragmentShader() { + final String expectedOesFragmentShader = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "varying vec2 tc;\n" + + "uniform samplerExternalOES tex;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(tex, tc);\n" + + "}\n"; + final String oesFragmentShader = + GlGenericDrawer.createFragmentShaderString(FRAGMENT_SHADER, GlGenericDrawer.ShaderType.OES); + assertEquals(expectedOesFragmentShader, oesFragmentShader); + } + + @Test + public void testRgbFragmentShader() { + final String expectedRgbFragmentShader = "precision mediump float;\n" + + "varying vec2 tc;\n" + + "uniform sampler2D tex;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(tex, tc);\n" + + "}\n"; + final String rgbFragmentShader = + GlGenericDrawer.createFragmentShaderString(FRAGMENT_SHADER, GlGenericDrawer.ShaderType.RGB); + assertEquals(expectedRgbFragmentShader, rgbFragmentShader); + } + + @Test + public void testYuvFragmentShader() { + final String expectedYuvFragmentShader = "precision mediump float;\n" + + "varying vec2 tc;\n" + + "uniform sampler2D y_tex;\n" + + "uniform sampler2D u_tex;\n" + + "uniform sampler2D v_tex;\n" + + "vec4 sample(vec2 p) {\n" + + " float y = texture2D(y_tex, p).r * 1.16438;\n" + + " float u = texture2D(u_tex, p).r;\n" + + " float v = texture2D(v_tex, p).r;\n" + + " return vec4(y + 1.59603 * v - 0.874202,\n" + + " y - 0.391762 * u - 0.812968 * v + 0.531668,\n" + + " y + 2.01723 * u - 1.08563, 1);\n" + + "}\n" + + "void main() {\n" + + " gl_FragColor = sample(tc);\n" + + "}\n"; + final String yuvFragmentShader = + GlGenericDrawer.createFragmentShaderString(FRAGMENT_SHADER, GlGenericDrawer.ShaderType.YUV); + assertEquals(expectedYuvFragmentShader, yuvFragmentShader); + } + + @Test + public void testShaderCallbacksOneRgbFrame() { + glGenericDrawer.drawRgb(TEXTURE_ID, TEX_MATRIX, FRAME_WIDTH, FRAME_HEIGHT, VIEWPORT_X, + VIEWPORT_Y, VIEWPORT_WIDTH, VIEWPORT_HEIGHT); + + verify(mockedCallbacks).onNewShader(mockedShader); + verify(mockedCallbacks) + .onPrepareShader( + mockedShader, TEX_MATRIX, FRAME_WIDTH, FRAME_HEIGHT, VIEWPORT_WIDTH, VIEWPORT_HEIGHT); + } + + @Test + public void testShaderCallbacksTwoRgbFrames() { + glGenericDrawer.drawRgb(TEXTURE_ID, TEX_MATRIX, FRAME_WIDTH, FRAME_HEIGHT, VIEWPORT_X, + VIEWPORT_Y, VIEWPORT_WIDTH, VIEWPORT_HEIGHT); + glGenericDrawer.drawRgb(TEXTURE_ID, TEX_MATRIX, FRAME_WIDTH, FRAME_HEIGHT, VIEWPORT_X, + VIEWPORT_Y, VIEWPORT_WIDTH, VIEWPORT_HEIGHT); + + // Expect only one shader to be created, but two frames to be drawn. + verify(mockedCallbacks, times(1)).onNewShader(mockedShader); + verify(mockedCallbacks, times(2)) + .onPrepareShader( + mockedShader, TEX_MATRIX, FRAME_WIDTH, FRAME_HEIGHT, VIEWPORT_WIDTH, VIEWPORT_HEIGHT); + } + + @Test + public void testShaderCallbacksChangingShaderType() { + glGenericDrawer.drawRgb(TEXTURE_ID, TEX_MATRIX, FRAME_WIDTH, FRAME_HEIGHT, VIEWPORT_X, + VIEWPORT_Y, VIEWPORT_WIDTH, VIEWPORT_HEIGHT); + glGenericDrawer.drawOes(TEXTURE_ID, TEX_MATRIX, FRAME_WIDTH, FRAME_HEIGHT, VIEWPORT_X, + VIEWPORT_Y, VIEWPORT_WIDTH, VIEWPORT_HEIGHT); + + // Expect two shaders to be created, and two frames to be drawn. + verify(mockedCallbacks, times(2)).onNewShader(mockedShader); + verify(mockedCallbacks, times(2)) + .onPrepareShader( + mockedShader, TEX_MATRIX, FRAME_WIDTH, FRAME_HEIGHT, VIEWPORT_WIDTH, VIEWPORT_HEIGHT); + } +} diff --git a/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/HardwareVideoEncoderTest.java b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/HardwareVideoEncoderTest.java new file mode 100644 index 0000000000..bd4a642f00 --- /dev/null +++ b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/HardwareVideoEncoderTest.java @@ -0,0 +1,370 @@ +/* + * 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 java.util.concurrent.TimeUnit.SECONDS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.os.Bundle; +import androidx.test.runner.AndroidJUnit4; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +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.webrtc.EncodedImage; +import org.webrtc.EncodedImage.FrameType; +import org.webrtc.FakeMediaCodecWrapper.State; +import org.webrtc.VideoCodecStatus; +import org.webrtc.VideoEncoder; +import org.webrtc.VideoEncoder.BitrateAllocation; +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(AndroidJUnit4.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, + /* capabilities= */ new VideoEncoder.Capabilities(false /* lossNotification */)); + private static final long POLL_DELAY_MS = 10; + private static final long DELIVER_ENCODED_IMAGE_DELAY_MS = 10; + private static final EncodeInfo ENCODE_INFO_KEY_FRAME = + new EncodeInfo(new FrameType[] {FrameType.VideoFrameKey}); + private static final EncodeInfo ENCODE_INFO_DELTA_FRAME = + new EncodeInfo(new FrameType[] {FrameType.VideoFrameDelta}); + + private static class TestEncoder extends HardwareVideoEncoder { + private final Object deliverEncodedImageLock = new Object(); + private boolean deliverEncodedImageDone = true; + + TestEncoder(MediaCodecWrapperFactory mediaCodecWrapperFactory, String codecName, + VideoCodecMimeType codecType, Integer surfaceColorFormat, Integer yuvColorFormat, + Map<String, String> 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 VideoCodecMimeType codecType = VideoCodecMimeType.VP8; + private BitrateAdjuster bitrateAdjuster = new BaseBitrateAdjuster(); + + public TestEncoderBuilder setCodecType(VideoCodecMimeType codecType) { + this.codecType = codecType; + return this; + } + + public TestEncoderBuilder setBitrateAdjuster(BitrateAdjuster bitrateAdjuster) { + this.bitrateAdjuster = bitrateAdjuster; + 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, + /* sharedContext= */ null); + } + } + + private VideoFrame createTestVideoFrame(long timestampNs) { + 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); + return new VideoFrame(testBuffer, /* rotation= */ 0, timestampNs); + } + + @Mock VideoEncoder.Callback mockEncoderCallback; + private FakeMediaCodecWrapper fakeMediaCodecWrapper; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + MediaFormat inputFormat = new MediaFormat(); + MediaFormat outputFormat = new MediaFormat(); + // TODO(sakal): Add more details to output format as needed. + fakeMediaCodecWrapper = spy(new FakeMediaCodecWrapper(inputFormat, outputFormat)); + } + + @Test + public void testInit() { + // Set-up. + HardwareVideoEncoder encoder = + new TestEncoderBuilder().setCodecType(VideoCodecMimeType.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(VideoCodecMimeType.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<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(), + anyLong(), anyInt()); + ByteBuffer buffer = fakeMediaCodecWrapper.getInputBuffer(indexCaptor.getValue()); + CodecTestHelper.assertEqualContents( + i420, buffer, offsetCaptor.getValue(), sizeCaptor.getValue()); + } + + @Test + public void testDeliversOutputData() throws InterruptedException { + // Set-up. + TestEncoder encoder = new TestEncoderBuilder().build(); + encoder.initEncode(TEST_ENCODER_SETTINGS, mockEncoderCallback); + encoder.encode(createTestVideoFrame(/* timestampNs= */ 42), ENCODE_INFO_KEY_FRAME); + + // Test. + byte[] outputData = CodecTestHelper.generateRandomData(100); + fakeMediaCodecWrapper.addOutputData(outputData, + /* presentationTimestampUs= */ 0, + /* flags= */ MediaCodec.BUFFER_FLAG_SYNC_FRAME); + + encoder.waitDeliverEncodedImage(); + + // Verify. + ArgumentCaptor<EncodedImage> 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.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); + } + + @Test + public void testFramerateWithFramerateBitrateAdjuster() { + // Enable FramerateBitrateAdjuster and initialize encoder with frame rate 15fps. Vefity that our + // initial frame rate setting is ignored and media encoder is initialized with 30fps + // (FramerateBitrateAdjuster default). + HardwareVideoEncoder encoder = + new TestEncoderBuilder().setBitrateAdjuster(new FramerateBitrateAdjuster()).build(); + encoder.initEncode( + new Settings( + /* numberOfCores= */ 1, + /* width= */ 640, + /* height= */ 480, + /* startBitrate= */ 10000, + /* maxFramerate= */ 15, + /* numberOfSimulcastStreams= */ 1, + /* automaticResizeOn= */ true, + /* capabilities= */ new VideoEncoder.Capabilities(false /* lossNotification */)), + mockEncoderCallback); + + MediaFormat mediaFormat = fakeMediaCodecWrapper.getConfiguredFormat(); + assertThat(mediaFormat.getFloat(MediaFormat.KEY_FRAME_RATE)).isEqualTo(30.0f); + } + + @Test + public void testBitrateWithFramerateBitrateAdjuster() throws InterruptedException { + // Enable FramerateBitrateAdjuster and change frame rate while encoding video. Verify that + // bitrate setting passed to media encoder is adjusted to compensate for changes in frame rate. + TestEncoder encoder = + new TestEncoderBuilder().setBitrateAdjuster(new FramerateBitrateAdjuster()).build(); + encoder.initEncode(TEST_ENCODER_SETTINGS, mockEncoderCallback); + + encoder.encode(createTestVideoFrame(/* timestampNs= */ 0), ENCODE_INFO_KEY_FRAME); + + // Reduce frame rate by half. + BitrateAllocation bitrateAllocation = new BitrateAllocation( + /* bitratesBbs= */ new int[][] {new int[] {TEST_ENCODER_SETTINGS.startBitrate}}); + encoder.setRateAllocation(bitrateAllocation, TEST_ENCODER_SETTINGS.maxFramerate / 2); + + // Generate output to trigger bitrate update in encoder wrapper. + fakeMediaCodecWrapper.addOutputData( + CodecTestHelper.generateRandomData(100), /* presentationTimestampUs= */ 0, /* flags= */ 0); + encoder.waitDeliverEncodedImage(); + + // Frame rate has been reduced by half. Verify that bitrate doubled. + ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class); + verify(fakeMediaCodecWrapper, times(2)).setParameters(bundleCaptor.capture()); + Bundle params = bundleCaptor.getAllValues().get(1); + assertThat(params.containsKey(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE)).isTrue(); + assertThat(params.getInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE)) + .isEqualTo(TEST_ENCODER_SETTINGS.startBitrate * 2); + } + + @Test + public void testTimestampsWithFramerateBitrateAdjuster() throws InterruptedException { + // Enable FramerateBitrateAdjuster and change frame rate while encoding video. Verify that + // encoder ignores changes in frame rate and calculates frame timestamps based on fixed frame + // rate 30fps. + TestEncoder encoder = + new TestEncoderBuilder().setBitrateAdjuster(new FramerateBitrateAdjuster()).build(); + encoder.initEncode(TEST_ENCODER_SETTINGS, mockEncoderCallback); + + encoder.encode(createTestVideoFrame(/* timestampNs= */ 0), ENCODE_INFO_KEY_FRAME); + + // Reduce frametate by half. + BitrateAllocation bitrateAllocation = new BitrateAllocation( + /* bitratesBbs= */ new int[][] {new int[] {TEST_ENCODER_SETTINGS.startBitrate}}); + encoder.setRateAllocation(bitrateAllocation, TEST_ENCODER_SETTINGS.maxFramerate / 2); + + // Encoder is allowed to buffer up to 2 frames. Generate output to avoid frame dropping. + fakeMediaCodecWrapper.addOutputData( + CodecTestHelper.generateRandomData(100), /* presentationTimestampUs= */ 0, /* flags= */ 0); + encoder.waitDeliverEncodedImage(); + + encoder.encode(createTestVideoFrame(/* timestampNs= */ 1), ENCODE_INFO_DELTA_FRAME); + encoder.encode(createTestVideoFrame(/* timestampNs= */ 2), ENCODE_INFO_DELTA_FRAME); + + ArgumentCaptor<Long> timestampCaptor = ArgumentCaptor.forClass(Long.class); + verify(fakeMediaCodecWrapper, times(3)) + .queueInputBuffer( + /* index= */ anyInt(), + /* offset= */ anyInt(), + /* size= */ anyInt(), timestampCaptor.capture(), + /* flags= */ anyInt()); + + long frameDurationMs = SECONDS.toMicros(1) / 30; + assertThat(timestampCaptor.getAllValues()) + .containsExactly(0L, frameDurationMs, 2 * frameDurationMs); + } +} diff --git a/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/IceCandidateTest.java b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/IceCandidateTest.java new file mode 100644 index 0000000000..437e146415 --- /dev/null +++ b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/IceCandidateTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019 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 androidx.test.runner.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; +import org.webrtc.IceCandidate; + +@RunWith(AndroidJUnit4.class) +@Config(manifest = Config.NONE) +public class IceCandidateTest { + @Test + public void testIceCandidateEquals() { + IceCandidate c1 = new IceCandidate( + "audio", 0, "candidate:1532086002 1 udp 2122194687 192.168.86.144 37138 typ host"); + IceCandidate c2 = new IceCandidate( + "audio", 0, "candidate:1532086002 1 udp 2122194687 192.168.86.144 37138 typ host"); + + // c3 differ by sdpMid + IceCandidate c3 = new IceCandidate( + "video", 0, "candidate:1532086002 1 udp 2122194687 192.168.86.144 37138 typ host"); + // c4 differ by sdpMLineIndex + IceCandidate c4 = new IceCandidate( + "audio", 1, "candidate:1532086002 1 udp 2122194687 192.168.86.144 37138 typ host"); + // c5 differ by sdp. + IceCandidate c5 = new IceCandidate( + "audio", 0, "candidate:1532086002 1 udp 2122194687 192.168.86.144 37139 typ host"); + + assertThat(c1.equals(c2)).isTrue(); + assertThat(c2.equals(c1)).isTrue(); + assertThat(c1.equals(null)).isFalse(); + assertThat(c1.equals(c3)).isFalse(); + assertThat(c1.equals(c4)).isFalse(); + assertThat(c5.equals(c1)).isFalse(); + + Object o2 = c2; + assertThat(c1.equals(o2)).isTrue(); + assertThat(o2.equals(c1)).isTrue(); + } +}
\ No newline at end of file diff --git a/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/RefCountDelegateTest.java b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/RefCountDelegateTest.java new file mode 100644 index 0000000000..eafd722a17 --- /dev/null +++ b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/RefCountDelegateTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 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.Mockito.never; +import static org.mockito.Mockito.verify; + +import androidx.test.runner.AndroidJUnit4; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; + +@RunWith(AndroidJUnit4.class) +@Config(manifest = Config.NONE) +public class RefCountDelegateTest { + @Mock Runnable mockReleaseCallback; + private RefCountDelegate refCountDelegate; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + refCountDelegate = new RefCountDelegate(mockReleaseCallback); + } + + @Test + public void testReleaseRunsReleaseCallback() { + refCountDelegate.release(); + verify(mockReleaseCallback).run(); + } + + @Test + public void testRetainIncreasesRefCount() { + refCountDelegate.retain(); + + refCountDelegate.release(); + verify(mockReleaseCallback, never()).run(); + + refCountDelegate.release(); + verify(mockReleaseCallback).run(); + } + + @Test(expected = IllegalStateException.class) + public void testReleaseAfterFreeThrowsIllegalStateException() { + refCountDelegate.release(); + refCountDelegate.release(); + } + + @Test(expected = IllegalStateException.class) + public void testRetainAfterFreeThrowsIllegalStateException() { + refCountDelegate.release(); + refCountDelegate.retain(); + } + + @Test + public void testSafeRetainBeforeFreeReturnsTrueAndIncreasesRefCount() { + assertThat(refCountDelegate.safeRetain()).isTrue(); + + refCountDelegate.release(); + verify(mockReleaseCallback, never()).run(); + + refCountDelegate.release(); + verify(mockReleaseCallback).run(); + } + + @Test + public void testSafeRetainAfterFreeReturnsFalse() { + refCountDelegate.release(); + assertThat(refCountDelegate.safeRetain()).isFalse(); + } +} diff --git a/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/ScalingSettingsTest.java b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/ScalingSettingsTest.java new file mode 100644 index 0000000000..18db61d2dc --- /dev/null +++ b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/ScalingSettingsTest.java @@ -0,0 +1,29 @@ +/* + * 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 org.junit.Assert.assertEquals; + +import androidx.test.runner.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; +import org.webrtc.VideoEncoder.ScalingSettings; + +@RunWith(AndroidJUnit4.class) +@Config(manifest = Config.NONE) +public class ScalingSettingsTest { + @Test + public void testToString() { + assertEquals("[ 1, 2 ]", new ScalingSettings(1, 2).toString()); + assertEquals("OFF", ScalingSettings.OFF.toString()); + } +} diff --git a/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/audio/LowLatencyAudioBufferManagerTest.java b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/audio/LowLatencyAudioBufferManagerTest.java new file mode 100644 index 0000000000..2575cea60e --- /dev/null +++ b/third_party/libwebrtc/sdk/android/tests/src/org/webrtc/audio/LowLatencyAudioBufferManagerTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2020 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.audio; + +import static org.junit.Assert.assertTrue; +import static org.mockito.AdditionalMatchers.gt; +import static org.mockito.AdditionalMatchers.lt; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.media.AudioTrack; +import android.os.Build; +import androidx.test.runner.AndroidJUnit4; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; +import org.webrtc.audio.LowLatencyAudioBufferManager; + +/** + * Tests for LowLatencyAudioBufferManager. + */ +@RunWith(AndroidJUnit4.class) +@Config(manifest = Config.NONE, sdk = Build.VERSION_CODES.O) +public class LowLatencyAudioBufferManagerTest { + @Mock private AudioTrack mockAudioTrack; + private LowLatencyAudioBufferManager bufferManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + bufferManager = new LowLatencyAudioBufferManager(); + } + + @Test + public void testBufferSizeDecrease() { + when(mockAudioTrack.getUnderrunCount()).thenReturn(0); + when(mockAudioTrack.getBufferSizeInFrames()).thenReturn(100); + when(mockAudioTrack.getPlaybackRate()).thenReturn(1000); + for (int i = 0; i < 9; i++) { + bufferManager.maybeAdjustBufferSize(mockAudioTrack); + } + // Check that the buffer size was not changed yet. + verify(mockAudioTrack, times(0)).setBufferSizeInFrames(anyInt()); + // After the 10th call without underruns, we expect the buffer size to decrease. + bufferManager.maybeAdjustBufferSize(mockAudioTrack); + // The expected size is 10ms below the existing size, which works out to 100 - (1000 / 100) + // = 90. + verify(mockAudioTrack, times(1)).setBufferSizeInFrames(90); + } + + @Test + public void testBufferSizeNeverBelow10ms() { + when(mockAudioTrack.getUnderrunCount()).thenReturn(0); + when(mockAudioTrack.getBufferSizeInFrames()).thenReturn(11); + when(mockAudioTrack.getPlaybackRate()).thenReturn(1000); + for (int i = 0; i < 10; i++) { + bufferManager.maybeAdjustBufferSize(mockAudioTrack); + } + // Check that the buffer size was not set to a value below 10 ms. + verify(mockAudioTrack, times(0)).setBufferSizeInFrames(lt(10)); + } + + @Test + public void testUnderrunBehavior() { + when(mockAudioTrack.getUnderrunCount()).thenReturn(1); + when(mockAudioTrack.getBufferSizeInFrames()).thenReturn(100); + when(mockAudioTrack.getPlaybackRate()).thenReturn(1000); + bufferManager.maybeAdjustBufferSize(mockAudioTrack); + // Check that the buffer size was increased after the underrrun. + verify(mockAudioTrack, times(1)).setBufferSizeInFrames(gt(100)); + when(mockAudioTrack.getUnderrunCount()).thenReturn(0); + for (int i = 0; i < 10; i++) { + bufferManager.maybeAdjustBufferSize(mockAudioTrack); + } + // Check that the buffer size was not changed again, even though there were no underruns for + // 10 calls. + verify(mockAudioTrack, times(1)).setBufferSizeInFrames(anyInt()); + } + + @Test + public void testBufferIncrease() { + when(mockAudioTrack.getBufferSizeInFrames()).thenReturn(100); + when(mockAudioTrack.getPlaybackRate()).thenReturn(1000); + for (int i = 1; i < 30; i++) { + when(mockAudioTrack.getUnderrunCount()).thenReturn(i); + bufferManager.maybeAdjustBufferSize(mockAudioTrack); + } + // Check that the buffer size was not increased more than 5 times. + verify(mockAudioTrack, times(5)).setBufferSizeInFrames(gt(100)); + } +} |