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); }