diff --git a/app/meson.build b/app/meson.build index 3bcb9bc10b..a6e511c462 100644 --- a/app/meson.build +++ b/app/meson.build @@ -110,6 +110,10 @@ conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited # overridden by option --bit-rate conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps +# the default display number +# overridden by option --display +conf.set('DEFAULT_DISPLAY_ID', '0') # Primary display + # enable High DPI support conf.set('HIDPI_SUPPORT', get_option('hidpi_support')) diff --git a/app/src/cli.c b/app/src/cli.c index d9e1013a0a..973961ab10 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -35,6 +35,10 @@ 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" + " -d, --display value\n" + " Use specified display in multi-display Android systems.\n" + " Default is %d.\n" + "\n" " -f, --fullscreen\n" " Start in fullscreen.\n" "\n" @@ -192,6 +196,7 @@ scrcpy_print_usage(const char *arg0) { "\n", arg0, DEFAULT_BIT_RATE, + DEFAULT_DISPLAY_ID, DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)", DEFAULT_LOCAL_PORT); } @@ -235,6 +240,19 @@ parse_bit_rate(const char *s, uint32_t *bit_rate) { 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"); + if (!ok) { + return false; + } + + *display_id = (uint16_t) value; + return true; +} + + static bool parse_max_size(const char *s, uint16_t *max_size) { long value; @@ -347,6 +365,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, 'd'}, {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, {"max-fps", required_argument, NULL, OPT_MAX_FPS}, @@ -379,7 +398,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { optind = 0; // reset to start from the first argument in tests int c; - while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, + while ((c = getopt_long(argc, argv, "b:c:d:fF:hm:nNp:r:s:StTv", long_options, NULL)) != -1) { switch (c) { case 'b': @@ -390,6 +409,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case 'c': LOGW("Deprecated option -c. Use --crop instead."); // fall through + case 'd': + if (!parse_display_id(optarg, &opts->display_id)) { + return false; + } + break; case OPT_CROP: opts->crop = optarg; break; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 17be1ed415..41a253f163 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -285,6 +285,7 @@ scrcpy(const struct scrcpy_options *options) { .bit_rate = options->bit_rate, .max_fps = options->max_fps, .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 75de8717f3..e52804691c 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -18,6 +18,7 @@ struct scrcpy_options { uint16_t port; uint16_t max_size; uint32_t bit_rate; + uint16_t display_id; uint16_t max_fps; int16_t window_x; int16_t window_y; @@ -44,6 +45,7 @@ struct scrcpy_options { .port = DEFAULT_LOCAL_PORT, \ .max_size = DEFAULT_MAX_SIZE, \ .bit_rate = DEFAULT_BIT_RATE, \ + .display_id = DEFAULT_DISPLAY_ID, \ .max_fps = 0, \ .window_x = -1, \ .window_y = -1, \ diff --git a/app/src/server.c b/app/src/server.c index ff167aebcc..a189a8e456 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -124,9 +124,11 @@ execute_server(struct server *server, const struct server_params *params) { char max_size_string[6]; char bit_rate_string[11]; char max_fps_string[6]; + 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(display_id_string, "%"PRIu16, params->display_id); const char *const cmd[] = { "shell", "CLASSPATH=" DEVICE_SERVER_PATH, @@ -146,6 +148,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 0cb1ab3a6d..68a8f3ea10 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -37,6 +37,7 @@ struct server_params { uint32_t bit_rate; uint16_t max_fps; bool control; + uint16_t display_id; }; // init default values diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 539c3c94cf..0ea213c1f2 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -48,6 +48,7 @@ static void test_options(void) { "--fullscreen", "--max-fps", "30", "--max-size", "1024", + "--display", "1", // "--no-control" is not compatible with "--turn-screen-off" // "--no-display" is not compatible with "--fulscreen" "--port", "1234", @@ -78,6 +79,7 @@ static void test_options(void) { assert(opts->fullscreen); assert(opts->max_fps == 30); assert(opts->max_size == 1024); + assert(opts->display_id == 1); assert(opts->port == 1234); assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->record_filename, "file")); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index dc0fa67bc1..8c89286ef2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.InputManager; +import com.genymobile.scrcpy.wrappers.MotionEventWrapper; import android.os.SystemClock; import android.view.InputDevice; @@ -179,9 +180,10 @@ private boolean injectTouch(int action, long pointerId, Position position, float } } - MotionEvent event = MotionEvent + MotionEvent event = MotionEventWrapper .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0, - InputDevice.SOURCE_TOUCHSCREEN, 0); + InputDevice.SOURCE_TOUCHSCREEN, device.getScreenInfo().getDisplayId(), 0); + return injectEvent(event); } @@ -202,9 +204,9 @@ private boolean injectScroll(Position position, int hScroll, int vScroll) { coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); - MotionEvent event = MotionEvent + MotionEvent event = MotionEventWrapper .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0, - InputDevice.SOURCE_MOUSE, 0); + InputDevice.SOURCE_MOUSE, device.getScreenInfo().getDisplayId(), 0); return injectEvent(event); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 9448098a5b..56dd3de7c9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -26,7 +26,8 @@ public interface RotationListener { private RotationListener rotationListener; public Device(Options options) { - screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize()); + int displayId = options.getDisplayId(); + screenInfo = computeScreenInfo(displayId, options.getCrop(), options.getMaxSize()); registerRotationWatcher(new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) throws RemoteException { @@ -39,15 +40,15 @@ public void onRotationChanged(int rotation) throws RemoteException { } } } - }); + }, displayId); } public synchronized ScreenInfo getScreenInfo() { return screenInfo; } - private ScreenInfo computeScreenInfo(Rect crop, int maxSize) { - DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); + private ScreenInfo computeScreenInfo(int displayId, Rect crop, int maxSize) { + DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId); boolean rotated = (displayInfo.getRotation() & 1) != 0; Size deviceSize = displayInfo.getSize(); Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); @@ -64,7 +65,7 @@ private ScreenInfo computeScreenInfo(Rect crop, int maxSize) { } Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); - return new ScreenInfo(contentRect, videoSize, rotated); + return new ScreenInfo(displayId, contentRect, videoSize, rotated); } private static String formatCrop(Rect rect) { @@ -129,8 +130,8 @@ public boolean isScreenOn() { return serviceManager.getPowerManager().isScreenOn(); } - public void registerRotationWatcher(IRotationWatcher rotationWatcher) { - serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher); + public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { + serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); } public synchronized void setRotationListener(RotationListener rotationListener) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 5b993f3085..75e2e0c16a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -3,6 +3,7 @@ import android.graphics.Rect; public class Options { + private int displayId; private int maxSize; private int bitRate; private int maxFps; @@ -11,6 +12,14 @@ public class Options { private boolean sendFrameMeta; // send PTS so that the client may record properly private boolean control; + public int getDisplayId() { + return displayId; + } + + public void setDisplayId(int displayId) { + this.displayId = displayId; + } + public int getMaxSize() { return maxSize; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index c9a37f847b..03b4360c16 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -64,10 +64,11 @@ public void streamScreen(Device device, FileDescriptor fd) throws IOException { IBinder display = createDisplay(); Rect contentRect = device.getScreenInfo().getContentRect(); Rect videoRect = device.getScreenInfo().getVideoSize().toRect(); + int displayId = device.getScreenInfo().getDisplayId(); setSize(format, videoRect.width(), videoRect.height()); configure(codec, format); Surface surface = codec.createInputSurface(); - setDisplaySurface(display, surface, contentRect, videoRect); + setDisplaySurface(display, surface, displayId, contentRect, videoRect); codec.start(); try { alive = encode(codec, fd); @@ -172,12 +173,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, Rect deviceRect, Rect displayRect) { + private static void setDisplaySurface(IBinder display, Surface surface, int displayId, Rect deviceRect, Rect displayRect) { SurfaceControl.openTransaction(); try { SurfaceControl.setDisplaySurface(display, surface); SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect); - SurfaceControl.setDisplayLayerStack(display, 0); + SurfaceControl.setDisplayLayerStack(display, displayId); } finally { SurfaceControl.closeTransaction(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java index f2fce1d66b..d536dd54aa 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -6,13 +6,19 @@ public final class ScreenInfo { private final Rect contentRect; // device size, possibly cropped private final Size videoSize; private final boolean rotated; + private final int displayId; - public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) { + public ScreenInfo(int displayId, Rect contentRect, Size videoSize, boolean rotated) { + this.displayId = displayId; this.contentRect = contentRect; this.videoSize = videoSize; this.rotated = rotated; } + public int getDisplayId() { + return displayId; + } + public Rect getContentRect() { return contentRect; } @@ -26,6 +32,6 @@ public ScreenInfo withRotation(int rotation) { if (rotated == newRotated) { return this; } - return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated); + return new ScreenInfo(displayId, Device.flipRect(contentRect), videoSize.rotate(), newRotated); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 56b738fbc7..0869059269 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -79,8 +79,8 @@ private static Options createOptions(String... args) { "The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")"); } - if (args.length != 8) { - throw new IllegalArgumentException("Expecting 8 parameters"); + if (args.length != 9) { + throw new IllegalArgumentException("Expecting 9 parameters"); } Options options = new Options(); @@ -107,6 +107,9 @@ private static Options createOptions(String... args) { boolean control = Boolean.parseBoolean(args[7]); options.setControl(control); + int displayId = Integer.parseInt(args[8]); + options.setDisplayId(displayId); + return options; } 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..06876db4b4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -12,9 +12,9 @@ public DisplayManager(IInterface manager) { this.manager = manager; } - public DisplayInfo getDisplayInfo() { + public DisplayInfo getDisplayInfo(int display) { try { - Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0); + Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, display); Class cls = displayInfo.getClass(); // width and height already take the rotation into account int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/MotionEventWrapper.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/MotionEventWrapper.java new file mode 100644 index 0000000000..6df8ed9db1 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/MotionEventWrapper.java @@ -0,0 +1,29 @@ +package com.genymobile.scrcpy.wrappers; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import android.view.MotionEvent; + +public final class MotionEventWrapper { + static public MotionEvent obtain(long downTime, long eventTime, + int action, int pointerCount, MotionEvent.PointerProperties[] pointerProperties, + MotionEvent.PointerCoords[] pointerCoords, int metaState, int buttonState, + float xPrecision, float yPrecision, int deviceId, + int edgeFlags, int source, int displayId, int flags) { + MotionEvent motionEvent; + try { + // Modern Android 10 (29+ API), Android R has new displayId parameter, to support multi-display touch + motionEvent = (MotionEvent)MotionEvent.class.getMethod("obtain", + long.class, long.class, + int.class, int.class, MotionEvent.PointerProperties[].class, + MotionEvent.PointerCoords[].class, int.class, int.class, + float.class, float.class, int.class, + int.class, int.class, int.class, int.class) + .invoke(null, downTime, eventTime, action, pointerCount, pointerProperties, pointerCoords, metaState, buttonState, xPrecision, yPrecision, deviceId, edgeFlags, source, displayId, flags); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + motionEvent = MotionEvent.obtain(downTime, eventTime, action, pointerCount, pointerProperties, pointerCoords, metaState, buttonState, xPrecision, yPrecision, deviceId, edgeFlags, source, flags); + } + return motionEvent; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index cc687cd5e3..faa366a5d1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -93,13 +93,13 @@ public void thawRotation() { } } - public void registerRotationWatcher(IRotationWatcher rotationWatcher) { + public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { try { Class cls = manager.getClass(); try { // display parameter added since this commit: // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1 - cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0); + cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId); } catch (NoSuchMethodException e) { // old version cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);