| /* | |
| * 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 android.os.Build.VERSION; | |
| import android.os.Build.VERSION_CODES; | |
| import android.os.Handler; | |
| import android.os.Looper; | |
| import android.os.Trace; | |
| import android.view.Choreographer; | |
| import androidx.annotation.GuardedBy; | |
| import java.util.List; | |
| import java.util.concurrent.CopyOnWriteArrayList; | |
| import java.util.concurrent.TimeUnit; | |
| /** | |
| * Class to synchronize rendering updates with display refresh cycles and save power by blocking | |
| * updates that exceeds the target frame rate. | |
| */ | |
| public final class RenderSynchronizer { | |
| /** Interface for listening to render window updates. */ | |
| public interface Listener { | |
| void onRenderWindowOpen(); | |
| void onRenderWindowClose(); | |
| } | |
| private static final String TAG = "RenderSynchronizer"; | |
| private static final float DEFAULT_TARGET_FPS = 30f; | |
| private final Object lock = new Object(); | |
| private final List<Listener> listeners = new CopyOnWriteArrayList<>(); | |
| private final long targetFrameIntervalNanos; | |
| private final Handler mainThreadHandler; | |
| private Choreographer choreographer; | |
| @GuardedBy("lock") | |
| private boolean isListening; | |
| private boolean renderWindowOpen; | |
| private long lastRefreshTimeNanos; | |
| private long lastOpenedTimeNanos; | |
| public RenderSynchronizer(float targetFrameRateFps) { | |
| this.targetFrameIntervalNanos = Math.round(TimeUnit.SECONDS.toNanos(1) / targetFrameRateFps); | |
| this.mainThreadHandler = new Handler(Looper.getMainLooper()); | |
| mainThreadHandler.post(() -> this.choreographer = Choreographer.getInstance()); | |
| Logging.d(TAG, "Created"); | |
| } | |
| public RenderSynchronizer() { | |
| this(DEFAULT_TARGET_FPS); | |
| } | |
| public void registerListener(Listener listener) { | |
| listeners.add(listener); | |
| synchronized (lock) { | |
| if (!isListening) { | |
| Logging.d(TAG, "First listener, subscribing to frame callbacks"); | |
| isListening = true; | |
| mainThreadHandler.post( | |
| () -> choreographer.postFrameCallback(this::onDisplayRefreshCycleBegin)); | |
| } | |
| } | |
| } | |
| public void removeListener(Listener listener) { | |
| listeners.remove(listener); | |
| } | |
| private void onDisplayRefreshCycleBegin(long refreshTimeNanos) { | |
| synchronized (lock) { | |
| if (listeners.isEmpty()) { | |
| Logging.d(TAG, "No listeners, unsubscribing to frame callbacks"); | |
| isListening = false; | |
| return; | |
| } | |
| } | |
| choreographer.postFrameCallback(this::onDisplayRefreshCycleBegin); | |
| long lastOpenDeltaNanos = refreshTimeNanos - lastOpenedTimeNanos; | |
| long refreshDeltaNanos = refreshTimeNanos - lastRefreshTimeNanos; | |
| lastRefreshTimeNanos = refreshTimeNanos; | |
| // Make a greedy choice whether to open (or keep open) the render window. If the current time | |
| // since the render window was last opened is closer to the target than what we predict it would | |
| // be in the next refresh cycle then we open the window. | |
| if (Math.abs(lastOpenDeltaNanos - targetFrameIntervalNanos) | |
| < Math.abs(lastOpenDeltaNanos - targetFrameIntervalNanos + refreshDeltaNanos)) { | |
| lastOpenedTimeNanos = refreshTimeNanos; | |
| openRenderWindow(); | |
| } else if (renderWindowOpen) { | |
| closeRenderWindow(); | |
| } | |
| } | |
| private void traceRenderWindowChange() { | |
| if (VERSION.SDK_INT >= VERSION_CODES.Q) { | |
| Trace.setCounter("RenderWindow", renderWindowOpen ? 1 : 0); | |
| } | |
| } | |
| private void openRenderWindow() { | |
| renderWindowOpen = true; | |
| traceRenderWindowChange(); | |
| for (Listener listener : listeners) { | |
| listener.onRenderWindowOpen(); | |
| } | |
| } | |
| private void closeRenderWindow() { | |
| renderWindowOpen = false; | |
| traceRenderWindowChange(); | |
| for (Listener listener : listeners) { | |
| listener.onRenderWindowClose(); | |
| } | |
| } | |
| } |