diff --git a/sdk/android/BUILD.gn b/sdk/android/BUILD.gn index 7a3b8c1e56..b050933aac 100644 --- a/sdk/android/BUILD.gn +++ b/sdk/android/BUILD.gn @@ -1720,6 +1720,7 @@ if (is_android) { "tests/src/org/webrtc/HardwareVideoEncoderTest.java", "tests/src/org/webrtc/IceCandidateTest.java", "tests/src/org/webrtc/RefCountDelegateTest.java", + "tests/src/org/webrtc/RenderSynchronizerTest.java", "tests/src/org/webrtc/ScalingSettingsTest.java", "tests/src/org/webrtc/audio/LowLatencyAudioBufferManagerTest.java", ] diff --git a/sdk/android/tests/src/org/webrtc/RenderSynchronizerTest.java b/sdk/android/tests/src/org/webrtc/RenderSynchronizerTest.java new file mode 100644 index 0000000000..3292f72c2d --- /dev/null +++ b/sdk/android/tests/src/org/webrtc/RenderSynchronizerTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2023 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 java.lang.Math.ceil; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import androidx.test.runner.AndroidJUnit4; +import java.time.Duration; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLooper; +import org.robolectric.shadows.ShadowChoreographer; + +@RunWith(AndroidJUnit4.class) +@Config(manifest = Config.NONE) +public class RenderSynchronizerTest { + + private static final Duration FRAME_DELAY = Duration.ofMillis(16); + private static final float DEFAULT_FRAME_RATE = 30f; + + @Mock private RenderSynchronizer.Listener mockListener; + private RenderSynchronizer renderSynchronizer; + private Duration refreshDelay; + private float frameRate; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + ShadowChoreographer.setPaused(true); + } + + private void init(float displayRefreshRate, float renderFrameRate) { + refreshDelay = Duration.ofMillis((long)ceil(1000 / displayRefreshRate)); + frameRate = renderFrameRate; + + ShadowChoreographer.setFrameDelay(refreshDelay); + renderSynchronizer = new RenderSynchronizer(renderFrameRate); + renderSynchronizer.registerListener(mockListener); + } + + @Test + public void renderWindowOpensOnFirstFrame() { + init(/* displayRefreshRate= */60f, /* renderFrameRate= */30f); + + advanceOneRefreshCycle(); + verify(mockListener).onRenderWindowOpen(); + } + + @Test + public void doubleDisplayRefreshRate() { + init(/* displayRefreshRate= */60f, /* renderFrameRate= */30f); + + InOrder inOrder = inOrder(mockListener); + for (int i = 0; i < 10; ++i) { + advanceOneRefreshCycle(); + inOrder.verify(mockListener).onRenderWindowOpen(); + + advanceOneRefreshCycle(); + inOrder.verify(mockListener).onRenderWindowClose(); + } + } + + @Test + public void trippleRefreshRate() { + init(/* displayRefreshRate= */90f, /* renderFrameRate= */30f); + InOrder inOrder = inOrder(mockListener); + for (int i = 0; i < 10; ++i) { + advanceOneRefreshCycle(); + inOrder.verify(mockListener).onRenderWindowOpen(); + + advanceOneRefreshCycle(); + inOrder.verify(mockListener).onRenderWindowClose(); + + advanceOneRefreshCycle(); + // No action expected, window stays closed/ + } + } + + @Test + public void equalRefreshRate() { + init(/* displayRefreshRate= */30f, /* renderFrameRate= */30f); + for (int i = 0; i < 10; ++i) { + advanceOneRefreshCycle(); + } + verify(mockListener, times(10)).onRenderWindowOpen(); + verify(mockListener, times(0)).onRenderWindowClose(); + } + + @Test + public void halfRefreshRate() { + init(/* displayRefreshRate= */30f, /* renderFrameRate= */60f); + for (int i = 0; i < 10; ++i) { + advanceOneRefreshCycle(); + } + verify(mockListener, times(10)).onRenderWindowOpen(); + verify(mockListener, times(0)).onRenderWindowClose(); + } + + private void advanceOneRefreshCycle() { + advanceBy(refreshDelay); + } + + private void advanceBy(Duration duration) { + ShadowLooper.idleMainLooper(duration.toMillis(), MILLISECONDS); + } +}