Compare commits

..

18 Commits
v1.2 ... v1.3

Author SHA1 Message Date
Romain Vimont
7e42358a7b Bump version to 1.3 2018-08-09 19:14:17 +02:00
Romain Vimont
caa9e30004 Add crop feature
Add an option to crop the screen on the server. This allows to mirror
only part of the device screen.
2018-08-09 19:12:27 +02:00
Romain Vimont
e85010fbc2 Move annotation comment
This confused the Android Studio code formatter.
2018-08-09 18:23:38 +02:00
Romain Vimont
820cd2bb54 Extract video size computation
One method, one thing.
2018-08-09 18:22:52 +02:00
Romain Vimont
8793c104ee Increase "adb forward" connection attempts
5 seconds might not be sufficient:
<https://github.com/Genymobile/scrcpy/issues/213>

Increase to 10 seconds (it does not harm).
2018-08-09 18:22:42 +02:00
Romain Vimont
fca806e095 Do not call deprecated av_register_all()
av_register_all() is deprecated in FFmpeg since this commit:
<http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=0694d8702421e7aff1340038559c438b61bb30dd>

It is now useless to call it:
<https://ffmpeg.org/pipermail/ffmpeg-devel/2018-February/225051.html>

Fixes <https://github.com/Genymobile/scrcpy/issues/203>.
2018-08-09 18:18:22 +02:00
Romain Vimont
280891837e Simplify README for Windows users
So that users stop downloading platform-tools unnecessarily.
2018-07-16 17:13:36 +02:00
Romain Vimont
49b2e63d13 Forward repeated volume events
Send repeated events when holding volume up/down shortcuts.
2018-06-24 21:51:54 +02:00
Romain Vimont
c12c64ed41 Send separate DOWN/UP key events
Shortcuts generated instant DOWN/UP key events. Instead, generate DOWN
event on Ctrl+key down and UP event on Ctrl+key up.

Fixes <https://github.com/Genymobile/scrcpy/issues/166>.
2018-06-24 21:50:53 +02:00
Romain Vimont
2f66acd75d Improve English comment
Replace "implying" by "involving" (both "impliquant" in French).
2018-06-24 20:46:14 +02:00
Romain Vimont
1846d2f078 Prevent killing unexpected process
A missing initialization (fixed by the previous commit) leaded to kill
unexpected process.

In order to prevent consequences of similar errors in the future, never
call kill() with a non-positive PID.

See <https://github.com/Genymobile/scrcpy/issues/182>.
2018-06-22 19:56:58 +02:00
Romain Vimont
1a0139321b Fix missing installer initialization
The current_process field of struct installer was not initialized.
Since the installer instance is static, its default value was 0.

The call to installer_stop() then called kill(0, SIGTERM) (on Linux),
which sent SIGTERM to every process in the process group. In particular,
the scrcpy process was killed.

As a consequence, the last cleanup steps, like disabling "show touches",
were not executed.

Fixes <https://github.com/Genymobile/scrcpy/issues/183>.
2018-06-22 18:35:58 +02:00
Romain Vimont
8890750681 Merge pull request #169 from zopelee/master
Fix meson error: ‘for’ loop initial declarations are only allowed in …
2018-06-08 08:08:00 +02:00
zack
aac9d5057f Fix meson error: ‘for’ loop initial declarations are only allowed in C99 mode. 2018-06-08 12:21:04 +08:00
Romain Vimont
f705a73149 Use a meson option to crossbuild for Windows
Meson decided to crossbuild for Windows as soon as
meson.is_cross_build() returned true. This made non-Windows crossbuilds
fail.

Instead, add an explicit option "crossbuild_windows".

Fixes <https://github.com/Genymobile/scrcpy/issues/165>.
2018-06-05 20:45:41 +02:00
Romain Vimont
27a9bd3424 Fix clean recipe in cross Makefile
Make the "clean" recipe also remove the noconsole build directories.
2018-06-05 20:42:35 +02:00
Romain Vimont
cf121a0f65 Upgrade gradle 2018-06-05 20:33:37 +02:00
Romain Vimont
c8a5f9dc63 Update links to v1.2 in README 2018-05-28 22:52:52 +02:00
21 changed files with 226 additions and 98 deletions

View File

@@ -38,7 +38,8 @@ release: clean zip-win32 zip-win64 sums
clean: clean:
$(GRADLE) clean $(GRADLE) clean
rm -rf "$(SERVER_BUILD_DIR)" "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" "$(DIST)" rm -rf "$(SERVER_BUILD_DIR)" "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" \
"$(WIN32_NOCONSOLE_BUILD_DIR)" "$(WIN64_NOCONSOLE_BUILD_DIR)" "$(DIST)"
build-server: build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
@@ -54,6 +55,7 @@ build-win32: prepare-deps-win32
meson "$(WIN32_BUILD_DIR)" \ meson "$(WIN32_BUILD_DIR)" \
--cross-file cross_win32.txt \ --cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \ --buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \ -Dbuild_server=false \
-Doverride_server_path=scrcpy-server.jar ) -Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN32_BUILD_DIR)" ninja -C "$(WIN32_BUILD_DIR)"
@@ -63,6 +65,7 @@ build-win32-noconsole: prepare-deps-win32
meson "$(WIN32_NOCONSOLE_BUILD_DIR)" \ meson "$(WIN32_NOCONSOLE_BUILD_DIR)" \
--cross-file cross_win32.txt \ --cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \ --buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \ -Dbuild_server=false \
-Dwindows_noconsole=true \ -Dwindows_noconsole=true \
-Doverride_server_path=scrcpy-server.jar ) -Doverride_server_path=scrcpy-server.jar )
@@ -76,6 +79,7 @@ build-win64: prepare-deps-win64
meson "$(WIN64_BUILD_DIR)" \ meson "$(WIN64_BUILD_DIR)" \
--cross-file cross_win64.txt \ --cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \ --buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \ -Dbuild_server=false \
-Doverride_server_path=scrcpy-server.jar ) -Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN64_BUILD_DIR)" ninja -C "$(WIN64_BUILD_DIR)"
@@ -85,6 +89,7 @@ build-win64-noconsole: prepare-deps-win64
meson "$(WIN64_NOCONSOLE_BUILD_DIR)" \ meson "$(WIN64_NOCONSOLE_BUILD_DIR)" \
--cross-file cross_win64.txt \ --cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \ --buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \ -Dbuild_server=false \
-Dwindows_noconsole=true \ -Dwindows_noconsole=true \
-Doverride_server_path=scrcpy-server.jar ) -Doverride_server_path=scrcpy-server.jar )

View File

@@ -1,4 +1,4 @@
# scrcpy # scrcpy (v1.2)
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.
@@ -14,12 +14,7 @@ The Android part requires at least API 21 (Android 5.0).
You need [adb]. It is available in the [Android SDK platform You need [adb]. It is available in the [Android SDK platform
tools][platform-tools], or packaged in your distribution (`android-adb-tools`). tools][platform-tools], or packaged in your distribution (`android-adb-tools`).
On Windows, if you use the [prebuilt application](#windows), it is already On Windows, just [download scrcpy for Windows](#windows), `adb` is included.
included. Otherwise, just download the [platform-tools][platform-tools-windows]
and extract the following files to a directory accessible from your `PATH`:
- `adb.exe`
- `AdbWinApi.dll`
- `AdbWinUsbApi.dll`
Make sure you [enabled adb debugging][enable-adb] on your device(s). Make sure you [enabled adb debugging][enable-adb] on your device(s).
@@ -81,18 +76,25 @@ Two [AUR] packages have been created by users:
#### Windows #### Windows
For Windows, for simplicity, a prebuilt archive with all the dependencies For Windows, for simplicity, prebuilt archives with all the dependencies
(including `adb`) is available: (including `adb`) are available:
- [`scrcpy-windows-with-deps-v1.1.zip`][direct-windows-with-deps]. - [`scrcpy-win32-v1.2.zip`][direct-win32].
_(SHA-256: 27eb36c15937601d1062c1dc0b45faae0e06fefea2019aadeb4fa7f76a07bb4c)_ _(SHA-256: a1fe1de67ec75dcf970ca5d97a04c26ff0f2d61871f2ef51b6f2f0bf666966b2)_
- [`scrcpy-win64-v1.2.zip`][direct-win64].
_(SHA-256: 35ae3bcee51771e7c51b8a8be87aef2295c9f267606a7cf83ebb0a4d583ef536)_
[direct-windows-with-deps]: https://github.com/Genymobile/scrcpy/releases/download/v1.1/scrcpy-windows-with-deps-v1.1.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.2/scrcpy-win32-v1.2.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.2/scrcpy-win64-v1.2.zip
_(It's just a portable version including _dll_ copied from MSYS2.)_
Instead, you may want to build it manually. Instead, you may want to build it manually.
In that case, download the [platform-tools][platform-tools-windows] and extract
the following files to a directory accessible from your `PATH`:
- `adb.exe`
- `AdbWinApi.dll`
- `AdbWinUsbApi.dll`
##### Cross-compile from Linux ##### Cross-compile from Linux
This is the preferred method (and the way the release is built). This is the preferred method (and the way the release is built).
@@ -245,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.1.jar`][direct-scrcpy-server]. - [`scrcpy-server-v1.2.jar`][direct-scrcpy-server].
_(SHA-256: 14826512bf38447ec94adf3b531676ce038d19e7e06757fb4e537882b17e77b3)_ _(SHA-256: cb39654ed2fda3d30ddff292806950ccc5c394375ea12b974f790c7f38f61f60)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.1/scrcpy-server-v1.1.jar [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.2/scrcpy-server-v1.2.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.
@@ -292,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

@@ -19,7 +19,7 @@ src = [
'src/tinyxpm.c', 'src/tinyxpm.c',
] ]
if not meson.is_cross_build() if not get_option('crossbuild_windows')
# native build # native build
dependencies = [ dependencies = [
@@ -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

@@ -29,52 +29,58 @@ static struct point get_mouse_point(struct screen *screen) {
}; };
} }
static void send_keycode(struct controller *controller, enum android_keycode keycode, const char *name) { static const int ACTION_DOWN = 1;
static const int ACTION_UP = 1 << 1;
static void send_keycode(struct controller *controller, enum android_keycode keycode, int actions, const char *name) {
// send DOWN event // send DOWN event
struct control_event control_event; struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_KEYCODE; control_event.type = CONTROL_EVENT_TYPE_KEYCODE;
control_event.keycode_event.action = AKEY_EVENT_ACTION_DOWN;
control_event.keycode_event.keycode = keycode; control_event.keycode_event.keycode = keycode;
control_event.keycode_event.metastate = 0; control_event.keycode_event.metastate = 0;
if (!controller_push_event(controller, &control_event)) { if (actions & ACTION_DOWN) {
LOGW("Cannot send %s (DOWN)", name); control_event.keycode_event.action = AKEY_EVENT_ACTION_DOWN;
return; if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot send %s (DOWN)", name);
return;
}
} }
// send UP event if (actions & ACTION_UP) {
control_event.keycode_event.action = AKEY_EVENT_ACTION_UP; control_event.keycode_event.action = AKEY_EVENT_ACTION_UP;
if (!controller_push_event(controller, &control_event)) { if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot send %s (UP)", name); LOGW("Cannot send %s (UP)", name);
}
} }
} }
static inline void action_home(struct controller *controller) { static inline void action_home(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_HOME, "HOME"); send_keycode(controller, AKEYCODE_HOME, actions, "HOME");
} }
static inline void action_back(struct controller *controller) { static inline void action_back(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_BACK, "BACK"); send_keycode(controller, AKEYCODE_BACK, actions, "BACK");
} }
static inline void action_app_switch(struct controller *controller) { static inline void action_app_switch(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_APP_SWITCH, "APP_SWITCH"); send_keycode(controller, AKEYCODE_APP_SWITCH, actions, "APP_SWITCH");
} }
static inline void action_power(struct controller *controller) { static inline void action_power(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_POWER, "POWER"); send_keycode(controller, AKEYCODE_POWER, actions, "POWER");
} }
static inline void action_volume_up(struct controller *controller) { static inline void action_volume_up(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_VOLUME_UP, "VOLUME_UP"); send_keycode(controller, AKEYCODE_VOLUME_UP, actions, "VOLUME_UP");
} }
static inline void action_volume_down(struct controller *controller) { static inline void action_volume_down(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_VOLUME_DOWN, "VOLUME_DOWN"); send_keycode(controller, AKEYCODE_VOLUME_DOWN, actions, "VOLUME_DOWN");
} }
static inline void action_menu(struct controller *controller) { static inline void action_menu(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_MENU, "MENU"); send_keycode(controller, AKEYCODE_MENU, actions, "MENU");
} }
// turn the screen on if it was off, press BACK otherwise // turn the screen on if it was off, press BACK otherwise
@@ -141,57 +147,74 @@ void input_manager_process_key(struct input_manager *input_manager,
// capture all Ctrl events // capture all Ctrl events
if (ctrl) { if (ctrl) {
SDL_bool repeat = event->repeat;
// only consider keydown events, and ignore repeated events
if (repeat || event->type != SDL_KEYDOWN) {
return;
}
SDL_bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT); SDL_bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
if (shift) { if (shift) {
// currently, there is no shortcut implying SHIFT // currently, there is no shortcut involving SHIFT
return; return;
} }
SDL_Keycode keycode = event->keysym.sym; SDL_Keycode keycode = event->keysym.sym;
int action = event->type == SDL_KEYDOWN ? ACTION_DOWN : ACTION_UP;
SDL_bool repeat = event->repeat;
switch (keycode) { switch (keycode) {
case SDLK_h: case SDLK_h:
action_home(input_manager->controller); if (!repeat) {
action_home(input_manager->controller, action);
}
return; return;
case SDLK_b: // fall-through case SDLK_b: // fall-through
case SDLK_BACKSPACE: case SDLK_BACKSPACE:
action_back(input_manager->controller); if (!repeat) {
action_back(input_manager->controller, action);
}
return; return;
case SDLK_s: case SDLK_s:
action_app_switch(input_manager->controller); if (!repeat) {
action_app_switch(input_manager->controller, action);
}
return; return;
case SDLK_m: case SDLK_m:
action_menu(input_manager->controller); if (!repeat) {
action_menu(input_manager->controller, action);
}
return; return;
case SDLK_p: case SDLK_p:
action_power(input_manager->controller); if (!repeat) {
action_power(input_manager->controller, action);
}
return; return;
case SDLK_DOWN: case SDLK_DOWN:
action_volume_down(input_manager->controller); // forward repeated events
action_volume_down(input_manager->controller, action);
return; return;
case SDLK_UP: case SDLK_UP:
action_volume_up(input_manager->controller); // forward repeated events
action_volume_up(input_manager->controller, action);
return; return;
case SDLK_v: case SDLK_v:
clipboard_paste(input_manager->controller); if (!repeat && event->type == SDL_KEYDOWN) {
clipboard_paste(input_manager->controller);
}
return; return;
case SDLK_f: case SDLK_f:
screen_switch_fullscreen(input_manager->screen); if (!repeat && event->type == SDL_KEYDOWN) {
screen_switch_fullscreen(input_manager->screen);
}
return; return;
case SDLK_x: case SDLK_x:
screen_resize_to_fit(input_manager->screen); if (!repeat && event->type == SDL_KEYDOWN) {
screen_resize_to_fit(input_manager->screen);
}
return; return;
case SDLK_g: case SDLK_g:
screen_resize_to_pixel_perfect(input_manager->screen); if (!repeat && event->type == SDL_KEYDOWN) {
screen_resize_to_pixel_perfect(input_manager->screen);
}
return; return;
case SDLK_i: case SDLK_i:
switch_fps_counter_state(input_manager->frames); if (!repeat && event->type == SDL_KEYDOWN) {
switch_fps_counter_state(input_manager->frames);
}
return; return;
} }
@@ -228,7 +251,7 @@ void input_manager_process_mouse_button(struct input_manager *input_manager,
return; return;
} }
if (event->button == SDL_BUTTON_MIDDLE) { if (event->button == SDL_BUTTON_MIDDLE) {
action_home(input_manager->controller); action_home(input_manager->controller, ACTION_DOWN | ACTION_UP);
return; return;
} }
// double-click on black borders resize to fit the device screen // double-click on black borders resize to fit the device screen

View File

@@ -79,6 +79,8 @@ SDL_bool installer_init(struct installer *installer, const char *serial) {
installer->initialized = SDL_FALSE; installer->initialized = SDL_FALSE;
installer->stopped = SDL_FALSE; installer->stopped = SDL_FALSE;
installer->current_process = PROCESS_NONE;
return SDL_TRUE; return SDL_TRUE;
} }

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

@@ -1,9 +1,11 @@
#include "command.h" #include "command.h"
#include <signal.h> #include <signal.h>
#include <stdlib.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <unistd.h> #include <unistd.h>
#include "log.h"
pid_t cmd_execute(const char *path, const char *const argv[]) { pid_t cmd_execute(const char *path, const char *const argv[]) {
pid_t pid = fork(); pid_t pid = fork();
@@ -20,6 +22,10 @@ pid_t cmd_execute(const char *path, const char *const argv[]) {
} }
SDL_bool cmd_terminate(pid_t pid) { SDL_bool cmd_terminate(pid_t pid) {
if (pid <= 0) {
LOGC("Requested to kill %d, this is an error. Please report the bug.\n", (int) pid);
abort();
}
return kill(pid, SIGTERM) != -1; return kill(pid, SIGTERM) != -1;
} }

View File

@@ -7,7 +7,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.0.1' classpath 'com.android.tools.build:gradle:3.1.1'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files

View File

@@ -1,6 +1,6 @@
#Mon Jan 29 16:38:49 CET 2018 #Mon Jun 04 11:48:32 CEST 2018
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

View File

@@ -1,4 +1,4 @@
project('scrcpy', 'c', meson_version: '>= 0.37') project('scrcpy', 'c', meson_version: '>= 0.37', default_options : 'c_std=c99')
if get_option('build_app') if get_option('build_app')
subdir('app') subdir('app')

View File

@@ -1,5 +1,6 @@
option('build_app', type: 'boolean', value: true, description: 'Build the client') option('build_app', type: 'boolean', value: true, description: 'Build the client')
option('build_server', type: 'boolean', value: true, description: 'Build the server') option('build_server', type: 'boolean', value: true, description: 'Build the server')
option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux')
option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)')
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
option('override_server_path', type: 'string', description: 'Hardcoded path to find the server at runtime') option('override_server_path', type: 'string', description: 'Hardcoded path to find the server at runtime')

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,9 +65,31 @@ 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;
}
Rect crop = parseCrop(args[3]);
options.setCrop(crop);
return options; 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() {
@Override @Override