Merge pull request #1 from Genymobile/master

v 1.3
This commit is contained in:
feiser2016 2018-08-13 10:52:17 +08:00 committed by GitHub
commit a979709986
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 132 additions and 42 deletions

View file

@ -1,4 +1,4 @@
# scrcpy (v1.2) # scrcpy (v1.3)
This application provides display and control of Android devices connected on This application provides display and control of Android devices connected on
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
@ -79,13 +79,13 @@ Two [AUR] packages have been created by users:
For Windows, for simplicity, prebuilt archives with all the dependencies For Windows, for simplicity, prebuilt archives with all the dependencies
(including `adb`) are available: (including `adb`) are available:
- [`scrcpy-win32-v1.2.zip`][direct-win32]. - [`scrcpy-win32-v1.3.zip`][direct-win32].
_(SHA-256: a1fe1de67ec75dcf970ca5d97a04c26ff0f2d61871f2ef51b6f2f0bf666966b2)_ _(SHA-256: 51a2990e631ed469a7a86ff38107d517a91d313fb3f8327eb7bc71dde40870b5)_
- [`scrcpy-win64-v1.2.zip`][direct-win64]. - [`scrcpy-win64-v1.3.zip`][direct-win64].
_(SHA-256: 35ae3bcee51771e7c51b8a8be87aef2295c9f267606a7cf83ebb0a4d583ef536)_ _(SHA-256: 0768a80d3d600d0bbcd220ca150ae88a3a58d1fe85c308a8c61f44480b711e43)_
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.2/scrcpy-win32-v1.2.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.3/scrcpy-win32-v1.3.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.2/scrcpy-win64-v1.2.zip [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.3/scrcpy-win64-v1.3.zip
Instead, you may want to build it manually. Instead, you may want to build it manually.
@ -247,10 +247,10 @@ Since the server binary, that will be pushed to the Android device, does not
depend on your system and architecture, you may want to use the prebuilt binary depend on your system and architecture, you may want to use the prebuilt binary
instead: instead:
- [`scrcpy-server-v1.2.jar`][direct-scrcpy-server]. - [`scrcpy-server-v1.3.jar`][direct-scrcpy-server].
_(SHA-256: cb39654ed2fda3d30ddff292806950ccc5c394375ea12b974f790c7f38f61f60)_ _(SHA-256: 0f9a5a217f33f0ed7a1498ceb3c0cccf31c53533893aa952e674c1571d2740c1)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.2/scrcpy-server-v1.2.jar [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.3/scrcpy-server-v1.3.jar
In that case, the build does not require Java or the Android SDK. In that case, the build does not require Java or the Android SDK.
@ -294,6 +294,12 @@ screen is smaller, or cannot decode such a high definition):
scrcpy -m 1024 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_: If several devices are listed in `adb devices`, you must specify the _serial_:
```bash ```bash

View file

@ -85,7 +85,7 @@ conf = configuration_data()
conf.set('BUILD_DEBUG', get_option('buildtype') == 'debug') conf.set('BUILD_DEBUG', get_option('buildtype') == 'debug')
# the version, updated on release # the version, updated on release
conf.set_quoted('SCRCPY_VERSION', '1.2') conf.set_quoted('SCRCPY_VERSION', '1.3')
# the prefix used during configuration (meson --prefix=PREFIX) # the prefix used during configuration (meson --prefix=PREFIX)
conf.set_quoted('PREFIX', get_option('prefix')) conf.set_quoted('PREFIX', get_option('prefix'))

View file

@ -265,7 +265,7 @@ void input_manager_process_mouse_button(struct input_manager *input_manager,
} }
// otherwise, send the click event to the device // otherwise, send the click event to the device
} }
}; }
struct control_event control_event; struct control_event control_event;
if (mouse_button_from_sdl_to_android(event, input_manager->screen->frame_size, &control_event)) { if (mouse_button_from_sdl_to_android(event, input_manager->screen->frame_size, &control_event)) {
if (!controller_push_event(input_manager->controller, &control_event)) { if (!controller_push_event(input_manager->controller, &control_event)) {

View file

@ -69,6 +69,7 @@ SDL_bool installer_init(struct installer *installer, const char *serial) {
installer->serial = SDL_strdup(serial); installer->serial = SDL_strdup(serial);
if (!installer->serial) { if (!installer->serial) {
LOGW("Cannot strdup serial"); LOGW("Cannot strdup serial");
SDL_DestroyMutex(installer->mutex);
return SDL_FALSE; return SDL_FALSE;
} }
} else { } else {

View file

@ -10,6 +10,7 @@
struct args { struct args {
const char *serial; const char *serial;
const char *crop;
SDL_bool help; SDL_bool help;
SDL_bool version; SDL_bool version;
SDL_bool show_touches; SDL_bool show_touches;
@ -29,6 +30,12 @@ static void usage(const char *arg0) {
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
" Default is %d.\n" " Default is %d.\n"
"\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" " -h, --help\n"
" Print this help.\n" " Print this help.\n"
"\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 SDL_bool parse_args(struct args *args, int argc, char *argv[]) {
static const struct option long_options[] = { static const struct option long_options[] = {
{"bit-rate", required_argument, NULL, 'b'}, {"bit-rate", required_argument, NULL, 'b'},
{"crop", required_argument, NULL, 'c'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{"max-size", required_argument, NULL, 'm'}, {"max-size", required_argument, NULL, 'm'},
{"port", required_argument, NULL, 'p'}, {"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 }, {NULL, 0, NULL, 0 },
}; };
int c; 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) { switch (c) {
case 'b': case 'b':
if (!parse_bit_rate(optarg, &args->bit_rate)) { if (!parse_bit_rate(optarg, &args->bit_rate)) {
return SDL_FALSE; return SDL_FALSE;
} }
break; break;
case 'c':
args->crop = optarg;
break;
case 'h': case 'h':
args->help = SDL_TRUE; args->help = SDL_TRUE;
break; break;
@ -253,6 +264,7 @@ int main(int argc, char *argv[]) {
#endif #endif
struct args args = { struct args args = {
.serial = NULL, .serial = NULL,
.crop = NULL,
.help = SDL_FALSE, .help = SDL_FALSE,
.version = SDL_FALSE, .version = SDL_FALSE,
.show_touches = SDL_FALSE, .show_touches = SDL_FALSE,
@ -274,7 +286,9 @@ int main(int argc, char *argv[]) {
return 0; return 0;
} }
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 9, 100)
av_register_all(); av_register_all();
#endif
if (avformat_network_init()) { if (avformat_network_init()) {
return 1; return 1;
@ -286,6 +300,7 @@ int main(int argc, char *argv[]) {
struct scrcpy_options options = { struct scrcpy_options options = {
.serial = args.serial, .serial = args.serial,
.crop = args.crop,
.port = args.port, .port = args.port,
.max_size = args.max_size, .max_size = args.max_size,
.bit_rate = args.bit_rate, .bit_rate = args.bit_rate,

View file

@ -126,7 +126,7 @@ static void wait_show_touches(process_t process) {
SDL_bool scrcpy(const struct scrcpy_options *options) { SDL_bool scrcpy(const struct scrcpy_options *options) {
if (!server_start(&server, options->serial, options->port, 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; return SDL_FALSE;
} }

View file

@ -5,6 +5,7 @@
struct scrcpy_options { struct scrcpy_options {
const char *serial; const char *serial;
const char *crop;
Uint16 port; Uint16 port;
Uint16 max_size; Uint16 max_size;
Uint32 bit_rate; Uint32 bit_rate;

View file

@ -77,7 +77,8 @@ static SDL_bool disable_tunnel(struct server *server) {
} }
static process_t execute_server(const char *serial, 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 max_size_string[6];
char bit_rate_string[11]; char bit_rate_string[11];
sprintf(max_size_string, "%"PRIu16, max_size); sprintf(max_size_string, "%"PRIu16, max_size);
@ -91,6 +92,7 @@ static process_t execute_server(const char *serial,
max_size_string, max_size_string,
bit_rate_string, bit_rate_string,
tunnel_forward ? "true" : "false", tunnel_forward ? "true" : "false",
crop ? crop : "",
}; };
return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0])); 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, 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; server->local_port = local_port;
if (serial) { 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 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->process == PROCESS_NONE) {
if (!server->tunnel_forward) { if (!server->tunnel_forward) {
close_socket(&server->server_socket); close_socket(&server->server_socket);
@ -207,7 +210,7 @@ socket_t server_connect_to(struct server *server) {
if (!server->tunnel_forward) { if (!server->tunnel_forward) {
server->device_socket = net_accept(server->server_socket); server->device_socket = net_accept(server->server_socket);
} else { } else {
Uint32 attempts = 50; Uint32 attempts = 100;
Uint32 delay = 100; // ms Uint32 delay = 100; // ms
server->device_socket = connect_to_server(server->local_port, attempts, delay); server->device_socket = connect_to_server(server->local_port, attempts, delay);
} }

View file

@ -31,7 +31,7 @@ void server_init(struct server *server);
// push, enable tunnel et start the server // push, enable tunnel et start the server
SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port, 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 // block until the communication with the server is established
socket_t server_connect_to(struct server *server); socket_t server_connect_to(struct server *server);

View file

@ -6,8 +6,8 @@ android {
applicationId "com.genymobile.scrcpy" applicationId "com.genymobile.scrcpy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 27 targetSdkVersion 27
versionCode 3 versionCode 4
versionName "1.2" versionName "1.3"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {

View file

@ -3,6 +3,7 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.Rect;
import android.os.Build; import android.os.Build;
import android.os.RemoteException; import android.os.RemoteException;
import android.view.IRotationWatcher; import android.view.IRotationWatcher;
@ -20,7 +21,7 @@ public final class Device {
private RotationListener rotationListener; private RotationListener rotationListener;
public Device(Options options) { public Device(Options options) {
screenInfo = computeScreenInfo(options.getMaxSize()); screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize());
registerRotationWatcher(new IRotationWatcher.Stub() { registerRotationWatcher(new IRotationWatcher.Stub() {
@Override @Override
public void onRotationChanged(int rotation) throws RemoteException { public void onRotationChanged(int rotation) throws RemoteException {
@ -40,18 +41,40 @@ public final class Device {
return screenInfo; return screenInfo;
} }
private ScreenInfo computeScreenInfo(Rect crop, int maxSize) {
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
boolean rotated = (displayInfo.getRotation() & 1) != 0;
Size deviceSize = displayInfo.getSize();
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") @SuppressWarnings("checkstyle:MagicNumber")
private ScreenInfo computeScreenInfo(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. // Compute the video size and the padding of the content inside this video.
// Principle: // Principle:
// - scale down the great side of the screen to maxSize (if necessary); // - scale down the great side of the screen to maxSize (if necessary);
// - scale down the other side so that the aspect ratio is preserved; // - 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) // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8)
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); w &= ~7; // in case it's not a multiple of 8
boolean rotated = (displayInfo.getRotation() & 1) != 0; h &= ~7;
Size deviceSize = displayInfo.getSize();
int w = deviceSize.getWidth() & ~7; // in case it's not a multiple of 8
int h = deviceSize.getHeight() & ~7;
if (maxSize > 0) { if (maxSize > 0) {
if (BuildConfig.DEBUG && maxSize % 8 != 0) { if (BuildConfig.DEBUG && maxSize % 8 != 0) {
throw new AssertionError("Max size must be a multiple of 8"); throw new AssertionError("Max size must be a multiple of 8");
@ -68,12 +91,12 @@ public final class Device {
w = portrait ? minor : major; w = portrait ? minor : major;
h = portrait ? major : minor; h = portrait ? major : minor;
} }
Size videoSize = new Size(w, h); return new Size(w, h);
return new ScreenInfo(deviceSize, videoSize, rotated);
} }
public Point getPhysicalPoint(Position position) { public Point getPhysicalPoint(Position position) {
@SuppressWarnings("checkstyle:HiddenField") // it hides the field on purpose, to read it with a lock // it hides the field on purpose, to read it with a lock
@SuppressWarnings("checkstyle:HiddenField")
ScreenInfo screenInfo = getScreenInfo(); // read with synchronization ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
Size videoSize = screenInfo.getVideoSize(); Size videoSize = screenInfo.getVideoSize();
Size clientVideoSize = position.getScreenSize(); Size clientVideoSize = position.getScreenSize();
@ -82,10 +105,10 @@ public final class Device {
// the device may have been rotated since the event was generated, so ignore the event // the device may have been rotated since the event was generated, so ignore the event
return null; return null;
} }
Size deviceSize = screenInfo.getDeviceSize(); Rect contentRect = screenInfo.getContentRect();
Point point = position.getPoint(); Point point = position.getPoint();
int scaledX = point.x * deviceSize.getWidth() / videoSize.getWidth(); int scaledX = contentRect.left + point.x * contentRect.width() / videoSize.getWidth();
int scaledY = point.y * deviceSize.getHeight() / videoSize.getHeight(); int scaledY = contentRect.top + point.y * contentRect.height() / videoSize.getHeight();
return new Point(scaledX, scaledY); return new Point(scaledX, scaledY);
} }
@ -108,4 +131,8 @@ public final class Device {
public synchronized void setRotationListener(RotationListener rotationListener) { public synchronized void setRotationListener(RotationListener rotationListener) {
this.rotationListener = rotationListener; this.rotationListener = rotationListener;
} }
static Rect flipRect(Rect crop) {
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
}
} }

View file

@ -1,9 +1,12 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.graphics.Rect;
public class Options { public class Options {
private int maxSize; private int maxSize;
private int bitRate; private int bitRate;
private boolean tunnelForward; private boolean tunnelForward;
private Rect crop;
public int getMaxSize() { public int getMaxSize() {
return maxSize; return maxSize;
@ -28,4 +31,12 @@ public class Options {
public void setTunnelForward(boolean tunnelForward) { public void setTunnelForward(boolean tunnelForward) {
this.tunnelForward = tunnelForward; this.tunnelForward = tunnelForward;
} }
public Rect getCrop() {
return crop;
}
public void setCrop(Rect crop) {
this.crop = crop;
}
} }

View file

@ -56,12 +56,12 @@ public class ScreenEncoder implements Device.RotationListener {
do { do {
MediaCodec codec = createCodec(); MediaCodec codec = createCodec();
IBinder display = createDisplay(); IBinder display = createDisplay();
Rect deviceRect = device.getScreenInfo().getDeviceSize().toRect(); Rect contentRect = device.getScreenInfo().getContentRect();
Rect videoRect = device.getScreenInfo().getVideoSize().toRect(); Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
setSize(format, videoRect.width(), videoRect.height()); setSize(format, videoRect.width(), videoRect.height());
configure(codec, format); configure(codec, format);
Surface surface = codec.createInputSurface(); Surface surface = codec.createInputSurface();
setDisplaySurface(display, surface, deviceRect, videoRect); setDisplaySurface(display, surface, contentRect, videoRect);
codec.start(); codec.start();
try { try {
alive = encode(codec, outputStream); alive = encode(codec, outputStream);

View file

@ -1,18 +1,20 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.graphics.Rect;
public final class ScreenInfo { public final class ScreenInfo {
private final Size deviceSize; private final Rect contentRect; // device size, possibly cropped
private final Size videoSize; private final Size videoSize;
private final boolean rotated; private final boolean rotated;
public ScreenInfo(Size deviceSize, Size videoSize, boolean rotated) { public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) {
this.deviceSize = deviceSize; this.contentRect = contentRect;
this.videoSize = videoSize; this.videoSize = videoSize;
this.rotated = rotated; this.rotated = rotated;
} }
public Size getDeviceSize() { public Rect getContentRect() {
return deviceSize; return contentRect;
} }
public Size getVideoSize() { public Size getVideoSize() {
@ -24,6 +26,6 @@ public final class ScreenInfo {
if (rotated == newRotated) { if (rotated == newRotated) {
return this; return this;
} }
return new ScreenInfo(deviceSize.rotate(), videoSize.rotate(), newRotated); return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated);
} }
} }

View file

@ -1,5 +1,7 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.graphics.Rect;
import java.io.IOException; import java.io.IOException;
public final class Server { public final class Server {
@ -63,8 +65,30 @@ public final class Server {
boolean tunnelForward = Boolean.parseBoolean(args[2]); boolean tunnelForward = Boolean.parseBoolean(args[2]);
options.setTunnelForward(tunnelForward); options.setTunnelForward(tunnelForward);
if (args.length < 4) {
return options; 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 { public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {