blob: c89f798c9c149cb175927366acb7e0b4bc70e338 [file] [log] [blame]
/*
* 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();
}
}
}