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:
parent
06385ce83b
commit
d72686c867
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user