Extract display size monitor

Detecting display size changes is not straightforward:
 - from a DisplayListener, "display changed" events are received, but
   this does not imply that the size has changed (it must be checked);
 - on Android 14 (see e26bdb07a21493d096ea5c8cfd870fc5a3f015dc),
   "display changed" events are not received on some versions, so as a
   fallback, a RotationWatcher and a DisplayFoldListener are registered,
   but unregistered as soon as a "display changed" event is actually
   received, which means that the problem is fixed.

Extract a "display size monitor" to share the code between screen
capture and virtual display capture.

PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
This commit is contained in:
Romain Vimont 2024-11-16 21:47:07 +01:00
parent 06385ce83b
commit d72686c867
2 changed files with 193 additions and 142 deletions

View File

@ -0,0 +1,189 @@
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.view.IDisplayFoldListener;
import android.view.IRotationWatcher;
public class DisplaySizeMonitor {
public interface Listener {
void onDisplaySizeChanged();
}
private DisplayManager.DisplayListenerHandle displayListenerHandle;
private HandlerThread handlerThread;
// On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really
// detect it directly, so register a RotationWatcher and a DisplayFoldListener as a fallback, until we receive the first event from
// DisplayListener (which proves that it works).
private boolean displayListenerWorks; // only accessed from the display listener thread
private IRotationWatcher rotationWatcher;
private IDisplayFoldListener displayFoldListener;
private int displayId = Device.DISPLAY_ID_NONE;
private Size sessionDisplaySize;
private Listener listener;
public void start(int displayId, Listener listener) {
// Once started, the listener and the displayId must never change
assert listener != null;
this.listener = listener;
assert this.displayId == Device.DISPLAY_ID_NONE;
this.displayId = displayId;
handlerThread = new HandlerThread("DisplayListener");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
registerDisplayListenerFallbacks();
}
displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(eventDisplayId -> {
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("DisplaySizeMonitor: onDisplayChanged(" + eventDisplayId + ")");
}
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
if (!displayListenerWorks) {
// On the first display listener event, we know it works, we can unregister the fallbacks
displayListenerWorks = true;
unregisterDisplayListenerFallbacks();
}
}
if (eventDisplayId == displayId) {
checkDisplaySizeChanged();
}
}, handler);
}
/**
* Stop and release the monitor.
* <p/>
* It must not be used anymore.
* It is ok to call this method even if {@link #start(int, Listener)} was not called.
*/
public void stopAndRelease() {
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
unregisterDisplayListenerFallbacks();
}
// displayListenerHandle may be null if registration failed
if (displayListenerHandle != null) {
ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle);
displayListenerHandle = null;
}
if (handlerThread != null) {
handlerThread.quitSafely();
}
}
private synchronized Size getSessionDisplaySize() {
return sessionDisplaySize;
}
public synchronized void setSessionDisplaySize(Size sessionDisplaySize) {
this.sessionDisplaySize = sessionDisplaySize;
}
private void checkDisplaySizeChanged() {
DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (di == null) {
Ln.w("DisplayInfo for " + displayId + " cannot be retrieved");
// We can't compare with the current size, so reset unconditionally
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("DisplaySizeMonitor: requestReset(): " + getSessionDisplaySize() + " -> (unknown)");
}
setSessionDisplaySize(null);
listener.onDisplaySizeChanged();
} else {
Size size = di.getSize();
// The field is hidden on purpose, to read it with synchronization
@SuppressWarnings("checkstyle:HiddenField")
Size sessionDisplaySize = getSessionDisplaySize(); // synchronized
// .equals() also works if sessionDisplaySize == null
if (!size.equals(sessionDisplaySize)) {
// Reset only if the size is different
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("DisplaySizeMonitor: requestReset(): " + sessionDisplaySize + " -> " + size);
}
// Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare()
// considers that the current size is the requested size (to avoid a duplicate requestReset())
setSessionDisplaySize(size);
listener.onDisplaySizeChanged();
} else if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("DisplaySizeMonitor: Size not changed (" + size + "): do not requestReset()");
}
}
}
private void registerDisplayListenerFallbacks() {
rotationWatcher = new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) {
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("DisplaySizeMonitor: onRotationChanged(" + rotation + ")");
}
checkDisplaySizeChanged();
}
};
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
// Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14)
displayFoldListener = new IDisplayFoldListener.Stub() {
private boolean first = true;
@Override
public void onDisplayFoldChanged(int displayId, boolean folded) {
if (first) {
// An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding.
first = false;
return;
}
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("DisplaySizeMonitor: onDisplayFoldChanged(" + displayId + ", " + folded + ")");
}
if (DisplaySizeMonitor.this.displayId != displayId) {
// Ignore events related to other display ids
return;
}
checkDisplaySizeChanged();
}
};
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
}
private synchronized void unregisterDisplayListenerFallbacks() {
if (rotationWatcher != null) {
ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher);
rotationWatcher = null;
}
if (displayFoldListener != null) {
// Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14)
ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener);
displayFoldListener = null;
}
}
}

View File

@ -13,18 +13,13 @@ import com.genymobile.scrcpy.opengl.OpenGLRunner;
import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.AffineMatrix;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect; import android.graphics.Rect;
import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplay;
import android.os.Build; import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder; import android.os.IBinder;
import android.view.IDisplayFoldListener;
import android.view.IRotationWatcher;
import android.view.Surface; import android.view.Surface;
import java.io.IOException; import java.io.IOException;
@ -40,8 +35,7 @@ public class ScreenCapture extends SurfaceCapture {
private DisplayInfo displayInfo; private DisplayInfo displayInfo;
private Size videoSize; private Size videoSize;
// Source display size (before resizing/crop) for the current session private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor();
private Size sessionDisplaySize;
private IBinder display; private IBinder display;
private VirtualDisplay virtualDisplay; private VirtualDisplay virtualDisplay;
@ -49,16 +43,6 @@ public class ScreenCapture extends SurfaceCapture {
private AffineMatrix transform; private AffineMatrix transform;
private OpenGLRunner glRunner; private OpenGLRunner glRunner;
private DisplayManager.DisplayListenerHandle displayListenerHandle;
private HandlerThread handlerThread;
// On Android 14, the DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really
// detect it directly, so register a RotationWatcher and a DisplayFoldListener as a fallback, until we receive the first event from
// DisplayListener (which proves that it works).
private boolean displayListenerWorks; // only accessed from the display listener thread
private IRotationWatcher rotationWatcher;
private IDisplayFoldListener displayFoldListener;
public ScreenCapture(VirtualDisplayListener vdListener, Options options) { public ScreenCapture(VirtualDisplayListener vdListener, Options options) {
this.vdListener = vdListener; this.vdListener = vdListener;
this.displayId = options.getDisplayId(); this.displayId = options.getDisplayId();
@ -70,57 +54,7 @@ public class ScreenCapture extends SurfaceCapture {
@Override @Override
public void init() { public void init() {
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { displaySizeMonitor.start(displayId, this::invalidate);
registerDisplayListenerFallbacks();
}
handlerThread = new HandlerThread("DisplayListener");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(displayId -> {
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: onDisplayChanged(" + displayId + ")");
}
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
if (!displayListenerWorks) {
// On the first display listener event, we know it works, we can unregister the fallbacks
displayListenerWorks = true;
unregisterDisplayListenerFallbacks();
}
}
if (this.displayId == displayId) {
DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (di == null) {
Ln.w("DisplayInfo for " + displayId + " cannot be retrieved");
// We can't compare with the current size, so reset unconditionally
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: requestReset(): " + getSessionDisplaySize() + " -> (unknown)");
}
setSessionDisplaySize(null);
invalidate();
} else {
Size size = di.getSize();
// The field is hidden on purpose, to read it with synchronization
@SuppressWarnings("checkstyle:HiddenField")
Size sessionDisplaySize = getSessionDisplaySize(); // synchronized
// .equals() also works if sessionDisplaySize == null
if (!size.equals(sessionDisplaySize)) {
// Reset only if the size is different
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: requestReset(): " + sessionDisplaySize + " -> " + size);
}
// Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare()
// considers that the current size is the requested size (to avoid a duplicate requestReset())
setSessionDisplaySize(size);
invalidate();
} else if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: Size not changed (" + size + "): do not requestReset()");
}
}
}
}, handler);
} }
@Override @Override
@ -136,7 +70,7 @@ public class ScreenCapture extends SurfaceCapture {
} }
Size displaySize = displayInfo.getSize(); Size displaySize = displayInfo.getSize();
setSessionDisplaySize(displaySize); displaySizeMonitor.setSessionDisplaySize(displaySize);
if (lockVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { if (lockVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) {
// The user requested to lock the video orientation to the current orientation // The user requested to lock the video orientation to the current orientation
@ -226,18 +160,7 @@ public class ScreenCapture extends SurfaceCapture {
@Override @Override
public void release() { public void release() {
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { displaySizeMonitor.stopAndRelease();
unregisterDisplayListenerFallbacks();
}
handlerThread.quitSafely();
handlerThread = null;
// displayListenerHandle may be null if registration failed
if (displayListenerHandle != null) {
ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle);
displayListenerHandle = null;
}
if (display != null) { if (display != null) {
SurfaceControl.destroyDisplay(display); SurfaceControl.destroyDisplay(display);
@ -279,67 +202,6 @@ public class ScreenCapture extends SurfaceCapture {
} }
} }
private synchronized Size getSessionDisplaySize() {
return sessionDisplaySize;
}
private synchronized void setSessionDisplaySize(Size sessionDisplaySize) {
this.sessionDisplaySize = sessionDisplaySize;
}
private void registerDisplayListenerFallbacks() {
rotationWatcher = new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) {
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")");
}
invalidate();
}
};
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
// Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14)
displayFoldListener = new IDisplayFoldListener.Stub() {
private boolean first = true;
@Override
public void onDisplayFoldChanged(int displayId, boolean folded) {
if (first) {
// An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding.
first = false;
return;
}
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: onDisplayFoldChanged(" + displayId + ", " + folded + ")");
}
if (ScreenCapture.this.displayId != displayId) {
// Ignore events related to other display ids
return;
}
invalidate();
}
};
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
}
private void unregisterDisplayListenerFallbacks() {
synchronized (this) {
if (rotationWatcher != null) {
ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher);
rotationWatcher = null;
}
if (displayFoldListener != null) {
// Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14)
ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener);
displayFoldListener = null;
}
}
}
@Override @Override
public void requestInvalidate() { public void requestInvalidate() {
invalidate(); invalidate();