mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-12-17 05:24:19 +01:00
Add virtual display feature
Add a feature to create a new (separate) virtual display instead of
mirroring the device screen:
scrcpy --new-display=1920x1080
scrcpy --new-display=1920x1080/420 # force 420 dpi
scrcpy --new-display # use the main display size and density
scrcpy --new-display -m1920 # scaled to fit a max size of 1920
scrcpy --new-display=/240 # use the main display size and 240 dpi
Fixes #1887 <https://github.com/Genymobile/scrcpy/issues/1887>
PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
Co-authored-by: anirudhb <anirudhb@users.noreply.github.com>
This commit is contained in:
@@ -139,8 +139,10 @@ public final class CleanUp {
|
||||
|
||||
if (Device.isScreenOn()) {
|
||||
if (powerOffScreen) {
|
||||
Ln.i("Power off screen");
|
||||
Device.powerOffScreen(displayId);
|
||||
if (displayId != Device.DISPLAY_ID_NONE) {
|
||||
Ln.i("Power off screen");
|
||||
Device.powerOffScreen(displayId);
|
||||
}
|
||||
} else if (restoreNormalPowerMode) {
|
||||
Ln.i("Restoring normal power mode");
|
||||
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.audio.AudioCodec;
|
||||
import com.genymobile.scrcpy.audio.AudioSource;
|
||||
import com.genymobile.scrcpy.device.NewDisplay;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.CodecOption;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
@@ -54,6 +55,8 @@ public class Options {
|
||||
private boolean cleanup = true;
|
||||
private boolean powerOn = true;
|
||||
|
||||
private NewDisplay newDisplay;
|
||||
|
||||
private boolean listEncoders;
|
||||
private boolean listDisplays;
|
||||
private boolean listCameras;
|
||||
@@ -205,6 +208,10 @@ public class Options {
|
||||
return powerOn;
|
||||
}
|
||||
|
||||
public NewDisplay getNewDisplay() {
|
||||
return newDisplay;
|
||||
}
|
||||
|
||||
public boolean getList() {
|
||||
return listEncoders || listDisplays || listCameras || listCameraSizes;
|
||||
}
|
||||
@@ -418,6 +425,9 @@ public class Options {
|
||||
case "camera_high_speed":
|
||||
options.cameraHighSpeed = Boolean.parseBoolean(value);
|
||||
break;
|
||||
case "new_display":
|
||||
options.newDisplay = parseNewDisplay(value);
|
||||
break;
|
||||
case "send_device_meta":
|
||||
options.sendDeviceMeta = Boolean.parseBoolean(value);
|
||||
break;
|
||||
@@ -504,4 +514,36 @@ public class Options {
|
||||
throw new IllegalArgumentException("Invalid float value for " + key + ": \"" + value + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
private static NewDisplay parseNewDisplay(String newDisplay) {
|
||||
// Possible inputs:
|
||||
// - "" (empty string)
|
||||
// - "<width>x<height>/<dpi>"
|
||||
// - "<width>x<height>"
|
||||
// - "/<dpi>"
|
||||
if (newDisplay.isEmpty()) {
|
||||
return new NewDisplay();
|
||||
}
|
||||
|
||||
String[] tokens = newDisplay.split("/");
|
||||
|
||||
Size size;
|
||||
if (!tokens[0].isEmpty()) {
|
||||
size = parseSize(tokens[0]);
|
||||
} else {
|
||||
size = null;
|
||||
}
|
||||
|
||||
int dpi;
|
||||
if (tokens.length >= 2) {
|
||||
dpi = Integer.parseInt(tokens[1]);
|
||||
if (dpi <= 0) {
|
||||
throw new IllegalArgumentException("Invalid non-positive dpi: " + tokens[1]);
|
||||
}
|
||||
} else {
|
||||
dpi = 0;
|
||||
}
|
||||
|
||||
return new NewDisplay(size, dpi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,14 @@ import com.genymobile.scrcpy.control.Controller;
|
||||
import com.genymobile.scrcpy.device.ConfigurationException;
|
||||
import com.genymobile.scrcpy.device.DesktopConnection;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.NewDisplay;
|
||||
import com.genymobile.scrcpy.device.Streamer;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
import com.genymobile.scrcpy.util.Settings;
|
||||
import com.genymobile.scrcpy.util.SettingsException;
|
||||
import com.genymobile.scrcpy.video.CameraCapture;
|
||||
import com.genymobile.scrcpy.video.NewDisplayCapture;
|
||||
import com.genymobile.scrcpy.video.ScreenCapture;
|
||||
import com.genymobile.scrcpy.video.SurfaceCapture;
|
||||
import com.genymobile.scrcpy.video.SurfaceEncoder;
|
||||
@@ -128,8 +130,11 @@ public final class Server {
|
||||
CleanUp cleanUp = null;
|
||||
Thread initThread = null;
|
||||
|
||||
NewDisplay newDisplay = options.getNewDisplay();
|
||||
int displayId = newDisplay == null ? options.getDisplayId() : Device.DISPLAY_ID_NONE;
|
||||
|
||||
if (options.getCleanup()) {
|
||||
cleanUp = CleanUp.configure(options.getDisplayId());
|
||||
cleanUp = CleanUp.configure(displayId);
|
||||
initThread = startInitThread(options, cleanUp);
|
||||
}
|
||||
|
||||
@@ -154,7 +159,7 @@ public final class Server {
|
||||
|
||||
if (control) {
|
||||
ControlChannel controlChannel = connection.getControlChannel();
|
||||
controller = new Controller(options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn());
|
||||
controller = new Controller(displayId, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn());
|
||||
asyncProcessors.add(controller);
|
||||
}
|
||||
|
||||
@@ -184,8 +189,13 @@ public final class Server {
|
||||
options.getSendFrameMeta());
|
||||
SurfaceCapture surfaceCapture;
|
||||
if (options.getVideoSource() == VideoSource.DISPLAY) {
|
||||
surfaceCapture = new ScreenCapture(controller, options.getDisplayId(), options.getMaxSize(), options.getCrop(),
|
||||
options.getLockVideoOrientation());
|
||||
if (newDisplay != null) {
|
||||
surfaceCapture = new NewDisplayCapture(controller, newDisplay, options.getMaxSize());
|
||||
} else {
|
||||
assert displayId != Device.DISPLAY_ID_NONE;
|
||||
surfaceCapture = new ScreenCapture(controller, displayId, options.getMaxSize(), options.getCrop(),
|
||||
options.getLockVideoOrientation());
|
||||
}
|
||||
} else {
|
||||
surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(),
|
||||
options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed());
|
||||
|
||||
@@ -40,6 +40,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
* In order to make events work correctly in all cases:
|
||||
* - virtualDisplayId must be used for events relative to the display (mouse and touch events with coordinates);
|
||||
* - displayId must be used for other events (like key events).
|
||||
*
|
||||
* If a new separate virtual display is created (using --new-display), then displayId == Device.DISPLAY_ID_NONE. In that case, all events are
|
||||
* sent to the virtual display id.
|
||||
*/
|
||||
|
||||
private static final class DisplayData {
|
||||
@@ -151,7 +154,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
private void control() throws IOException {
|
||||
// on start, power on the device
|
||||
if (powerOn && !Device.isScreenOn()) {
|
||||
if (powerOn && displayId != Device.DISPLAY_ID_NONE && !Device.isScreenOn()) {
|
||||
Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
|
||||
|
||||
// dirty hack
|
||||
@@ -270,7 +273,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
Device.rotateDevice(displayId);
|
||||
Device.rotateDevice(getActionDisplayId());
|
||||
break;
|
||||
case ControlMessage.TYPE_UHID_CREATE:
|
||||
getUhidManager().open(msg.getId(), msg.getText(), msg.getData());
|
||||
@@ -305,8 +308,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
if (events == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int actionDisplayId = getActionDisplayId();
|
||||
for (KeyEvent event : events) {
|
||||
if (!Device.injectEvent(event, displayId, Device.INJECT_MODE_ASYNC)) {
|
||||
if (!Device.injectEvent(event, actionDisplayId, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -543,10 +548,26 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
}
|
||||
|
||||
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) {
|
||||
return Device.injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode);
|
||||
return Device.injectKeyEvent(action, keyCode, repeat, metaState, getActionDisplayId(), injectMode);
|
||||
}
|
||||
|
||||
private boolean pressReleaseKeycode(int keyCode, int injectMode) {
|
||||
return Device.pressReleaseKeycode(keyCode, displayId, injectMode);
|
||||
return Device.pressReleaseKeycode(keyCode, getActionDisplayId(), injectMode);
|
||||
}
|
||||
|
||||
private int getActionDisplayId() {
|
||||
if (displayId != Device.DISPLAY_ID_NONE) {
|
||||
// Real screen mirrored, use the source display id
|
||||
return displayId;
|
||||
}
|
||||
|
||||
// Virtual display created by --new-display, use the virtualDisplayId
|
||||
DisplayData data = displayData.get();
|
||||
if (data == null) {
|
||||
// If no virtual display id is initialized yet, use the main display id
|
||||
return 0;
|
||||
}
|
||||
|
||||
return data.virtualDisplayId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import android.view.KeyEvent;
|
||||
|
||||
public final class Device {
|
||||
|
||||
public static final int DISPLAY_ID_NONE = -1;
|
||||
|
||||
public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF;
|
||||
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
|
||||
|
||||
@@ -159,6 +161,8 @@ public final class Device {
|
||||
}
|
||||
|
||||
public static boolean powerOffScreen(int displayId) {
|
||||
assert displayId != DISPLAY_ID_NONE;
|
||||
|
||||
if (!isScreenOn()) {
|
||||
return true;
|
||||
}
|
||||
@@ -169,6 +173,8 @@ public final class Device {
|
||||
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
|
||||
*/
|
||||
public static void rotateDevice(int displayId) {
|
||||
assert displayId != DISPLAY_ID_NONE;
|
||||
|
||||
WindowManager wm = ServiceManager.getWindowManager();
|
||||
|
||||
boolean accelerometerRotation = !wm.isRotationFrozen(displayId);
|
||||
@@ -187,6 +193,8 @@ public final class Device {
|
||||
}
|
||||
|
||||
private static int getCurrentRotation(int displayId) {
|
||||
assert displayId != DISPLAY_ID_NONE;
|
||||
|
||||
if (displayId == 0) {
|
||||
return ServiceManager.getWindowManager().getRotation();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.genymobile.scrcpy.device;
|
||||
|
||||
public final class NewDisplay {
|
||||
private Size size;
|
||||
private int dpi;
|
||||
|
||||
public NewDisplay() {
|
||||
// Auto size and dpi
|
||||
}
|
||||
|
||||
public NewDisplay(Size size, int dpi) {
|
||||
this.size = size;
|
||||
this.dpi = dpi;
|
||||
}
|
||||
|
||||
public Size getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public int getDpi() {
|
||||
return dpi;
|
||||
}
|
||||
|
||||
public boolean hasExplicitSize() {
|
||||
return size != null;
|
||||
}
|
||||
|
||||
public boolean hasExplicitDpi() {
|
||||
return dpi != 0;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@ public final class Size {
|
||||
return height;
|
||||
}
|
||||
|
||||
public int getMax() {
|
||||
return Math.max(width, height);
|
||||
}
|
||||
|
||||
public Size rotate() {
|
||||
return new Size(height, width);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.genymobile.scrcpy.video;
|
||||
|
||||
import com.genymobile.scrcpy.AndroidVersions;
|
||||
import com.genymobile.scrcpy.control.PositionMapper;
|
||||
import com.genymobile.scrcpy.device.DisplayInfo;
|
||||
import com.genymobile.scrcpy.device.NewDisplay;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.os.Build;
|
||||
import android.view.Surface;
|
||||
|
||||
public class NewDisplayCapture extends SurfaceCapture {
|
||||
|
||||
// Internal fields copied from android.hardware.display.DisplayManager
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS = 1 << 9;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_TRUSTED = 1 << 10;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP = 1 << 11;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED = 1 << 12;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED = 1 << 13;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_OWN_FOCUS = 1 << 14;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP = 1 << 15;
|
||||
|
||||
private final VirtualDisplayListener vdListener;
|
||||
private final NewDisplay newDisplay;
|
||||
|
||||
private Size mainDisplaySize;
|
||||
private int mainDisplayDpi;
|
||||
private int maxSize; // only used if newDisplay.getSize() != null
|
||||
|
||||
private VirtualDisplay virtualDisplay;
|
||||
private Size size;
|
||||
private int dpi;
|
||||
|
||||
public NewDisplayCapture(VirtualDisplayListener vdListener, NewDisplay newDisplay, int maxSize) {
|
||||
this.vdListener = vdListener;
|
||||
this.newDisplay = newDisplay;
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
size = newDisplay.getSize();
|
||||
dpi = newDisplay.getDpi();
|
||||
if (size == null || dpi == 0) {
|
||||
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(0);
|
||||
if (displayInfo != null) {
|
||||
mainDisplaySize = displayInfo.getSize();
|
||||
mainDisplayDpi = displayInfo.getDpi();
|
||||
} else {
|
||||
Ln.w("Main display not found, fallback to 1920x1080 240dpi");
|
||||
mainDisplaySize = new Size(1920, 1080);
|
||||
mainDisplayDpi = 240;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare() {
|
||||
if (!newDisplay.hasExplicitSize()) {
|
||||
size = ScreenInfo.computeVideoSize(mainDisplaySize.getWidth(), mainDisplaySize.getHeight(), maxSize);
|
||||
}
|
||||
if (!newDisplay.hasExplicitDpi()) {
|
||||
dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, size);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Surface surface) {
|
||||
if (virtualDisplay != null) {
|
||||
virtualDisplay.release();
|
||||
virtualDisplay = null;
|
||||
}
|
||||
|
||||
int virtualDisplayId;
|
||||
try {
|
||||
int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
|
||||
| DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
|
||||
| VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH
|
||||
| VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT
|
||||
| VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL
|
||||
| VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_33_ANDROID_13) {
|
||||
flags |= VIRTUAL_DISPLAY_FLAG_TRUSTED
|
||||
| VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP
|
||||
| VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED
|
||||
| VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED;
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) {
|
||||
flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS
|
||||
| VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
|
||||
}
|
||||
}
|
||||
virtualDisplay = ServiceManager.getDisplayManager()
|
||||
.createNewVirtualDisplay("scrcpy", size.getWidth(), size.getHeight(), dpi, surface, flags);
|
||||
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
|
||||
Ln.i("New display: " + size.getWidth() + "x" + size.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")");
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not create display", e);
|
||||
throw new AssertionError("Could not create display");
|
||||
}
|
||||
|
||||
if (vdListener != null) {
|
||||
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
|
||||
Rect contentRect = new Rect(0, 0, size.getWidth(), size.getHeight());
|
||||
PositionMapper positionMapper = new PositionMapper(size, contentRect, 0);
|
||||
vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (virtualDisplay != null) {
|
||||
virtualDisplay.release();
|
||||
virtualDisplay = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Size getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean setMaxSize(int newMaxSize) {
|
||||
if (newDisplay.hasExplicitSize()) {
|
||||
// Cannot retry with a different size if the display size was explicitly provided
|
||||
return false;
|
||||
}
|
||||
|
||||
maxSize = newMaxSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int scaleDpi(Size initialSize, int initialDpi, Size size) {
|
||||
int den = initialSize.getMax();
|
||||
int num = size.getMax();
|
||||
return initialDpi * num / den;
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ public final class ScreenInfo {
|
||||
return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top;
|
||||
}
|
||||
|
||||
private static Size computeVideoSize(int w, int h, int maxSize) {
|
||||
public static Size computeVideoSize(int w, int h, int maxSize) {
|
||||
// Compute the video size and the padding of the content inside this video.
|
||||
// Principle:
|
||||
// - scale down the great side of the screen to maxSize (if necessary);
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.FakeContext;
|
||||
import com.genymobile.scrcpy.device.DisplayInfo;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.Command;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.view.Display;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.regex.Matcher;
|
||||
@@ -126,4 +129,12 @@ public final class DisplayManager {
|
||||
Method method = getCreateVirtualDisplayMethod();
|
||||
return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface);
|
||||
}
|
||||
|
||||
public VirtualDisplay createNewVirtualDisplay(String name, int width, int height, int dpi, Surface surface, int flags) throws Exception {
|
||||
Constructor<android.hardware.display.DisplayManager> ctor = android.hardware.display.DisplayManager.class.getDeclaredConstructor(
|
||||
Context.class);
|
||||
ctor.setAccessible(true);
|
||||
android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get());
|
||||
return dm.createVirtualDisplay(name, width, height, dpi, surface, flags);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user