From 89f6a3cfe7ea74be51b0f91b8da46f27c5214038 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Jan 2018 15:40:33 +0100 Subject: [PATCH] Handle resized video stream Accept a parameter to limit the video size. For instance, with "-m 960", the great side of the video will be scaled down to 960 (if necessary), while the other side will be scaled down so that the aspect ratio is preserved. Both dimensions must be a multiple of 8, so black bands might be added, and the mouse positions must be computed accordingly. --- app/src/main.c | 21 +++++- app/src/scrcpy.c | 4 +- app/src/scrcpy.h | 2 +- app/src/server.c | 7 +- app/src/server.h | 2 +- server/Makefile | 3 + .../genymobile/scrcpy/DesktopConnection.java | 8 +-- server/src/com/genymobile/scrcpy/Device.java | 67 ++++++++++++++----- .../com/genymobile/scrcpy/DisplayInfo.java | 20 ++++++ server/src/com/genymobile/scrcpy/Options.java | 13 ++++ server/src/com/genymobile/scrcpy/Point.java | 16 +++++ .../src/com/genymobile/scrcpy/Position.java | 48 +++++++------ .../com/genymobile/scrcpy/ScrCpyServer.java | 13 ++-- .../src/com/genymobile/scrcpy/ScreenInfo.java | 43 +++++++----- .../com/genymobile/scrcpy/ScreenStreamer.java | 6 +- .../scrcpy/ScreenStreamerSession.java | 20 ++++-- server/src/com/genymobile/scrcpy/Size.java | 47 +++++++++++++ .../scrcpy/wrappers/DisplayManager.java | 13 ++-- 18 files changed, 270 insertions(+), 83 deletions(-) create mode 100644 server/src/com/genymobile/scrcpy/DisplayInfo.java create mode 100644 server/src/com/genymobile/scrcpy/Options.java create mode 100644 server/src/com/genymobile/scrcpy/Size.java diff --git a/app/src/main.c b/app/src/main.c index 4b09880b..c68117b0 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -9,15 +9,16 @@ struct args { const char *serial; Uint16 port; + Uint16 maximum_size; }; int parse_args(struct args *args, int argc, char *argv[]) { int c; - while ((c = getopt(argc, argv, "p:")) != -1) { + while ((c = getopt(argc, argv, "p:m:")) != -1) { switch (c) { case 'p': { char *endptr; - long int value = strtol(optarg, &endptr, 0); + long value = strtol(optarg, &endptr, 0); if (*optarg == '\0' || *endptr != '\0') { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid port: %s\n", optarg); return -1; @@ -29,6 +30,20 @@ int parse_args(struct args *args, int argc, char *argv[]) { args->port = (Uint16) value; break; } + case 'm': { + char *endptr; + long value = strtol(optarg, &endptr, 0); + if (*optarg == '\0' || *endptr != '\0') { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid maximum size: %s\n", optarg); + return -1; + } + if (value & ~0xffff) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Maximum size must be between 0 and 65535: %ld\n", value); + return -1; + } + args->maximum_size = (Uint16) value; + break; + } default: // getopt prints the error message on stderr return -1; @@ -65,7 +80,7 @@ int main(int argc, char *argv[]) { SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); - res = scrcpy(args.serial, args.port) ? 0 : 1; + res = scrcpy(args.serial, args.port, args.maximum_size) ? 0 : 1; avformat_network_deinit(); // ignore failure diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index b1e60a8e..ef51f2ce 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -382,7 +382,7 @@ void event_loop(void) { } } -SDL_bool scrcpy(const char *serial, Uint16 local_port) { +SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 maximum_size) { SDL_bool ret = 0; process_t push_proc = push_server(serial); @@ -402,7 +402,7 @@ SDL_bool scrcpy(const char *serial, Uint16 local_port) { } // server will connect to our socket - process_t server = start_server(serial); + process_t server = start_server(serial, maximum_size); if (server == PROCESS_NONE) { ret = SDL_FALSE; SDLNet_TCP_Close(server_socket); diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index adbddd14..addc7b40 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -3,6 +3,6 @@ #include -SDL_bool scrcpy(const char *serial, Uint16 local_port); +SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 maximum_size); #endif diff --git a/app/src/server.c b/app/src/server.c index 92ba1d2d..1b752166 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -21,13 +21,16 @@ process_t disable_tunnel(const char *serial) { return adb_reverse_remove(serial, SOCKET_NAME); } -process_t start_server(const char *serial) { +process_t start_server(const char *serial, Uint16 maximum_size) { + char maximum_size_string[6]; + sprintf(maximum_size_string, "%d", maximum_size); const char *const cmd[] = { "shell", "CLASSPATH=/data/local/tmp/scrcpy-server.jar", "app_process", "/system/bin", - "com.genymobile.scrcpy.ScrCpyServer" + "com.genymobile.scrcpy.ScrCpyServer", + maximum_size_string, }; return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0])); } diff --git a/app/src/server.h b/app/src/server.h index 45279bee..e366fac2 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -4,5 +4,5 @@ process_t push_server(const char *serial); process_t enable_tunnel(const char *serial, Uint16 local_port); process_t disable_tunnel(const char *serial); -process_t start_server(const char *serial); +process_t start_server(const char *serial, Uint16 maximum_size); void stop_server(process_t server); diff --git a/server/Makefile b/server/Makefile index 22cea8e4..af9e3a64 100644 --- a/server/Makefile +++ b/server/Makefile @@ -24,13 +24,16 @@ SRC := com/genymobile/scrcpy/ScrCpyServer.java \ com/genymobile/scrcpy/ControlEventReader.java \ com/genymobile/scrcpy/DesktopConnection.java \ com/genymobile/scrcpy/Device.java \ + com/genymobile/scrcpy/DisplayInfo.java \ com/genymobile/scrcpy/EventController.java \ com/genymobile/scrcpy/Ln.java \ + com/genymobile/scrcpy/Options.java \ com/genymobile/scrcpy/Point.java \ com/genymobile/scrcpy/Position.java \ com/genymobile/scrcpy/ScreenInfo.java \ com/genymobile/scrcpy/ScreenStreamer.java \ com/genymobile/scrcpy/ScreenStreamerSession.java \ + com/genymobile/scrcpy/Size.java \ com/genymobile/scrcpy/wrappers/DisplayManager.java \ com/genymobile/scrcpy/wrappers/InputManager.java \ com/genymobile/scrcpy/wrappers/ServiceManager.java \ diff --git a/server/src/com/genymobile/scrcpy/DesktopConnection.java b/server/src/com/genymobile/scrcpy/DesktopConnection.java index 50b4b942..6e71c63a 100644 --- a/server/src/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/com/genymobile/scrcpy/DesktopConnection.java @@ -36,12 +36,9 @@ public class DesktopConnection implements Closeable { public static DesktopConnection open(Device device) throws IOException { LocalSocket socket = connect(SOCKET_NAME); - ScreenInfo initialScreenInfo = device.getScreenInfo(); - int width = initialScreenInfo.getLogicalWidth(); - int height = initialScreenInfo.getLogicalHeight(); - DesktopConnection connection = new DesktopConnection(socket); - connection.send(Device.getDeviceName(), width, height); + Size videoSize = device.getScreenInfo().getVideoSize(); + connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); return connection; } @@ -81,4 +78,3 @@ public class DesktopConnection implements Closeable { return event; } } - diff --git a/server/src/com/genymobile/scrcpy/Device.java b/server/src/com/genymobile/scrcpy/Device.java index ec5f7800..baecf06e 100644 --- a/server/src/com/genymobile/scrcpy/Device.java +++ b/server/src/com/genymobile/scrcpy/Device.java @@ -7,7 +7,7 @@ import android.view.IRotationWatcher; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; -public class Device { +public final class Device { public interface RotationListener { void onRotationChanged(int rotation); @@ -18,13 +18,12 @@ public class Device { private ScreenInfo screenInfo; private RotationListener rotationListener; - public Device() { - screenInfo = readScreenInfo(); + public Device(Options options) { + screenInfo = computeScreenInfo(options.getMaximumSize()); registerRotationWatcher(new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) throws RemoteException { synchronized (Device.this) { - // update screenInfo cache screenInfo = screenInfo.withRotation(rotation); // notify @@ -37,23 +36,59 @@ public class Device { } public synchronized ScreenInfo getScreenInfo() { - if (screenInfo == null) { - screenInfo = readScreenInfo(); - } return screenInfo; } - public Point getPhysicalPoint(Position position) { - ScreenInfo screenInfo = getScreenInfo(); - int deviceWidth = screenInfo.getLogicalWidth(); - int deviceHeight = screenInfo.getLogicalHeight(); - int scaledX = position.getX() * deviceWidth / position.getScreenWidth(); - int scaledY = position.getY() * deviceHeight / position.getScreenHeight(); - return new Point(scaledX, scaledY); + private ScreenInfo computeScreenInfo(int maximumSize) { + DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); + boolean rotated = (displayInfo.getRotation() & 1) != 0; + Size deviceSize = displayInfo.getSize(); + int w = deviceSize.getWidth(); + int h = deviceSize.getHeight(); + int padding = 0; + if (maximumSize > 0) { + assert maximumSize % 8 == 0; + boolean portrait = h > w; + int major = portrait ? h : w; + int minor = portrait ? w : h; + if (major > maximumSize) { + int minorExact = minor * maximumSize / major; + // +7 to ceil the value on rounding to the next multiple of 8, + // so that any necessary black bands to keep the aspect ratio are added to the smallest dimension + minor = (minorExact + 7) & ~7; + major = maximumSize; + padding = minor - minorExact; + } + w = portrait ? minor : major; + h = portrait ? major : minor; + } + return new ScreenInfo(deviceSize, new Size(w, h), padding, rotated); } - private ScreenInfo readScreenInfo() { - return serviceManager.getDisplayManager().getScreenInfo(); + public Point getPhysicalPoint(Position position) { + ScreenInfo screenInfo = getScreenInfo(); // read with synchronization + Size videoSize = screenInfo.getVideoSize(); + Size clientVideoSize = position.getScreenSize(); + if (!videoSize.equals(clientVideoSize)) { + // The client sends a click relative to a video with wrong dimensions, + // the device may have been rotated since the event was generated, so ignore the event + return null; + } + Size deviceSize = screenInfo.getDeviceSize(); + int xPadding = screenInfo.getXPadding(); + int yPadding = screenInfo.getYPadding(); + int contentWidth = videoSize.getWidth() - xPadding; + int contentHeight = videoSize.getHeight() - yPadding; + Point point = position.getPoint(); + int x = point.getX() - xPadding / 2; + int y = point.getY() - yPadding / 2; + if (x < 0 || x >= contentWidth || y < 0 || y >= contentHeight) { + // out of screen + return null; + } + int scaledX = x * deviceSize.getWidth() / videoSize.getWidth(); + int scaledY = y * deviceSize.getHeight() / videoSize.getHeight(); + return new Point(scaledX, scaledY); } public static String getDeviceName() { diff --git a/server/src/com/genymobile/scrcpy/DisplayInfo.java b/server/src/com/genymobile/scrcpy/DisplayInfo.java new file mode 100644 index 00000000..639869b5 --- /dev/null +++ b/server/src/com/genymobile/scrcpy/DisplayInfo.java @@ -0,0 +1,20 @@ +package com.genymobile.scrcpy; + +public final class DisplayInfo { + private final Size size; + private final int rotation; + + public DisplayInfo(Size size, int rotation) { + this.size = size; + this.rotation = rotation; + } + + public Size getSize() { + return size; + } + + public int getRotation() { + return rotation; + } +} + diff --git a/server/src/com/genymobile/scrcpy/Options.java b/server/src/com/genymobile/scrcpy/Options.java new file mode 100644 index 00000000..f37ed7fb --- /dev/null +++ b/server/src/com/genymobile/scrcpy/Options.java @@ -0,0 +1,13 @@ +package com.genymobile.scrcpy; + +public class Options { + private int maximumSize; + + public int getMaximumSize() { + return maximumSize; + } + + public void setMaximumSize(int maximumSize) { + this.maximumSize = maximumSize; + } +} diff --git a/server/src/com/genymobile/scrcpy/Point.java b/server/src/com/genymobile/scrcpy/Point.java index 22ed3309..f02b22a0 100644 --- a/server/src/com/genymobile/scrcpy/Point.java +++ b/server/src/com/genymobile/scrcpy/Point.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy; +import java.util.Objects; + public class Point { private int x; private int y; @@ -17,6 +19,20 @@ public class Point { return y; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Point point = (Point) o; + return x == point.x && + y == point.y; + } + + @Override + public int hashCode() { + return Objects.hash(x, y); + } + @Override public String toString() { return "Point{" + diff --git a/server/src/com/genymobile/scrcpy/Position.java b/server/src/com/genymobile/scrcpy/Position.java index 9b6495d8..3e7f814a 100644 --- a/server/src/com/genymobile/scrcpy/Position.java +++ b/server/src/com/genymobile/scrcpy/Position.java @@ -1,42 +1,48 @@ package com.genymobile.scrcpy; +import java.util.Objects; + public class Position { + private Point point; + private Size screenSize; - private int x; - private int y; - private int screenWidth; - private int screenHeight; + public Position(Point point, Size screenSize) { + this.point = point; + this.screenSize = screenSize; + } public Position(int x, int y, int screenWidth, int screenHeight) { - this.x = x; - this.y = y; - this.screenWidth = screenWidth; - this.screenHeight = screenHeight; + this(new Point(x, y), new Size(screenWidth, screenHeight)); } - public int getX() { - return x; + public Point getPoint() { + return point; } - public int getY() { - return y; + public Size getScreenSize() { + return screenSize; } - public int getScreenWidth() { - return screenWidth; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Position position = (Position) o; + return Objects.equals(point, position.point) && + Objects.equals(screenSize, position.screenSize); } - public int getScreenHeight() { - return screenHeight; + @Override + public int hashCode() { + return Objects.hash(point, screenSize); } @Override public String toString() { - return "Point{" + - "x=" + x + - ", y=" + y + - ", screenWidth=" + screenWidth + - ", screenHeight=" + screenHeight + + return "Position{" + + "point=" + point + + ", screenSize=" + screenSize + '}'; } + } diff --git a/server/src/com/genymobile/scrcpy/ScrCpyServer.java b/server/src/com/genymobile/scrcpy/ScrCpyServer.java index c5ef3b9b..8089d6c5 100644 --- a/server/src/com/genymobile/scrcpy/ScrCpyServer.java +++ b/server/src/com/genymobile/scrcpy/ScrCpyServer.java @@ -6,10 +6,10 @@ public class ScrCpyServer { private static final String TAG = "scrcpy"; - private static void scrcpy() throws IOException { - final Device device = new Device(); + private static void scrcpy(Options options) throws IOException { + final Device device = new Device(options); try (DesktopConnection connection = DesktopConnection.open(device)) { - final ScreenStreamer streamer = new ScreenStreamer(connection); + final ScreenStreamer streamer = new ScreenStreamer(device, connection); device.setRotationListener(new Device.RotationListener() { @Override public void onRotationChanged(int rotation) { @@ -43,8 +43,13 @@ public class ScrCpyServer { } public static void main(String... args) throws Exception { + Options options = new Options(); + if (args.length > 0) { + int maximumSize = Integer.parseInt(args[0]) & ~7; // multiple of 8 + options.setMaximumSize(maximumSize); + } try { - scrcpy(); + scrcpy(options); } catch (Throwable t) { t.printStackTrace(); throw t; diff --git a/server/src/com/genymobile/scrcpy/ScreenInfo.java b/server/src/com/genymobile/scrcpy/ScreenInfo.java index 312142e1..827e9ee8 100644 --- a/server/src/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/com/genymobile/scrcpy/ScreenInfo.java @@ -1,26 +1,39 @@ package com.genymobile.scrcpy; -public class ScreenInfo { - private final int width; - private final int height; - private int rotation; +public final class ScreenInfo { + private final Size deviceSize; + private final Size videoSize; + private final int padding; // padding inside the video stream, along the smallest dimension + private final boolean rotated; - public ScreenInfo(int width, int height, int rotation) { - this.width = width; - this.height = height; - this.rotation = rotation; + public ScreenInfo(Size deviceSize, Size videoSize, int padding, boolean rotated) { + this.deviceSize = deviceSize; + this.videoSize = videoSize; + this.padding = padding; + this.rotated = rotated; } - public ScreenInfo withRotation(int rotation) { - return new ScreenInfo(width, height, rotation); + public Size getDeviceSize() { + return deviceSize; } - public int getLogicalWidth() { - return (rotation & 1) == 0 ? width : height; + public Size getVideoSize() { + return videoSize; } - public int getLogicalHeight() { - return (rotation & 1) == 0 ? height : width; + public int getXPadding() { + return videoSize.getWidth() < videoSize.getHeight() ? padding : 0; + } + + public int getYPadding() { + return videoSize.getHeight() < videoSize.getWidth() ? padding : 0; } -} + public ScreenInfo withRotation(int rotation) { + boolean newRotated = (rotation & 1) != 0; + if (rotated == newRotated) { + return this; + } + return new ScreenInfo(deviceSize.rotate(), videoSize.rotate(), padding, newRotated); + } +} diff --git a/server/src/com/genymobile/scrcpy/ScreenStreamer.java b/server/src/com/genymobile/scrcpy/ScreenStreamer.java index a48f41bb..38509105 100644 --- a/server/src/com/genymobile/scrcpy/ScreenStreamer.java +++ b/server/src/com/genymobile/scrcpy/ScreenStreamer.java @@ -5,15 +5,17 @@ import java.io.InterruptedIOException; public class ScreenStreamer { + private final Device device; private final DesktopConnection connection; private ScreenStreamerSession currentStreamer; // protected by 'this' - public ScreenStreamer(DesktopConnection connection) { + public ScreenStreamer(Device device, DesktopConnection connection) { + this.device = device; this.connection = connection; } private synchronized ScreenStreamerSession newScreenStreamerSession() { - currentStreamer = new ScreenStreamerSession(connection); + currentStreamer = new ScreenStreamerSession(device, connection); return currentStreamer; } diff --git a/server/src/com/genymobile/scrcpy/ScreenStreamerSession.java b/server/src/com/genymobile/scrcpy/ScreenStreamerSession.java index 99b2a03a..9ab0365a 100644 --- a/server/src/com/genymobile/scrcpy/ScreenStreamerSession.java +++ b/server/src/com/genymobile/scrcpy/ScreenStreamerSession.java @@ -2,16 +2,20 @@ package com.genymobile.scrcpy; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; public class ScreenStreamerSession { + private final Device device; private final DesktopConnection connection; private Process screenRecordProcess; // protected by 'this' private final AtomicBoolean stopped = new AtomicBoolean(); private final byte[] buffer = new byte[0x10000]; - public ScreenStreamerSession(DesktopConnection connection) { + public ScreenStreamerSession(Device device, DesktopConnection connection) { + this.device = device; this.connection = connection; } @@ -28,7 +32,8 @@ public class ScreenStreamerSession { */ private boolean streamScreenOnce() throws IOException { Ln.d("Recording..."); - Process process = startScreenRecord(); + Size videoSize = device.getScreenInfo().getVideoSize(); + Process process = startScreenRecord(videoSize); setCurrentProcess(process); InputStream inputStream = process.getInputStream(); int r; @@ -44,8 +49,15 @@ public class ScreenStreamerSession { killCurrentProcess(); } - private static Process startScreenRecord() throws IOException { - Process process = new ProcessBuilder("screenrecord", "--output-format=h264", "-").start(); + private static Process startScreenRecord(Size videoSize) throws IOException { + List command = new ArrayList<>(); + command.add("screenrecord"); + command.add("--output-format=h264"); + if (videoSize != null) { + command.add("--size=" + videoSize.getWidth() + "x" + videoSize.getHeight()); + } + command.add("-"); + Process process = new ProcessBuilder(command).start(); process.getOutputStream().close(); return process; } diff --git a/server/src/com/genymobile/scrcpy/Size.java b/server/src/com/genymobile/scrcpy/Size.java new file mode 100644 index 00000000..66d67b37 --- /dev/null +++ b/server/src/com/genymobile/scrcpy/Size.java @@ -0,0 +1,47 @@ +package com.genymobile.scrcpy; + +import java.util.Objects; + +public final class Size { + private final int width; + private final int height; + + public Size(int width, int height) { + this.width = width; + this.height = height; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public Size rotate() { + return new Size(height, width); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Size size = (Size) o; + return width == size.width && + height == size.height; + } + + @Override + public int hashCode() { + return Objects.hash(width, height); + } + + @Override + public String toString() { + return "Size{" + + "width=" + width + + ", height=" + height + + '}'; + } +} diff --git a/server/src/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/com/genymobile/scrcpy/wrappers/DisplayManager.java index 4fddde23..99dcf258 100644 --- a/server/src/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -2,7 +2,8 @@ package com.genymobile.scrcpy.wrappers; import android.os.IInterface; -import com.genymobile.scrcpy.ScreenInfo; +import com.genymobile.scrcpy.DisplayInfo; +import com.genymobile.scrcpy.Size; public class DisplayManager { private final IInterface manager; @@ -11,15 +12,15 @@ public class DisplayManager { this.manager = manager; } - public ScreenInfo getScreenInfo() { + public DisplayInfo getDisplayInfo() { try { Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0); Class cls = displayInfo.getClass(); - // width and height do not depend on the rotation - int width = (Integer) cls.getMethod("getNaturalWidth").invoke(displayInfo); - int height = (Integer) cls.getMethod("getNaturalHeight").invoke(displayInfo); + // 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 ScreenInfo(width, height, rotation); + return new DisplayInfo(new Size(width, height), rotation); } catch (Exception e) { throw new AssertionError(e); }