/* * 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); } }