From 98ed5eb643553b6886babfd083931bef4fb4a5b8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH] 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 PR #5370 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> Co-authored-by: anirudhb --- app/data/bash-completion/scrcpy | 2 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 12 ++ app/src/cli.c | 43 ++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 4 + app/src/server.h | 1 + .../java/com/genymobile/scrcpy/CleanUp.java | 6 +- .../java/com/genymobile/scrcpy/Options.java | 42 +++++ .../java/com/genymobile/scrcpy/Server.java | 18 ++- .../genymobile/scrcpy/control/Controller.java | 31 +++- .../com/genymobile/scrcpy/device/Device.java | 8 + .../genymobile/scrcpy/device/NewDisplay.java | 31 ++++ .../com/genymobile/scrcpy/device/Size.java | 4 + .../scrcpy/video/NewDisplayCapture.java | 146 ++++++++++++++++++ .../genymobile/scrcpy/video/ScreenInfo.java | 2 +- .../scrcpy/wrappers/DisplayManager.java | 11 ++ 19 files changed, 353 insertions(+), 12 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index db825ecc..4f40d466 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -46,6 +46,8 @@ _scrcpy() { --mouse-bind= -n --no-control -N --no-playback + --new-display + --new-display= --no-audio --no-audio-playback --no-cleanup diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index fa0fa84f..f65430e0 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -52,6 +52,7 @@ arguments=( '--mouse-bind=[Configure bindings of secondary clicks]' {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' {-N,--no-playback}'[Disable video and audio playback]' + '--new-display=[Create a new display]' '--no-audio[Disable audio forwarding]' '--no-audio-playback[Disable audio playback]' '--no-cleanup[Disable device cleanup actions on exit]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 3fd3eb29..8a0d09aa 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -314,6 +314,18 @@ Disable device control (mirror the device in read\-only). .B \-N, \-\-no\-playback Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR). +.TP +\fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]] +Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI, and \fB\-\-max\-size\fR is considered. + +Examples: + + \-\-new\-display=1920x1080 + \-\-new\-display=1920x1080/420 + \-\-new\-display # main display size and density + \-\-new\-display -m1920 # scaled to fit a max size of 1920 + \-\-new\-display=/240 # main display size and 240 dpi + .TP .B \-\-no\-audio Disable audio forwarding. diff --git a/app/src/cli.c b/app/src/cli.c index 4fc3c534..88477c00 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -102,6 +102,7 @@ enum { OPT_NO_MOUSE_HOVER, OPT_AUDIO_DUP, OPT_GAMEPAD, + OPT_NEW_DISPLAY, }; struct sc_option { @@ -557,6 +558,21 @@ static const struct sc_option options[] = { .text = "Disable video and audio playback on the computer (equivalent " "to --no-video-playback --no-audio-playback).", }, + { + .longopt_id = OPT_NEW_DISPLAY, + .longopt = "new-display", + .argdesc = "[x][/]", + .optional_arg = true, + .text = "Create a new display with the specified resolution and " + "density. If not provided, they default to the main display " + "dimensions and DPI, and --max-size is considered.\n" + "Examples:\n" + " --new-display=1920x1080\n" + " --new-display=1920x1080/420 # force 420 dpi\n" + " --new-display # main display size and density\n" + " --new-display -m1920 # scaled to fit a max size of 1920\n" + " --new-display=/240 # main display size and 240 dpi", + }, { .longopt_id = OPT_NO_AUDIO, .longopt = "no-audio", @@ -2668,6 +2684,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_NEW_DISPLAY: + opts->new_display = optarg ? optarg : ""; + break; default: // getopt prints the error message on stderr return false; @@ -2848,6 +2867,25 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->new_display) { + if (opts->video_source != SC_VIDEO_SOURCE_DISPLAY) { + LOGE("--new-display is only available with --video-source=display"); + return false; + } + + if (!opts->video) { + LOGE("--new-display is incompatible with --no-video"); + return false; + } + + if (opts->max_size && opts->new_display[0] != '\0' + && opts->new_display[0] != '/') { + // An explicit size is defined (not "" nor "/") + LOGE("Cannot specify both --new-display size and -m/--max-size"); + return false; + } + } + if (otg) { if (!opts->control) { LOGE("--no-control is not allowed in OTG mode"); @@ -2954,6 +2992,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + if (opts->display_id != 0 && opts->new_display) { + LOGE("Cannot specify both --display-id and --new-display"); + return false; + } + if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) { // Select the audio source according to the video source if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) { diff --git a/app/src/options.c b/app/src/options.c index f8448792..62fcd925 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -103,6 +103,7 @@ const struct scrcpy_options scrcpy_options_default = { .window = true, .mouse_hover = true, .audio_dup = false, + .new_display = NULL, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 5f6726e0..f3d27a88 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -308,6 +308,7 @@ struct scrcpy_options { bool window; bool mouse_hover; bool audio_dup; + const char *new_display; // [x][/] parsed by the server }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 854657fb..502498ad 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -431,6 +431,7 @@ scrcpy(struct scrcpy_options *options) { .lock_video_orientation = options->lock_video_orientation, .control = options->control, .display_id = options->display_id, + .new_display = options->new_display, .video = options->video, .audio = options->audio, .audio_dup = options->audio_dup, diff --git a/app/src/server.c b/app/src/server.c index b7f3b56d..26725fa0 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -355,6 +355,10 @@ execute_server(struct sc_server *server, // By default, power_on is true ADD_PARAM("power_on=false"); } + if (params->new_display) { + VALIDATE_STRING(params->new_display); + ADD_PARAM("new_display=%s", params->new_display); + } if (params->list & SC_OPTION_LIST_ENCODERS) { ADD_PARAM("list_encoders=true"); } diff --git a/app/src/server.h b/app/src/server.h index d9d42582..4ff5539d 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -48,6 +48,7 @@ struct sc_server_params { int8_t lock_video_orientation; bool control; uint32_t display_id; + const char *new_display; bool video; bool audio; bool audio_dup; diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 1b8d4248..a47aae90 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -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); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 9eab1d90..a4b9d28b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -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) + // - "x/" + // - "x" + // - "/" + 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); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 91e7ce6c..fd854e06 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -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()); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 5175ed5e..1bc4c692 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -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; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 35266d0e..1cf9aae5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -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(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java new file mode 100644 index 00000000..3aa2996a --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java @@ -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; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java index bc9dce1c..230fd29e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -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); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java new file mode 100644 index 00000000..8f507fdf --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -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; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java index 1f74ce34..cc82a654 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -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); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index b91b7146..c8c405bb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -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 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); + } }