diff --git a/README.md b/README.md index b190406ca2..96b2f087eb 100644 --- a/README.md +++ b/README.md @@ -353,6 +353,21 @@ scrcpy --no-control scrcpy -n ``` +#### Display + +If several displays are available, it is possible to select the display to +mirror: + +```bash +scrcpy --display 1 +``` + +The list of display ids can be retrieved by: + +``` +adb shell dumpsys display # search "mDisplayId=" in the output +``` + #### Turn screen off It is possible to turn the device screen off while mirroring on start with a diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 55b7a631b2..da6c7f2d90 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -33,6 +33,15 @@ The values are expressed in the device natural orientation (typically, portrait .B \-\-max\-size value is computed on the cropped size. +.TP +.BI "\-\-display " id +Specify the display id to mirror. + +The list of possible display ids can be listed by "adb shell dumpsys display" +(search "mDisplayId=" in the output). + +Default is 0. + .TP .B \-f, \-\-fullscreen Start in fullscreen. diff --git a/app/src/cli.c b/app/src/cli.c index d2cd93b744..bb379f47cb 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -36,6 +36,15 @@ scrcpy_print_usage(const char *arg0) { " (typically, portrait for a phone, landscape for a tablet).\n" " Any --max-size value is computed on the cropped size.\n" "\n" + " --display id\n" + " Specify the display id to mirror.\n" + "\n" + " The list of possible display ids can be listed by:\n" + " adb shell dumpsys display\n" + " (search \"mDisplayId=\" in the output)\n" + "\n" + " Default is 0.\n" + "\n" " -f, --fullscreen\n" " Start in fullscreen.\n" "\n" @@ -363,6 +372,18 @@ parse_port_range(const char *s, struct port_range *port_range) { return true; } +static bool +parse_display_id(const char *s, uint16_t *display_id) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "display id"); + if (!ok) { + return false; + } + + *display_id = (uint16_t) value; + return true; +} + static bool parse_record_format(const char *optarg, enum recorder_format *format) { if (!strcmp(optarg, "mp4")) { @@ -407,6 +428,7 @@ guess_record_format(const char *filename) { #define OPT_WINDOW_BORDERLESS 1011 #define OPT_MAX_FPS 1012 #define OPT_LOCK_VIDEO_ORIENTATION 1013 +#define OPT_DISPLAY_ID 1014 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -414,6 +436,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, {"bit-rate", required_argument, NULL, 'b'}, {"crop", required_argument, NULL, OPT_CROP}, + {"display", required_argument, NULL, OPT_DISPLAY_ID}, {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, {"lock-video-orientation", required_argument, NULL, @@ -462,6 +485,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_CROP: opts->crop = optarg; break; + case OPT_DISPLAY_ID: + if (!parse_display_id(optarg, &opts->display_id)) { + return false; + } + break; case 'f': opts->fullscreen = true; break; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 4d9ad88b0b..84c87ad8a7 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -286,6 +286,7 @@ scrcpy(const struct scrcpy_options *options) { .max_fps = options->max_fps, .lock_video_orientation = options->lock_video_orientation, .control = options->control, + .display_id = options->display_id, }; if (!server_start(&server, options->serial, ¶ms)) { return false; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 014f2cfebb..b6c52fd783 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -25,6 +25,7 @@ struct scrcpy_options { int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; uint16_t window_height; + uint16_t display_id; bool show_touches; bool fullscreen; bool always_on_top; @@ -55,6 +56,7 @@ struct scrcpy_options { .window_y = WINDOW_POSITION_UNDEFINED, \ .window_width = 0, \ .window_height = 0, \ + .display_id = 0, \ .show_touches = false, \ .fullscreen = false, \ .always_on_top = false, \ diff --git a/app/src/server.c b/app/src/server.c index a8d598e42b..89bf9c72f8 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -234,10 +234,12 @@ execute_server(struct server *server, const struct server_params *params) { char bit_rate_string[11]; char max_fps_string[6]; char lock_video_orientation_string[3]; + char display_id_string[6]; sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); sprintf(max_fps_string, "%"PRIu16, params->max_fps); sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation); + sprintf(display_id_string, "%"PRIu16, params->display_id); const char *const cmd[] = { "shell", "CLASSPATH=" DEVICE_SERVER_PATH, @@ -264,6 +266,7 @@ execute_server(struct server *server, const struct server_params *params) { params->crop ? params->crop : "-", "true", // always send frame meta (packet boundaries + timestamp) params->control ? "true" : "false", + display_id_string, }; #ifdef SERVER_DEBUGGER LOGI("Server debugger waiting for a client on device port " diff --git a/app/src/server.h b/app/src/server.h index d84a5cc8c2..cc712b63f7 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -44,6 +44,7 @@ struct server_params { uint16_t max_fps; int8_t lock_video_orientation; bool control; + uint16_t display_id; }; // init default values diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 7595a9d7f0..32a18e16af 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -75,19 +75,29 @@ private void handleEvent() throws IOException { ControlMessage msg = connection.receiveControlMessage(); switch (msg.getType()) { case ControlMessage.TYPE_INJECT_KEYCODE: - injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); + if (device.supportsInputEvents()) { + injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); + } break; case ControlMessage.TYPE_INJECT_TEXT: - injectText(msg.getText()); + if (device.supportsInputEvents()) { + injectText(msg.getText()); + } break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); + if (device.supportsInputEvents()) { + injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); + } break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: - injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + if (device.supportsInputEvents()) { + injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + } break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: - pressBackOrTurnScreenOn(); + if (device.supportsInputEvents()) { + pressBackOrTurnScreenOn(); + } break; case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: device.expandNotificationPanel(); @@ -103,7 +113,9 @@ private void handleEvent() throws IOException { device.setClipboardText(msg.getText()); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: - device.setScreenPowerMode(msg.getAction()); + if (device.supportsInputEvents()) { + device.setScreenPowerMode(msg.getAction()); + } break; case ControlMessage.TYPE_ROTATE_DEVICE: device.rotateDevice(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 1b1fbf7dac..64d77de3a7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.WindowManager; @@ -25,9 +26,35 @@ public interface RotationListener { private ScreenInfo screenInfo; private RotationListener rotationListener; + /** + * Logical display identifier + */ + private final int displayId; + + /** + * The surface flinger layer stack associated with this logical display + */ + private final int layerStack; + + /** + * The FLAG_PRESENTATION from the DisplayInfo + */ + private final boolean isPresentationDisplay; + public Device(Options options) { - DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); + displayId = options.getDisplayId(); + DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + int[] displayIds = serviceManager.getDisplayManager().getDisplayIds(); + throw new InvalidDisplayIdException(displayId, displayIds); + } + + int displayInfoFlags = displayInfo.getFlags(); + screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); + layerStack = displayInfo.getLayerStack(); + isPresentationDisplay = (displayInfoFlags & DisplayInfo.FLAG_PRESENTATION) != 0; + registerRotationWatcher(new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) throws RemoteException { @@ -41,12 +68,24 @@ public void onRotationChanged(int rotation) throws RemoteException { } } }); + + if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { + Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); + } + + if (!supportsInputEvents()) { + Ln.w("Input events are not supported for displays with FLAG_PRESENTATION enabled for devices with API lower than 29"); + } } public synchronized ScreenInfo getScreenInfo() { return screenInfo; } + public int getLayerStack() { + return layerStack; + } + public Point getPhysicalPoint(Position position) { // it hides the field on purpose, to read it with a lock @SuppressWarnings("checkstyle:HiddenField") @@ -76,7 +115,22 @@ public static String getDeviceName() { return Build.MODEL; } + public boolean supportsInputEvents() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return true; + } + return !isPresentationDisplay; + } + public boolean injectInputEvent(InputEvent inputEvent, int mode) { + if (!supportsInputEvents()) { + throw new AssertionError("Could not inject input event if !supportsInputEvents()"); + } + + if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) { + return false; + } + return serviceManager.getInputManager().injectInputEvent(inputEvent, mode); } diff --git a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java index 639869b514..50bc94aa63 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java @@ -1,12 +1,25 @@ package com.genymobile.scrcpy; public final class DisplayInfo { + private final int displayId; private final Size size; private final int rotation; + private final int layerStack; + private final int flags; - public DisplayInfo(Size size, int rotation) { + public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; + public static final int FLAG_PRESENTATION = 0x00000008; + + public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) { + this.displayId = displayId; this.size = size; this.rotation = rotation; + this.layerStack = layerStack; + this.flags = flags; + } + + public int getDisplayId() { + return displayId; } public Size getSize() { @@ -16,5 +29,13 @@ public Size getSize() { public int getRotation() { return rotation; } + + public int getLayerStack() { + return layerStack; + } + + public int getFlags() { + return flags; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java b/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java new file mode 100644 index 0000000000..81e3b90377 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java @@ -0,0 +1,21 @@ +package com.genymobile.scrcpy; + +public class InvalidDisplayIdException extends RuntimeException { + + private final int displayId; + private final int[] availableDisplayIds; + + public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) { + super("There is no display having id " + displayId); + this.displayId = displayId; + this.availableDisplayIds = availableDisplayIds; + } + + public int getDisplayId() { + return displayId; + } + + public int[] getAvailableDisplayIds() { + return availableDisplayIds; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index d9a29452b0..2a4bba3374 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -11,6 +11,7 @@ public class Options { private Rect crop; private boolean sendFrameMeta; // send PTS so that the client may record properly private boolean control; + private int displayId; public int getMaxSize() { return maxSize; @@ -75,4 +76,12 @@ public boolean getControl() { public void setControl(boolean control) { this.control = control; } + + public int getDisplayId() { + return displayId; + } + + public void setDisplayId(int displayId) { + this.displayId = displayId; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index d75833f3b8..40457af8a4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -71,10 +71,12 @@ public void streamScreen(Device device, FileDescriptor fd) throws IOException { // does not include the locked video orientation Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); int videoRotation = screenInfo.getVideoRotation(); + int layerStack = device.getLayerStack(); + setSize(format, videoRect.width(), videoRect.height()); configure(codec, format); Surface surface = codec.createInputSurface(); - setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); codec.start(); try { alive = encode(codec, fd); @@ -177,12 +179,12 @@ private static void setSize(MediaFormat format, int width, int height) { format.setInteger(MediaFormat.KEY_HEIGHT, height); } - private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect) { + private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) { SurfaceControl.openTransaction(); try { SurfaceControl.setDisplaySurface(display, surface); SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); - SurfaceControl.setDisplayLayerStack(display, 0); + SurfaceControl.setDisplayLayerStack(display, layerStack); } finally { SurfaceControl.closeTransaction(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index c495442967..bd38728537 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -80,8 +80,8 @@ private static Options createOptions(String... args) { "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - if (args.length != 9) { - throw new IllegalArgumentException("Expecting 9 parameters"); + if (args.length != 10) { + throw new IllegalArgumentException("Expecting 10 parameters"); } Options options = new Options(); @@ -111,6 +111,9 @@ private static Options createOptions(String... args) { boolean control = Boolean.parseBoolean(args[8]); options.setControl(control); + int displayId = Integer.parseInt(args[9]); + options.setDisplayId(displayId); + return options; } @@ -149,6 +152,16 @@ private static void suggestFix(Throwable e) { } } } + if (e instanceof InvalidDisplayIdException) { + InvalidDisplayIdException idie = (InvalidDisplayIdException) e; + int[] displayIds = idie.getAvailableDisplayIds(); + if (displayIds != null && displayIds.length > 0) { + Ln.e("Try to use one of the available display ids:"); + for (int id : displayIds) { + Ln.e(" scrcpy --display " + id); + } + } + } } public static void main(String... args) throws Exception { 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 568afacd6f..cedb3f47ed 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -12,15 +12,28 @@ public DisplayManager(IInterface manager) { this.manager = manager; } - public DisplayInfo getDisplayInfo() { + public DisplayInfo getDisplayInfo(int displayId) { try { - Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0); + Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); + if (displayInfo == null) { + return null; + } Class cls = displayInfo.getClass(); // width and height already take the rotation into account int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo); int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo); int rotation = cls.getDeclaredField("rotation").getInt(displayInfo); - return new DisplayInfo(new Size(width, height), rotation); + int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); + int flags = cls.getDeclaredField("flags").getInt(displayInfo); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public int[] getDisplayIds() { + try { + return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager); } catch (Exception e) { throw new AssertionError(e); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 44fa613b2a..36d073532e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -17,6 +17,8 @@ public final class InputManager { private final IInterface manager; private Method injectInputEventMethod; + private static Method setDisplayIdMethod; + public InputManager(IInterface manager) { this.manager = manager; } @@ -37,4 +39,23 @@ public boolean injectInputEvent(InputEvent inputEvent, int mode) { return false; } } + + private static Method getSetDisplayIdMethod() throws NoSuchMethodException { + if (setDisplayIdMethod == null) { + setDisplayIdMethod = InputEvent.class.getMethod("setDisplayId", int.class); + } + return setDisplayIdMethod; + } + + public static boolean setDisplayId(InputEvent inputEvent, int displayId) { + try { + Method method = getSetDisplayIdMethod(); + method.invoke(inputEvent, displayId); + return true; + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + // just a warning, it might happen on old devices + Ln.w("Cannot associate a display id to the input event"); + return false; + } + } }