From caa9e30004bcbbb5e063352e9106c5c430b02c08 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 9 Aug 2018 19:12:27 +0200 Subject: [PATCH] Add crop feature Add an option to crop the screen on the server. This allows to mirror only part of the device screen. --- README.md | 6 +++ app/src/main.c | 15 ++++++- app/src/scrcpy.c | 2 +- app/src/scrcpy.h | 1 + app/src/server.c | 9 ++-- app/src/server.h | 2 +- .../java/com/genymobile/scrcpy/Device.java | 42 ++++++++++++++----- .../java/com/genymobile/scrcpy/Options.java | 11 +++++ .../com/genymobile/scrcpy/ScreenEncoder.java | 4 +- .../com/genymobile/scrcpy/ScreenInfo.java | 14 ++++--- .../java/com/genymobile/scrcpy/Server.java | 24 +++++++++++ 11 files changed, 106 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 6f13759f..1cc97598 100644 --- a/README.md +++ b/README.md @@ -294,6 +294,12 @@ screen is smaller, or cannot decode such a high definition): scrcpy -m 1024 ``` +The device screen may be cropped to mirror only part of the screen: + +```bash +scrcpy -c 1224:1440:0:0 # 1224x1440 at offset (0,0) +``` + If several devices are listed in `adb devices`, you must specify the _serial_: ```bash diff --git a/app/src/main.c b/app/src/main.c index 4e4de808..1317da79 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -10,6 +10,7 @@ struct args { const char *serial; + const char *crop; SDL_bool help; SDL_bool version; SDL_bool show_touches; @@ -29,6 +30,12 @@ static void usage(const char *arg0) { " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" " Default is %d.\n" "\n" + " -c, --crop width:height:x:y\n" + " Crop the device screen on the server.\n" + " The values are expressed in the device natural orientation\n" + " (typically, portrait for a phone, landscape for a tablet).\n" + " Any --max-size value is computed on the cropped size.\n" + "\n" " -h, --help\n" " Print this help.\n" "\n" @@ -192,6 +199,7 @@ static SDL_bool parse_port(char *optarg, Uint16 *port) { static SDL_bool parse_args(struct args *args, int argc, char *argv[]) { static const struct option long_options[] = { {"bit-rate", required_argument, NULL, 'b'}, + {"crop", required_argument, NULL, 'c'}, {"help", no_argument, NULL, 'h'}, {"max-size", required_argument, NULL, 'm'}, {"port", required_argument, NULL, 'p'}, @@ -201,13 +209,16 @@ static SDL_bool parse_args(struct args *args, int argc, char *argv[]) { {NULL, 0, NULL, 0 }, }; int c; - while ((c = getopt_long(argc, argv, "b:hm:p:s:tv", long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "b:c:hm:p:s:tv", long_options, NULL)) != -1) { switch (c) { case 'b': if (!parse_bit_rate(optarg, &args->bit_rate)) { return SDL_FALSE; } break; + case 'c': + args->crop = optarg; + break; case 'h': args->help = SDL_TRUE; break; @@ -253,6 +264,7 @@ int main(int argc, char *argv[]) { #endif struct args args = { .serial = NULL, + .crop = NULL, .help = SDL_FALSE, .version = SDL_FALSE, .show_touches = SDL_FALSE, @@ -288,6 +300,7 @@ int main(int argc, char *argv[]) { struct scrcpy_options options = { .serial = args.serial, + .crop = args.crop, .port = args.port, .max_size = args.max_size, .bit_rate = args.bit_rate, diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index b53f3d47..1cc6ee87 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -126,7 +126,7 @@ static void wait_show_touches(process_t process) { SDL_bool scrcpy(const struct scrcpy_options *options) { if (!server_start(&server, options->serial, options->port, - options->max_size, options->bit_rate)) { + options->max_size, options->bit_rate, options->crop)) { return SDL_FALSE; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 583cd608..7ddabf28 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -5,6 +5,7 @@ struct scrcpy_options { const char *serial; + const char *crop; Uint16 port; Uint16 max_size; Uint32 bit_rate; diff --git a/app/src/server.c b/app/src/server.c index 39a5fbc2..91eac7b7 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -77,7 +77,8 @@ static SDL_bool disable_tunnel(struct server *server) { } static process_t execute_server(const char *serial, - Uint16 max_size, Uint32 bit_rate, SDL_bool tunnel_forward) { + Uint16 max_size, Uint32 bit_rate, + const char *crop, SDL_bool tunnel_forward) { char max_size_string[6]; char bit_rate_string[11]; sprintf(max_size_string, "%"PRIu16, max_size); @@ -91,6 +92,7 @@ static process_t execute_server(const char *serial, max_size_string, bit_rate_string, tunnel_forward ? "true" : "false", + crop ? crop : "", }; return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0])); } @@ -147,7 +149,7 @@ void server_init(struct server *server) { } SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port, - Uint16 max_size, Uint32 bit_rate) { + Uint16 max_size, Uint32 bit_rate, const char *crop) { server->local_port = local_port; if (serial) { @@ -188,7 +190,8 @@ SDL_bool server_start(struct server *server, const char *serial, Uint16 local_po } // server will connect to our server socket - server->process = execute_server(serial, max_size, bit_rate, server->tunnel_forward); + server->process = execute_server(serial, max_size, bit_rate, crop, + server->tunnel_forward); if (server->process == PROCESS_NONE) { if (!server->tunnel_forward) { close_socket(&server->server_socket); diff --git a/app/src/server.h b/app/src/server.h index 13541457..2bc3f41f 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -31,7 +31,7 @@ void server_init(struct server *server); // push, enable tunnel et start the server SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port, - Uint16 max_size, Uint32 bit_rate); + Uint16 max_size, Uint32 bit_rate, const char *crop); // block until the communication with the server is established socket_t server_connect_to(struct server *server); diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 124c84a4..d2862acb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.graphics.Point; +import android.graphics.Rect; import android.os.Build; import android.os.RemoteException; import android.view.IRotationWatcher; @@ -20,7 +21,7 @@ public final class Device { private RotationListener rotationListener; public Device(Options options) { - screenInfo = computeScreenInfo(options.getMaxSize()); + screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize()); registerRotationWatcher(new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) throws RemoteException { @@ -40,23 +41,40 @@ public final class Device { return screenInfo; } - private ScreenInfo computeScreenInfo(int maxSize) { + private ScreenInfo computeScreenInfo(Rect crop, int maxSize) { DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); boolean rotated = (displayInfo.getRotation() & 1) != 0; Size deviceSize = displayInfo.getSize(); - Size videoSize = computeVideoSize(deviceSize, maxSize); - return new ScreenInfo(deviceSize, videoSize, rotated); + Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); + if (crop != null) { + if (rotated) { + // the crop (provided by the user) is expressed in the natural orientation + crop = flipRect(crop); + } + if (!contentRect.intersect(crop)) { + // intersect() changes contentRect so that it is intersected with crop + Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); + contentRect = new Rect(); // empty + } + } + + Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); + return new ScreenInfo(contentRect, videoSize, rotated); + } + + private static String formatCrop(Rect rect) { + return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; } @SuppressWarnings("checkstyle:MagicNumber") - private static Size computeVideoSize(Size inputSize, int maxSize) { + private 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); // - scale down the other side so that the aspect ratio is preserved; // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) - int w = inputSize.getWidth() & ~7; // in case it's not a multiple of 8 - int h = inputSize.getHeight() & ~7; + w &= ~7; // in case it's not a multiple of 8 + h &= ~7; if (maxSize > 0) { if (BuildConfig.DEBUG && maxSize % 8 != 0) { throw new AssertionError("Max size must be a multiple of 8"); @@ -87,10 +105,10 @@ public final class Device { // the device may have been rotated since the event was generated, so ignore the event return null; } - Size deviceSize = screenInfo.getDeviceSize(); + Rect contentRect = screenInfo.getContentRect(); Point point = position.getPoint(); - int scaledX = point.x * deviceSize.getWidth() / videoSize.getWidth(); - int scaledY = point.y * deviceSize.getHeight() / videoSize.getHeight(); + int scaledX = contentRect.left + point.x * contentRect.width() / videoSize.getWidth(); + int scaledY = contentRect.top + point.y * contentRect.height() / videoSize.getHeight(); return new Point(scaledX, scaledY); } @@ -113,4 +131,8 @@ public final class Device { public synchronized void setRotationListener(RotationListener rotationListener) { this.rotationListener = rotationListener; } + + static Rect flipRect(Rect crop) { + return new Rect(crop.top, crop.left, crop.bottom, crop.right); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 332463e6..93df896a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -1,9 +1,12 @@ package com.genymobile.scrcpy; +import android.graphics.Rect; + public class Options { private int maxSize; private int bitRate; private boolean tunnelForward; + private Rect crop; public int getMaxSize() { return maxSize; @@ -28,4 +31,12 @@ public class Options { public void setTunnelForward(boolean tunnelForward) { this.tunnelForward = tunnelForward; } + + public Rect getCrop() { + return crop; + } + + public void setCrop(Rect crop) { + this.crop = crop; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 3014397c..e2ee8122 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -56,12 +56,12 @@ public class ScreenEncoder implements Device.RotationListener { do { MediaCodec codec = createCodec(); IBinder display = createDisplay(); - Rect deviceRect = device.getScreenInfo().getDeviceSize().toRect(); + Rect contentRect = device.getScreenInfo().getContentRect(); Rect videoRect = device.getScreenInfo().getVideoSize().toRect(); setSize(format, videoRect.width(), videoRect.height()); configure(codec, format); Surface surface = codec.createInputSurface(); - setDisplaySurface(display, surface, deviceRect, videoRect); + setDisplaySurface(display, surface, contentRect, videoRect); codec.start(); try { alive = encode(codec, outputStream); diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java index 1f752758..f2fce1d6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -1,18 +1,20 @@ package com.genymobile.scrcpy; +import android.graphics.Rect; + public final class ScreenInfo { - private final Size deviceSize; + private final Rect contentRect; // device size, possibly cropped private final Size videoSize; private final boolean rotated; - public ScreenInfo(Size deviceSize, Size videoSize, boolean rotated) { - this.deviceSize = deviceSize; + public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) { + this.contentRect = contentRect; this.videoSize = videoSize; this.rotated = rotated; } - public Size getDeviceSize() { - return deviceSize; + public Rect getContentRect() { + return contentRect; } public Size getVideoSize() { @@ -24,6 +26,6 @@ public final class ScreenInfo { if (rotated == newRotated) { return this; } - return new ScreenInfo(deviceSize.rotate(), videoSize.rotate(), newRotated); + return new ScreenInfo(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 d7294ae0..9fd93863 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy; +import android.graphics.Rect; + import java.io.IOException; public final class Server { @@ -63,9 +65,31 @@ public final class Server { boolean tunnelForward = Boolean.parseBoolean(args[2]); options.setTunnelForward(tunnelForward); + if (args.length < 4) { + return options; + } + Rect crop = parseCrop(args[3]); + options.setCrop(crop); + return options; } + private static Rect parseCrop(String crop) { + if (crop.isEmpty()) { + return null; + } + // input format: "width:height:x:y" + String[] tokens = crop.split(":"); + if (tokens.length != 4) { + throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\""); + } + int width = Integer.parseInt(tokens[0]); + int height = Integer.parseInt(tokens[1]); + int x = Integer.parseInt(tokens[2]); + int y = Integer.parseInt(tokens[3]); + return new Rect(x, y, x + width, y + height); + } + public static void main(String... args) throws Exception { Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override