Merge branch 'master' into master

This commit is contained in:
RaenonX JELLYCAT 2020-08-09 12:21:53 -05:00 committed by GitHub
commit 7047b76e8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 878 additions and 385 deletions

3
.gitignore vendored
View file

@ -1,5 +1,8 @@
build/ build/
/dist/ /dist/
/build-*/
/build_*/
/release-*/
.idea/ .idea/
.gradle/ .gradle/
/x/ /x/

View file

@ -254,10 +254,10 @@ You can then [run](README.md#run) _scrcpy_.
## Prebuilt server ## Prebuilt server
- [`scrcpy-server-v1.14`][direct-scrcpy-server] - [`scrcpy-server-v1.15.1`][direct-scrcpy-server]
_(SHA-256: 1d1b18a2b80e956771fd63b99b414d2d028713a8f12ddfa5a369709ad4295620)_ _(SHA-256: fe06bd6a30da8c89860bf5e16eecce2b5054d4644c84289670ce00ca5d1637c3)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.14/scrcpy-server-v1.14 [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.15.1/scrcpy-server-v1.15.1
Download the prebuilt server somewhere, and specify its path during the Meson Download the prebuilt server somewhere, and specify its path during the Meson
configuration: configuration:

View file

@ -100,11 +100,11 @@ dist-win32: build-server build-win32 build-win32-noconsole
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
@ -115,11 +115,11 @@ dist-win64: build-server build-win64 build-win64-noconsole
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"

View file

@ -164,12 +164,12 @@ if get_option('buildtype') == 'debug'
'src/cli.c', 'src/cli.c',
'src/util/str_util.c', 'src/util/str_util.c',
]], ]],
['test_control_event_serialize', [ ['test_control_msg_serialize', [
'tests/test_control_msg_serialize.c', 'tests/test_control_msg_serialize.c',
'src/control_msg.c', 'src/control_msg.c',
'src/util/str_util.c', 'src/util/str_util.c',
]], ]],
['test_device_event_deserialize', [ ['test_device_msg_deserialize', [
'tests/test_device_msg_deserialize.c', 'tests/test_device_msg_deserialize.c',
'src/device_msg.c', 'src/device_msg.c',
]], ]],
@ -186,7 +186,7 @@ if get_option('buildtype') == 'debug'
exe = executable(t[0], t[1], exe = executable(t[0], t[1],
include_directories: src_dir, include_directories: src_dir,
dependencies: dependencies, dependencies: dependencies,
c_args: ['-DSDL_MAIN_HANDLED']) c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST'])
test(t[0], exe) test(t[0], exe)
endforeach endforeach
endif endif

View file

@ -43,6 +43,10 @@ The values are expressed in the device natural orientation (typically, portrait
.B \-\-max\-size .B \-\-max\-size
value is computed on the cropped size. value is computed on the cropped size.
.TP
.BI "\-\-disable-screensaver"
Disable screensaver while scrcpy is running.
.TP .TP
.BI "\-\-display " id .BI "\-\-display " id
Specify the display id to mirror. Specify the display id to mirror.
@ -92,6 +96,10 @@ Do not display device (only when screen recording is enabled).
.B \-\-no\-mipmaps .B \-\-no\-mipmaps
If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps. If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps.
.TP
.B \-\-no\-key\-repeat
Do not forward repeated key events when a key is held down.
.TP .TP
.BI "\-p, \-\-port " port[:port] .BI "\-p, \-\-port " port[:port]
Set the TCP port (range) used by the client to listen. Set the TCP port (range) used by the client to listen.
@ -145,6 +153,16 @@ Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each incre
.BI "\-s, \-\-serial " number .BI "\-s, \-\-serial " number
The device serial number. Mandatory only if several devices are connected to adb. The device serial number. Mandatory only if several devices are connected to adb.
.TP
.BI "\-\-shortcut\-mod " key[+...]][,...]
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','.
For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctrl+lalt,lsuper".
Default is "lalt,lsuper" (left-Alt or left-Super).
.TP .TP
.B \-S, \-\-turn\-screen\-off .B \-S, \-\-turn\-screen\-off
Turn the device screen off immediately. Turn the device screen off immediately.
@ -203,52 +221,55 @@ Default is 0 (automatic).\n
.SH SHORTCUTS .SH SHORTCUTS
In the following list, MOD is the shortcut modifier. By default, it's (left)
Alt or (left) Super, but it can be configured by \-\-shortcut-mod.
.TP .TP
.B Ctrl+f .B MOD+f
Switch fullscreen mode Switch fullscreen mode
.TP .TP
.B Ctrl+Left .B MOD+Left
Rotate display left Rotate display left
.TP .TP
.B Ctrl+Right .B MOD+Right
Rotate display right Rotate display right
.TP .TP
.B Ctrl+g .B MOD+g
Resize window to 1:1 (pixel\-perfect) Resize window to 1:1 (pixel\-perfect)
.TP .TP
.B Ctrl+x, Double\-click on black borders .B MOD+w, Double\-click on black borders
Resize window to remove black borders Resize window to remove black borders
.TP .TP
.B Ctrl+h, Home, Middle\-click .B MOD+h, Home, Middle\-click
Click on HOME Click on HOME
.TP .TP
.B Ctrl+b, Ctrl+Backspace, Right\-click (when screen is on) .B MOD+b, MOD+Backspace, Right\-click (when screen is on)
Click on BACK Click on BACK
.TP .TP
.B Ctrl+s .B MOD+s
Click on APP_SWITCH Click on APP_SWITCH
.TP .TP
.B Ctrl+m .B MOD+m
Click on MENU Click on MENU
.TP .TP
.B Ctrl+Up .B MOD+Up
Click on VOLUME_UP Click on VOLUME_UP
.TP .TP
.B Ctrl+Down .B MOD+Down
Click on VOLUME_DOWN Click on VOLUME_DOWN
.TP .TP
.B Ctrl+p .B MOD+p
Click on POWER (turn screen on/off) Click on POWER (turn screen on/off)
.TP .TP
@ -256,39 +277,43 @@ Click on POWER (turn screen on/off)
Turn screen on Turn screen on
.TP .TP
.B Ctrl+o .B MOD+o
Turn device screen off (keep mirroring) Turn device screen off (keep mirroring)
.TP .TP
.B Ctrl+Shift+o .B MOD+Shift+o
Turn device screen on Turn device screen on
.TP .TP
.B Ctrl+r .B MOD+r
Rotate device screen Rotate device screen
.TP .TP
.B Ctrl+n .B MOD+n
Expand notification panel Expand notification panel
.TP .TP
.B Ctrl+Shift+n .B MOD+Shift+n
Collapse notification panel Collapse notification panel
.TP .TP
.B Ctrl+c .B Mod+c
Copy device clipboard to computer Copy to clipboard (inject COPY keycode, Android >= 7 only)
.TP .TP
.B Ctrl+v .B Mod+x
Paste computer clipboard to device Cut to clipboard (inject CUT keycode, Android >= 7 only)
.TP .TP
.B Ctrl+Shift+v .B MOD+v
Copy computer clipboard to device (and paste if the device runs Android >= 7) Copy computer clipboard to device, then paste (inject PASTE keycode, Android >= 7 only)
.TP .TP
.B Ctrl+i .B MOD+Shift+v
Inject computer clipboard text as a sequence of key events
.TP
.B MOD+i
Enable/disable FPS counter (print frames/second in logs) Enable/disable FPS counter (print frames/second in logs)
.TP .TP

View file

@ -3,20 +3,16 @@
#include <assert.h> #include <assert.h>
#include <getopt.h> #include <getopt.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h>
#include <unistd.h> #include <unistd.h>
#include "config.h" #include "config.h"
#include "recorder.h" #include "scrcpy.h"
#include "util/log.h" #include "util/log.h"
#include "util/str_util.h" #include "util/str_util.h"
void void
scrcpy_print_usage(const char *arg0) { scrcpy_print_usage(const char *arg0) {
#ifdef __APPLE__
# define CTRL_OR_CMD "Cmd"
#else
# define CTRL_OR_CMD "Ctrl"
#endif
fprintf(stderr, fprintf(stderr,
"Usage: %s [options]\n" "Usage: %s [options]\n"
"\n" "\n"
@ -45,6 +41,9 @@ scrcpy_print_usage(const char *arg0) {
" (typically, portrait for a phone, landscape for a tablet).\n" " (typically, portrait for a phone, landscape for a tablet).\n"
" Any --max-size value is computed on the cropped size.\n" " Any --max-size value is computed on the cropped size.\n"
"\n" "\n"
" --disable-screensaver\n"
" Disable screensaver while scrcpy is running.\n"
"\n"
" --display id\n" " --display id\n"
" Specify the display id to mirror.\n" " Specify the display id to mirror.\n"
"\n" "\n"
@ -93,6 +92,9 @@ scrcpy_print_usage(const char *arg0) {
" mipmaps are automatically generated to improve downscaling\n" " mipmaps are automatically generated to improve downscaling\n"
" quality. This option disables the generation of mipmaps.\n" " quality. This option disables the generation of mipmaps.\n"
"\n" "\n"
" --no-key-repeat\n"
" Do not forward repeated key events when a key is held down.\n"
"\n"
" -p, --port port[:port]\n" " -p, --port port[:port]\n"
" Set the TCP port (range) used by the client to listen.\n" " Set the TCP port (range) used by the client to listen.\n"
" Default is %d:%d.\n" " Default is %d:%d.\n"
@ -139,6 +141,19 @@ scrcpy_print_usage(const char *arg0) {
" The device serial number. Mandatory only if several devices\n" " The device serial number. Mandatory only if several devices\n"
" are connected to adb.\n" " are connected to adb.\n"
"\n" "\n"
" --shortcut-mod key[+...]][,...]\n"
" Specify the modifiers to use for scrcpy shortcuts.\n"
" Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\",\n"
" \"lsuper\" and \"rsuper\".\n"
"\n"
" A shortcut can consist in several keys, separated by '+'.\n"
" Several shortcuts can be specified, separated by ','.\n"
"\n"
" For example, to use either LCtrl+LAlt or LSuper for scrcpy\n"
" shortcuts, pass \"lctrl+lalt,lsuper\".\n"
"\n"
" Default is \"lalt,lsuper\" (left-Alt or left-Super).\n"
"\n"
" -S, --turn-screen-off\n" " -S, --turn-screen-off\n"
" Turn the device screen off immediately.\n" " Turn the device screen off immediately.\n"
"\n" "\n"
@ -186,75 +201,82 @@ scrcpy_print_usage(const char *arg0) {
"\n" "\n"
"Shortcuts:\n" "Shortcuts:\n"
"\n" "\n"
" " CTRL_OR_CMD "+f\n" " In the following list, MOD is the shortcut modifier. By default,\n"
" it's (left) Alt or (left) Super, but it can be configured by\n"
" --shortcut-mod.\n"
"\n"
" MOD+f\n"
" Switch fullscreen mode\n" " Switch fullscreen mode\n"
"\n" "\n"
" " CTRL_OR_CMD "+Left\n" " MOD+Left\n"
" Rotate display left\n" " Rotate display left\n"
"\n" "\n"
" " CTRL_OR_CMD "+Right\n" " MOD+Right\n"
" Rotate display right\n" " Rotate display right\n"
"\n" "\n"
" " CTRL_OR_CMD "+g\n" " MOD+g\n"
" Resize window to 1:1 (pixel-perfect)\n" " Resize window to 1:1 (pixel-perfect)\n"
"\n" "\n"
" " CTRL_OR_CMD "+x\n" " MOD+w\n"
" Double-click on black borders\n" " Double-click on black borders\n"
" Resize window to remove black borders\n" " Resize window to remove black borders\n"
"\n" "\n"
" Ctrl+h\n" " MOD+h\n"
" Middle-click\n" " Middle-click\n"
" Click on HOME\n" " Click on HOME\n"
"\n" "\n"
" " CTRL_OR_CMD "+b\n" " MOD+b\n"
" " CTRL_OR_CMD "+Backspace\n" " MOD+Backspace\n"
" Right-click (when screen is on)\n" " Right-click (when screen is on)\n"
" Click on BACK\n" " Click on BACK\n"
"\n" "\n"
" " CTRL_OR_CMD "+s\n" " MOD+s\n"
" Click on APP_SWITCH\n" " Click on APP_SWITCH\n"
"\n" "\n"
" Ctrl+m\n" " MOD+m\n"
" Click on MENU\n" " Click on MENU\n"
"\n" "\n"
" " CTRL_OR_CMD "+Up\n" " MOD+Up\n"
" Click on VOLUME_UP\n" " Click on VOLUME_UP\n"
"\n" "\n"
" " CTRL_OR_CMD "+Down\n" " MOD+Down\n"
" Click on VOLUME_DOWN\n" " Click on VOLUME_DOWN\n"
"\n" "\n"
" " CTRL_OR_CMD "+p\n" " MOD+p\n"
" Click on POWER (turn screen on/off)\n" " Click on POWER (turn screen on/off)\n"
"\n" "\n"
" Right-click (when screen is off)\n" " Right-click (when screen is off)\n"
" Power on\n" " Power on\n"
"\n" "\n"
" " CTRL_OR_CMD "+o\n" " MOD+o\n"
" Turn device screen off (keep mirroring)\n" " Turn device screen off (keep mirroring)\n"
"\n" "\n"
" " CTRL_OR_CMD "+Shift+o\n" " MOD+Shift+o\n"
" Turn device screen on\n" " Turn device screen on\n"
"\n" "\n"
" " CTRL_OR_CMD "+r\n" " MOD+r\n"
" Rotate device screen\n" " Rotate device screen\n"
"\n" "\n"
" " CTRL_OR_CMD "+n\n" " MOD+n\n"
" Expand notification panel\n" " Expand notification panel\n"
"\n" "\n"
" " CTRL_OR_CMD "+Shift+n\n" " MOD+Shift+n\n"
" Collapse notification panel\n" " Collapse notification panel\n"
"\n" "\n"
" " CTRL_OR_CMD "+c\n" " MOD+c\n"
" Copy device clipboard to computer\n" " Copy to clipboard (inject COPY keycode, Android >= 7 only)\n"
"\n" "\n"
" " CTRL_OR_CMD "+v\n" " MOD+x\n"
" Paste computer clipboard to device\n" " Cut to clipboard (inject CUT keycode, Android >= 7 only)\n"
"\n" "\n"
" " CTRL_OR_CMD "+Shift+v\n" " MOD+v\n"
" Copy computer clipboard to device (and paste if the device\n" " Copy computer clipboard to device, then paste (inject PASTE\n"
" runs Android >= 7)\n" " keycode, Android >= 7 only)\n"
"\n" "\n"
" " CTRL_OR_CMD "+i\n" " MOD+Shift+v\n"
" Inject computer clipboard text as a sequence of key events\n"
"\n"
" MOD+i\n"
" Enable/disable FPS counter (print frames/second in logs)\n" " Enable/disable FPS counter (print frames/second in logs)\n"
"\n" "\n"
" Drag & drop APK file\n" " Drag & drop APK file\n"
@ -379,10 +401,10 @@ parse_rotation(const char *s, uint8_t *rotation) {
static bool static bool
parse_window_position(const char *s, int16_t *position) { parse_window_position(const char *s, int16_t *position) {
// special value for "auto" // special value for "auto"
static_assert(WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value"); static_assert(SC_WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value");
if (!strcmp(s, "auto")) { if (!strcmp(s, "auto")) {
*position = WINDOW_POSITION_UNDEFINED; *position = SC_WINDOW_POSITION_UNDEFINED;
return true; return true;
} }
@ -411,7 +433,7 @@ parse_window_dimension(const char *s, uint16_t *dimension) {
} }
static bool static bool
parse_port_range(const char *s, struct port_range *port_range) { parse_port_range(const char *s, struct sc_port_range *port_range) {
long values[2]; long values[2];
size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port"); size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port");
if (!count) { if (!count) {
@ -476,21 +498,116 @@ parse_log_level(const char *s, enum sc_log_level *log_level) {
return false; return false;
} }
// item is a list of mod keys separated by '+' (e.g. "lctrl+lalt")
// returns a bitwise-or of SC_MOD_* constants (or 0 on error)
static unsigned
parse_shortcut_mods_item(const char *item, size_t len) {
unsigned mod = 0;
for (;;) {
char *plus = strchr(item, '+');
// strchr() does not consider the "len" parameter, to it could find an
// occurrence too far in the string (there is no strnchr())
bool has_plus = plus && plus < item + len;
assert(!has_plus || plus > item);
size_t key_len = has_plus ? (size_t) (plus - item) : len;
#define STREQ(literal, s, len) \
((sizeof(literal)-1 == len) && !memcmp(literal, s, len))
if (STREQ("lctrl", item, key_len)) {
mod |= SC_MOD_LCTRL;
} else if (STREQ("rctrl", item, key_len)) {
mod |= SC_MOD_RCTRL;
} else if (STREQ("lalt", item, key_len)) {
mod |= SC_MOD_LALT;
} else if (STREQ("ralt", item, key_len)) {
mod |= SC_MOD_RALT;
} else if (STREQ("lsuper", item, key_len)) {
mod |= SC_MOD_LSUPER;
} else if (STREQ("rsuper", item, key_len)) {
mod |= SC_MOD_RSUPER;
} else {
LOGW("Unknown modifier key: %.*s", (int) key_len, item);
return 0;
}
#undef STREQ
if (!has_plus) {
break;
}
item = plus + 1;
assert(len >= key_len + 1);
len -= key_len + 1;
}
return mod;
}
static bool static bool
parse_record_format(const char *optarg, enum recorder_format *format) { parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
unsigned count = 0;
unsigned current = 0;
// LCtrl+LAlt or RCtrl or LCtrl+RSuper: "lctrl+lalt,rctrl,lctrl+rsuper"
for (;;) {
char *comma = strchr(s, ',');
if (comma && count == SC_MAX_SHORTCUT_MODS - 1) {
assert(count < SC_MAX_SHORTCUT_MODS);
LOGW("Too many shortcut modifiers alternatives");
return false;
}
assert(!comma || comma > s);
size_t limit = comma ? (size_t) (comma - s) : strlen(s);
unsigned mod = parse_shortcut_mods_item(s, limit);
if (!mod) {
LOGE("Invalid modifier keys: %.*s", (int) limit, s);
return false;
}
mods->data[current++] = mod;
++count;
if (!comma) {
break;
}
s = comma + 1;
}
mods->count = count;
return true;
}
#ifdef SC_TEST
// expose the function to unit-tests
bool
sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
return parse_shortcut_mods(s, mods);
}
#endif
static bool
parse_record_format(const char *optarg, enum sc_record_format *format) {
if (!strcmp(optarg, "mp4")) { if (!strcmp(optarg, "mp4")) {
*format = RECORDER_FORMAT_MP4; *format = SC_RECORD_FORMAT_MP4;
return true; return true;
} }
if (!strcmp(optarg, "mkv")) { if (!strcmp(optarg, "mkv")) {
*format = RECORDER_FORMAT_MKV; *format = SC_RECORD_FORMAT_MKV;
return true; return true;
} }
LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg);
return false; return false;
} }
static enum recorder_format static enum sc_record_format
guess_record_format(const char *filename) { guess_record_format(const char *filename) {
size_t len = strlen(filename); size_t len = strlen(filename);
if (len < 4) { if (len < 4) {
@ -498,10 +615,10 @@ guess_record_format(const char *filename) {
} }
const char *ext = &filename[len - 4]; const char *ext = &filename[len - 4];
if (!strcmp(ext, ".mp4")) { if (!strcmp(ext, ".mp4")) {
return RECORDER_FORMAT_MP4; return SC_RECORD_FORMAT_MP4;
} }
if (!strcmp(ext, ".mkv")) { if (!strcmp(ext, ".mkv")) {
return RECORDER_FORMAT_MKV; return SC_RECORD_FORMAT_MKV;
} }
return 0; return 0;
} }
@ -526,6 +643,9 @@ guess_record_format(const char *filename) {
#define OPT_NO_MIPMAPS 1017 #define OPT_NO_MIPMAPS 1017
#define OPT_CODEC_OPTIONS 1018 #define OPT_CODEC_OPTIONS 1018
#define OPT_FORCE_ADB_FORWARD 1019 #define OPT_FORCE_ADB_FORWARD 1019
#define OPT_DISABLE_SCREENSAVER 1020
#define OPT_SHORTCUT_MOD 1021
#define OPT_NO_KEY_REPEAT 1022
bool bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
@ -534,6 +654,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
{"bit-rate", required_argument, NULL, 'b'}, {"bit-rate", required_argument, NULL, 'b'},
{"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS}, {"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS},
{"crop", required_argument, NULL, OPT_CROP}, {"crop", required_argument, NULL, OPT_CROP},
{"disable-screensaver", no_argument, NULL,
OPT_DISABLE_SCREENSAVER},
{"display", required_argument, NULL, OPT_DISPLAY_ID}, {"display", required_argument, NULL, OPT_DISPLAY_ID},
{"force-adb-forward", no_argument, NULL, {"force-adb-forward", no_argument, NULL,
OPT_FORCE_ADB_FORWARD}, OPT_FORCE_ADB_FORWARD},
@ -546,6 +668,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
{"no-control", no_argument, NULL, 'n'}, {"no-control", no_argument, NULL, 'n'},
{"no-display", no_argument, NULL, 'N'}, {"no-display", no_argument, NULL, 'N'},
{"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS}, {"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS},
{"no-key-repeat", no_argument, NULL, OPT_NO_KEY_REPEAT},
{"port", required_argument, NULL, 'p'}, {"port", required_argument, NULL, 'p'},
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
{"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"push-target", required_argument, NULL, OPT_PUSH_TARGET},
@ -556,6 +679,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
OPT_RENDER_EXPIRED_FRAMES}, OPT_RENDER_EXPIRED_FRAMES},
{"rotation", required_argument, NULL, OPT_ROTATION}, {"rotation", required_argument, NULL, OPT_ROTATION},
{"serial", required_argument, NULL, 's'}, {"serial", required_argument, NULL, 's'},
{"shortcut-mod", required_argument, NULL, OPT_SHORTCUT_MOD},
{"show-touches", no_argument, NULL, 't'}, {"show-touches", no_argument, NULL, 't'},
{"stay-awake", no_argument, NULL, 'w'}, {"stay-awake", no_argument, NULL, 'w'},
{"turn-screen-off", no_argument, NULL, 'S'}, {"turn-screen-off", no_argument, NULL, 'S'},
@ -710,12 +834,23 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
case OPT_NO_MIPMAPS: case OPT_NO_MIPMAPS:
opts->mipmaps = false; opts->mipmaps = false;
break; break;
case OPT_NO_KEY_REPEAT:
opts->forward_key_repeat = false;
break;
case OPT_CODEC_OPTIONS: case OPT_CODEC_OPTIONS:
opts->codec_options = optarg; opts->codec_options = optarg;
break; break;
case OPT_FORCE_ADB_FORWARD: case OPT_FORCE_ADB_FORWARD:
opts->force_adb_forward = true; opts->force_adb_forward = true;
break; break;
case OPT_DISABLE_SCREENSAVER:
opts->disable_screensaver = true;
break;
case OPT_SHORTCUT_MOD:
if (!parse_shortcut_mods(optarg, &opts->shortcut_mods)) {
return false;
}
break;
default: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
return false; return false;

View file

@ -18,4 +18,9 @@ scrcpy_print_usage(const char *arg0);
bool bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]); scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]);
#ifdef SC_TEST
bool
sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods);
#endif
#endif #endif

View file

@ -20,9 +20,9 @@ write_position(uint8_t *buf, const struct position *position) {
static size_t static size_t
write_string(const char *utf8, size_t max_len, unsigned char *buf) { write_string(const char *utf8, size_t max_len, unsigned char *buf) {
size_t len = utf8_truncation_index(utf8, max_len); size_t len = utf8_truncation_index(utf8, max_len);
buffer_write16be(buf, (uint16_t) len); buffer_write32be(buf, len);
memcpy(&buf[2], utf8, len); memcpy(&buf[4], utf8, len);
return 2 + len; return 4 + len;
} }
static uint16_t static uint16_t
@ -42,8 +42,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
case CONTROL_MSG_TYPE_INJECT_KEYCODE: case CONTROL_MSG_TYPE_INJECT_KEYCODE:
buf[1] = msg->inject_keycode.action; buf[1] = msg->inject_keycode.action;
buffer_write32be(&buf[2], msg->inject_keycode.keycode); buffer_write32be(&buf[2], msg->inject_keycode.keycode);
buffer_write32be(&buf[6], msg->inject_keycode.metastate); buffer_write32be(&buf[6], msg->inject_keycode.repeat);
return 10; buffer_write32be(&buf[10], msg->inject_keycode.metastate);
return 14;
case CONTROL_MSG_TYPE_INJECT_TEXT: { case CONTROL_MSG_TYPE_INJECT_TEXT: {
size_t len = size_t len =
write_string(msg->inject_text.text, write_string(msg->inject_text.text,

View file

@ -10,10 +10,11 @@
#include "android/keycodes.h" #include "android/keycodes.h"
#include "common.h" #include "common.h"
#define CONTROL_MSG_MAX_SIZE (1 << 18) // 256k
#define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 #define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4092 // type: 1 byte; paste flag: 1 byte; length: 4 bytes
#define CONTROL_MSG_SERIALIZED_MAX_SIZE \ #define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 6)
(4 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH)
#define POINTER_ID_MOUSE UINT64_C(-1); #define POINTER_ID_MOUSE UINT64_C(-1);
@ -43,6 +44,7 @@ struct control_msg {
struct { struct {
enum android_keyevent_action action; enum android_keyevent_action action;
enum android_keycode keycode; enum android_keycode keycode;
uint32_t repeat;
enum android_metastate metastate; enum android_metastate metastate;
} inject_keycode; } inject_keycode;
struct { struct {
@ -70,7 +72,7 @@ struct control_msg {
}; };
}; };
// buf size must be at least CONTROL_MSG_SERIALIZED_MAX_SIZE // buf size must be at least CONTROL_MSG_MAX_SIZE
// return the number of bytes written // return the number of bytes written
size_t size_t
control_msg_serialize(const struct control_msg *msg, unsigned char *buf); control_msg_serialize(const struct control_msg *msg, unsigned char *buf);

View file

@ -60,7 +60,7 @@ controller_push_msg(struct controller *controller,
static bool static bool
process_msg(struct controller *controller, process_msg(struct controller *controller,
const struct control_msg *msg) { const struct control_msg *msg) {
unsigned char serialized_msg[CONTROL_MSG_SERIALIZED_MAX_SIZE]; static unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE];
int length = control_msg_serialize(msg, serialized_msg); int length = control_msg_serialize(msg, serialized_msg);
if (!length) { if (!length) {
return false; return false;

View file

@ -9,7 +9,7 @@
ssize_t ssize_t
device_msg_deserialize(const unsigned char *buf, size_t len, device_msg_deserialize(const unsigned char *buf, size_t len,
struct device_msg *msg) { struct device_msg *msg) {
if (len < 3) { if (len < 5) {
// at least type + empty string length // at least type + empty string length
return 0; // not available return 0; // not available
} }
@ -17,8 +17,8 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
msg->type = buf[0]; msg->type = buf[0];
switch (msg->type) { switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD: { case DEVICE_MSG_TYPE_CLIPBOARD: {
uint16_t clipboard_len = buffer_read16be(&buf[1]); size_t clipboard_len = buffer_read32be(&buf[1]);
if (clipboard_len > len - 3) { if (clipboard_len > len - 5) {
return 0; // not available return 0; // not available
} }
char *text = SDL_malloc(clipboard_len + 1); char *text = SDL_malloc(clipboard_len + 1);
@ -27,12 +27,12 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
return -1; return -1;
} }
if (clipboard_len) { if (clipboard_len) {
memcpy(text, &buf[3], clipboard_len); memcpy(text, &buf[5], clipboard_len);
} }
text[clipboard_len] = '\0'; text[clipboard_len] = '\0';
msg->clipboard.text = text; msg->clipboard.text = text;
return 3 + clipboard_len; return 5 + clipboard_len;
} }
default: default:
LOGW("Unknown device message type: %d", (int) msg->type); LOGW("Unknown device message type: %d", (int) msg->type);

View file

@ -7,8 +7,9 @@
#include "config.h" #include "config.h"
#define DEVICE_MSG_TEXT_MAX_LENGTH 4093 #define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k
#define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH) // type: 1 byte; length: 4 bytes
#define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5)
enum device_msg_type { enum device_msg_type {
DEVICE_MSG_TYPE_CLIPBOARD, DEVICE_MSG_TYPE_CLIPBOARD,

View file

@ -92,6 +92,10 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT); MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT);
MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN); MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN);
MAP(SDLK_UP, AKEYCODE_DPAD_UP); MAP(SDLK_UP, AKEYCODE_DPAD_UP);
MAP(SDLK_LCTRL, AKEYCODE_CTRL_LEFT);
MAP(SDLK_RCTRL, AKEYCODE_CTRL_RIGHT);
MAP(SDLK_LSHIFT, AKEYCODE_SHIFT_LEFT);
MAP(SDLK_RSHIFT, AKEYCODE_SHIFT_RIGHT);
} }
if (!(mod & (KMOD_NUM | KMOD_SHIFT))) { if (!(mod & (KMOD_NUM | KMOD_SHIFT))) {
@ -111,8 +115,8 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
} }
} }
if (prefer_text) { if (prefer_text && !(mod & KMOD_CTRL)) {
// do not forward alpha and space key events // do not forward alpha and space key events (unless Ctrl is pressed)
return false; return false;
} }

View file

@ -1,6 +1,7 @@
#include "input_manager.h" #include "input_manager.h"
#include <assert.h> #include <assert.h>
#include <SDL2/SDL_keycode.h>
#include "config.h" #include "config.h"
#include "event_converter.h" #include "event_converter.h"
@ -10,6 +11,67 @@
static const int ACTION_DOWN = 1; static const int ACTION_DOWN = 1;
static const int ACTION_UP = 1 << 1; static const int ACTION_UP = 1 << 1;
#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI)
static inline uint16_t
to_sdl_mod(unsigned mod) {
uint16_t sdl_mod = 0;
if (mod & SC_MOD_LCTRL) {
sdl_mod |= KMOD_LCTRL;
}
if (mod & SC_MOD_RCTRL) {
sdl_mod |= KMOD_RCTRL;
}
if (mod & SC_MOD_LALT) {
sdl_mod |= KMOD_LALT;
}
if (mod & SC_MOD_RALT) {
sdl_mod |= KMOD_RALT;
}
if (mod & SC_MOD_LSUPER) {
sdl_mod |= KMOD_LGUI;
}
if (mod & SC_MOD_RSUPER) {
sdl_mod |= KMOD_RGUI;
}
return sdl_mod;
}
static bool
is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) {
// keep only the relevant modifier keys
sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK;
assert(im->sdl_shortcut_mods.count);
assert(im->sdl_shortcut_mods.count < SC_MAX_SHORTCUT_MODS);
for (unsigned i = 0; i < im->sdl_shortcut_mods.count; ++i) {
if (im->sdl_shortcut_mods.data[i] == sdl_mod) {
return true;
}
}
return false;
}
void
input_manager_init(struct input_manager *im,
const struct scrcpy_options *options)
{
im->control = options->control;
im->forward_key_repeat = options->forward_key_repeat;
im->prefer_text = options->prefer_text;
const struct sc_shortcut_mods *shortcut_mods = &options->shortcut_mods;
assert(shortcut_mods->count);
assert(shortcut_mods->count < SC_MAX_SHORTCUT_MODS);
for (unsigned i = 0; i < shortcut_mods->count; ++i) {
uint16_t sdl_mod = to_sdl_mod(shortcut_mods->data[i]);
assert(sdl_mod);
im->sdl_shortcut_mods.data[i] = sdl_mod;
}
im->sdl_shortcut_mods.count = shortcut_mods->count;
}
static void static void
send_keycode(struct controller *controller, enum android_keycode keycode, send_keycode(struct controller *controller, enum android_keycode keycode,
int actions, const char *name) { int actions, const char *name) {
@ -18,6 +80,7 @@ send_keycode(struct controller *controller, enum android_keycode keycode,
msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE; msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
msg.inject_keycode.keycode = keycode; msg.inject_keycode.keycode = keycode;
msg.inject_keycode.metastate = 0; msg.inject_keycode.metastate = 0;
msg.inject_keycode.repeat = 0;
if (actions & ACTION_DOWN) { if (actions & ACTION_DOWN) {
msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN; msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN;
@ -70,6 +133,16 @@ action_menu(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_MENU, actions, "MENU"); send_keycode(controller, AKEYCODE_MENU, actions, "MENU");
} }
static inline void
action_copy(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_COPY, actions, "COPY");
}
static inline void
action_cut(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_CUT, actions, "CUT");
}
// turn the screen on if it was off, press BACK otherwise // turn the screen on if it was off, press BACK otherwise
static void static void
press_back_or_turn_screen_on(struct controller *controller) { press_back_or_turn_screen_on(struct controller *controller) {
@ -101,16 +174,6 @@ collapse_notification_panel(struct controller *controller) {
} }
} }
static void
request_device_clipboard(struct controller *controller) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request device clipboard");
}
}
static void static void
set_device_clipboard(struct controller *controller, bool paste) { set_device_clipboard(struct controller *controller, bool paste) {
char *text = SDL_GetClipboardText(); char *text = SDL_GetClipboardText();
@ -210,6 +273,10 @@ rotate_client_right(struct screen *screen) {
void void
input_manager_process_text_input(struct input_manager *im, input_manager_process_text_input(struct input_manager *im,
const SDL_TextInputEvent *event) { const SDL_TextInputEvent *event) {
if (is_shortcut_mod(im, SDL_GetModState())) {
// A shortcut must never generate text events
return;
}
if (!im->prefer_text) { if (!im->prefer_text) {
char c = event->text[0]; char c = event->text[0];
if (isalpha(c) || c == ' ') { if (isalpha(c) || c == ' ') {
@ -234,7 +301,7 @@ input_manager_process_text_input(struct input_manager *im,
static bool static bool
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
bool prefer_text) { bool prefer_text, uint32_t repeat) {
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
@ -247,6 +314,7 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
return false; return false;
} }
to->inject_keycode.repeat = repeat;
to->inject_keycode.metastate = convert_meta_state(mod); to->inject_keycode.metastate = convert_meta_state(mod);
return true; return true;
@ -254,74 +322,52 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
void void
input_manager_process_key(struct input_manager *im, input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event, const SDL_KeyboardEvent *event) {
bool control) {
// control: indicates the state of the command-line option --no-control // control: indicates the state of the command-line option --no-control
// ctrl: the Ctrl key bool control = im->control;
bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL); bool smod = is_shortcut_mod(im, event->keysym.mod);
bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI);
// use Cmd on macOS, Ctrl on other platforms
#ifdef __APPLE__
bool cmd = !ctrl && meta;
#else
if (meta) {
// no shortcuts involve Meta on platforms other than macOS, and it must
// not be forwarded to the device
return;
}
bool cmd = ctrl; // && !meta, already guaranteed
#endif
if (alt) {
// no shortcuts involve Alt, and it must not be forwarded to the device
return;
}
struct controller *controller = im->controller; struct controller *controller = im->controller;
// capture all Ctrl events SDL_Keycode keycode = event->keysym.sym;
if (ctrl || cmd) { bool down = event->type == SDL_KEYDOWN;
SDL_Keycode keycode = event->keysym.sym; bool ctrl = event->keysym.mod & KMOD_CTRL;
bool down = event->type == SDL_KEYDOWN; bool shift = event->keysym.mod & KMOD_SHIFT;
bool repeat = event->repeat;
// The shortcut modifier is pressed
if (smod) {
int action = down ? ACTION_DOWN : ACTION_UP; int action = down ? ACTION_DOWN : ACTION_UP;
bool repeat = event->repeat;
bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
switch (keycode) { switch (keycode) {
case SDLK_h: case SDLK_h:
// Ctrl+h on all platform, since Cmd+h is already captured by if (control && !shift && !repeat) {
// the system on macOS to hide the window
if (control && ctrl && !meta && !shift && !repeat) {
action_home(controller, action); action_home(controller, action);
} }
return; return;
case SDLK_b: // fall-through case SDLK_b: // fall-through
case SDLK_BACKSPACE: case SDLK_BACKSPACE:
if (control && cmd && !shift && !repeat) { if (control && !shift && !repeat) {
action_back(controller, action); action_back(controller, action);
} }
return; return;
case SDLK_s: case SDLK_s:
if (control && cmd && !shift && !repeat) { if (control && !shift && !repeat) {
action_app_switch(controller, action); action_app_switch(controller, action);
} }
return; return;
case SDLK_m: case SDLK_m:
// Ctrl+m on all platform, since Cmd+m is already captured by if (control && !shift && !repeat) {
// the system on macOS to minimize the window
if (control && ctrl && !meta && !shift && !repeat) {
action_menu(controller, action); action_menu(controller, action);
} }
return; return;
case SDLK_p: case SDLK_p:
if (control && cmd && !shift && !repeat) { if (control && !shift && !repeat) {
action_power(controller, action); action_power(controller, action);
} }
return; return;
case SDLK_o: case SDLK_o:
if (control && cmd && down) { if (control && !repeat && down) {
enum screen_power_mode mode = shift enum screen_power_mode mode = shift
? SCREEN_POWER_MODE_NORMAL ? SCREEN_POWER_MODE_NORMAL
: SCREEN_POWER_MODE_OFF; : SCREEN_POWER_MODE_OFF;
@ -329,67 +375,72 @@ input_manager_process_key(struct input_manager *im,
} }
return; return;
case SDLK_DOWN: case SDLK_DOWN:
if (control && cmd && !shift) { if (control && !shift) {
// forward repeated events // forward repeated events
action_volume_down(controller, action); action_volume_down(controller, action);
} }
return; return;
case SDLK_UP: case SDLK_UP:
if (control && cmd && !shift) { if (control && !shift) {
// forward repeated events // forward repeated events
action_volume_up(controller, action); action_volume_up(controller, action);
} }
return; return;
case SDLK_LEFT: case SDLK_LEFT:
if (cmd && !shift && down) { if (!shift && !repeat && down) {
rotate_client_left(im->screen); rotate_client_left(im->screen);
} }
return; return;
case SDLK_RIGHT: case SDLK_RIGHT:
if (cmd && !shift && down) { if (!shift && !repeat && down) {
rotate_client_right(im->screen); rotate_client_right(im->screen);
} }
return; return;
case SDLK_c: case SDLK_c:
if (control && cmd && !shift && !repeat && down) { if (control && !shift && !repeat) {
request_device_clipboard(controller); action_copy(controller, action);
}
return;
case SDLK_x:
if (control && !shift && !repeat) {
action_cut(controller, action);
} }
return; return;
case SDLK_v: case SDLK_v:
if (control && cmd && !repeat && down) { if (control && !repeat && down) {
if (shift) { if (shift) {
// store the text in the device clipboard and paste
set_device_clipboard(controller, true);
} else {
// inject the text as input events // inject the text as input events
clipboard_paste(controller); clipboard_paste(controller);
} else {
// store the text in the device clipboard and paste
set_device_clipboard(controller, true);
} }
} }
return; return;
case SDLK_f: case SDLK_f:
if (!shift && cmd && !repeat && down) { if (!shift && !repeat && down) {
screen_switch_fullscreen(im->screen); screen_switch_fullscreen(im->screen);
} }
return; return;
case SDLK_x: case SDLK_w:
if (!shift && cmd && !repeat && down) { if (!shift && !repeat && down) {
screen_resize_to_fit(im->screen); screen_resize_to_fit(im->screen);
} }
return; return;
case SDLK_g: case SDLK_g:
if (!shift && cmd && !repeat && down) { if (!shift && !repeat && down) {
screen_resize_to_pixel_perfect(im->screen); screen_resize_to_pixel_perfect(im->screen);
} }
return; return;
case SDLK_i: case SDLK_i:
if (!shift && cmd && !repeat && down) { if (!shift && !repeat && down) {
struct fps_counter *fps_counter = struct fps_counter *fps_counter =
im->video_buffer->fps_counter; im->video_buffer->fps_counter;
switch_fps_counter_state(fps_counter); switch_fps_counter_state(fps_counter);
} }
return; return;
case SDLK_n: case SDLK_n:
if (control && cmd && !repeat && down) { if (control && !repeat && down) {
if (shift) { if (shift) {
collapse_notification_panel(controller); collapse_notification_panel(controller);
} else { } else {
@ -398,7 +449,7 @@ input_manager_process_key(struct input_manager *im,
} }
return; return;
case SDLK_r: case SDLK_r:
if (control && cmd && !shift && !repeat && down) { if (control && !shift && !repeat && down) {
rotate_device(controller); rotate_device(controller);
} }
return; return;
@ -411,8 +462,23 @@ input_manager_process_key(struct input_manager *im,
return; return;
} }
if (event->repeat) {
if (!im->forward_key_repeat) {
return;
}
++im->repeat;
} else {
im->repeat = 0;
}
if (ctrl && !shift && keycode == SDLK_v && down && !repeat) {
// Synchronize the computer clipboard to the device clipboard before
// sending Ctrl+v, to allow seamless copy-paste.
set_device_clipboard(controller, false);
}
struct control_msg msg; struct control_msg msg;
if (convert_input_key(event, &msg, im->prefer_text)) { if (convert_input_key(event, &msg, im->prefer_text, im->repeat)) {
if (!controller_push_msg(controller, &msg)) { if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'inject keycode'"); LOGW("Could not request 'inject keycode'");
} }
@ -427,7 +493,7 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen,
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.screen_size = screen->frame_size;
to->inject_touch_event.position.point = to->inject_touch_event.position.point =
screen_convert_to_frame_coords(screen, from->x, from->y); screen_convert_window_to_frame_coords(screen, from->x, from->y);
to->inject_touch_event.pressure = 1.f; to->inject_touch_event.pressure = 1.f;
to->inject_touch_event.buttons = convert_mouse_buttons(from->state); to->inject_touch_event.buttons = convert_mouse_buttons(from->state);
@ -465,15 +531,15 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen,
to->inject_touch_event.pointer_id = from->fingerId; to->inject_touch_event.pointer_id = from->fingerId;
to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.screen_size = screen->frame_size;
int ww; int dw;
int wh; int dh;
SDL_GL_GetDrawableSize(screen->window, &ww, &wh); SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
// SDL touch event coordinates are normalized in the range [0; 1] // SDL touch event coordinates are normalized in the range [0; 1]
int32_t x = from->x * ww; int32_t x = from->x * dw;
int32_t y = from->y * wh; int32_t y = from->y * dh;
to->inject_touch_event.position.point = to->inject_touch_event.position.point =
screen_convert_to_frame_coords(screen, x, y); screen_convert_drawable_to_frame_coords(screen, x, y);
to->inject_touch_event.pressure = from->pressure; to->inject_touch_event.pressure = from->pressure;
to->inject_touch_event.buttons = 0; to->inject_touch_event.buttons = 0;
@ -503,8 +569,9 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.screen_size = screen->frame_size;
to->inject_touch_event.position.point = to->inject_touch_event.position.point =
screen_convert_to_frame_coords(screen, from->x, from->y); screen_convert_window_to_frame_coords(screen, from->x, from->y);
to->inject_touch_event.pressure = 1.f; to->inject_touch_event.pressure =
from->type == SDL_MOUSEBUTTONDOWN ? 1.f : 0.f;
to->inject_touch_event.buttons = to->inject_touch_event.buttons =
convert_mouse_buttons(SDL_BUTTON(from->button)); convert_mouse_buttons(SDL_BUTTON(from->button));
@ -513,8 +580,9 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
void void
input_manager_process_mouse_button(struct input_manager *im, input_manager_process_mouse_button(struct input_manager *im,
const SDL_MouseButtonEvent *event, const SDL_MouseButtonEvent *event) {
bool control) { bool control = im->control;
if (event->which == SDL_TOUCH_MOUSEID) { if (event->which == SDL_TOUCH_MOUSEID) {
// simulated from touch events, so it's a duplicate // simulated from touch events, so it's a duplicate
return; return;
@ -568,7 +636,8 @@ convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
struct position position = { struct position position = {
.screen_size = screen->frame_size, .screen_size = screen->frame_size,
.point = screen_convert_to_frame_coords(screen, mouse_x, mouse_y), .point = screen_convert_window_to_frame_coords(screen,
mouse_x, mouse_y),
}; };
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;

View file

@ -3,28 +3,46 @@
#include <stdbool.h> #include <stdbool.h>
#include <SDL2/SDL.h>
#include "config.h" #include "config.h"
#include "common.h" #include "common.h"
#include "controller.h" #include "controller.h"
#include "fps_counter.h" #include "fps_counter.h"
#include "video_buffer.h" #include "scrcpy.h"
#include "screen.h" #include "screen.h"
#include "video_buffer.h"
struct input_manager { struct input_manager {
struct controller *controller; struct controller *controller;
struct video_buffer *video_buffer; struct video_buffer *video_buffer;
struct screen *screen; struct screen *screen;
// SDL reports repeated events as a boolean, but Android expects the actual
// number of repetitions. This variable keeps track of the count.
unsigned repeat;
bool control;
bool forward_key_repeat;
bool prefer_text; bool prefer_text;
struct {
unsigned data[SC_MAX_SHORTCUT_MODS];
unsigned count;
} sdl_shortcut_mods;
}; };
void
input_manager_init(struct input_manager *im,
const struct scrcpy_options *options);
void void
input_manager_process_text_input(struct input_manager *im, input_manager_process_text_input(struct input_manager *im,
const SDL_TextInputEvent *event); const SDL_TextInputEvent *event);
void void
input_manager_process_key(struct input_manager *im, input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event, const SDL_KeyboardEvent *event);
bool control);
void void
input_manager_process_mouse_motion(struct input_manager *im, input_manager_process_mouse_motion(struct input_manager *im,
@ -36,8 +54,7 @@ input_manager_process_touch(struct input_manager *im,
void void
input_manager_process_mouse_button(struct input_manager *im, input_manager_process_mouse_button(struct input_manager *im,
const SDL_MouseButtonEvent *event, const SDL_MouseButtonEvent *event);
bool control);
void void
input_manager_process_mouse_wheel(struct input_manager *im, input_manager_process_mouse_wheel(struct input_manager *im,

View file

@ -1,5 +1,6 @@
#include "scrcpy.h" #include "scrcpy.h"
#include <assert.h>
#include <stdbool.h> #include <stdbool.h>
#include <unistd.h> #include <unistd.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
@ -42,7 +43,7 @@ convert_log_level_to_sdl(enum sc_log_level level) {
return SDL_LOG_PRIORITY_ERROR; return SDL_LOG_PRIORITY_ERROR;
default: default:
assert(!"unexpected log level"); assert(!"unexpected log level");
return SC_LOG_LEVEL_INFO; return SDL_LOG_PRIORITY_INFO;
} }
} }
@ -71,7 +72,7 @@ main(int argc, char *argv[]) {
} }
SDL_LogPriority sdl_log = convert_log_level_to_sdl(args.opts.log_level); SDL_LogPriority sdl_log = convert_log_level_to_sdl(args.opts.log_level);
SDL_LogSetAllPriority(sdl_log); SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log);
if (args.help) { if (args.help) {
scrcpy_print_usage(argv[0]); scrcpy_print_usage(argv[0]);

View file

@ -25,10 +25,19 @@ receiver_destroy(struct receiver *receiver) {
static void static void
process_msg(struct device_msg *msg) { process_msg(struct device_msg *msg) {
switch (msg->type) { switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD: case DEVICE_MSG_TYPE_CLIPBOARD: {
char *current = SDL_GetClipboardText();
bool same = current && !strcmp(current, msg->clipboard.text);
SDL_free(current);
if (same) {
LOGD("Computer clipboard unchanged");
return;
}
LOGI("Device clipboard copied"); LOGI("Device clipboard copied");
SDL_SetClipboardText(msg->clipboard.text); SDL_SetClipboardText(msg->clipboard.text);
break; break;
}
} }
} }
@ -60,28 +69,29 @@ static int
run_receiver(void *data) { run_receiver(void *data) {
struct receiver *receiver = data; struct receiver *receiver = data;
unsigned char buf[DEVICE_MSG_SERIALIZED_MAX_SIZE]; static unsigned char buf[DEVICE_MSG_MAX_SIZE];
size_t head = 0; size_t head = 0;
for (;;) { for (;;) {
assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE); assert(head < DEVICE_MSG_MAX_SIZE);
ssize_t r = net_recv(receiver->control_socket, buf, ssize_t r = net_recv(receiver->control_socket, buf + head,
DEVICE_MSG_SERIALIZED_MAX_SIZE - head); DEVICE_MSG_MAX_SIZE - head);
if (r <= 0) { if (r <= 0) {
LOGD("Receiver stopped"); LOGD("Receiver stopped");
break; break;
} }
ssize_t consumed = process_msgs(buf, r); head += r;
ssize_t consumed = process_msgs(buf, head);
if (consumed == -1) { if (consumed == -1) {
// an error occurred // an error occurred
break; break;
} }
if (consumed) { if (consumed) {
head -= consumed;
// shift the remaining data in the buffer // shift the remaining data in the buffer
memmove(buf, &buf[consumed], r - consumed); memmove(buf, &buf[consumed], head);
head = r - consumed;
} }
} }

View file

@ -63,7 +63,7 @@ recorder_queue_clear(struct recorder_queue *queue) {
bool bool
recorder_init(struct recorder *recorder, recorder_init(struct recorder *recorder,
const char *filename, const char *filename,
enum recorder_format format, enum sc_record_format format,
struct size declared_frame_size) { struct size declared_frame_size) {
recorder->filename = SDL_strdup(filename); recorder->filename = SDL_strdup(filename);
if (!recorder->filename) { if (!recorder->filename) {
@ -105,10 +105,10 @@ recorder_destroy(struct recorder *recorder) {
} }
static const char * static const char *
recorder_get_format_name(enum recorder_format format) { recorder_get_format_name(enum sc_record_format format) {
switch (format) { switch (format) {
case RECORDER_FORMAT_MP4: return "mp4"; case SC_RECORD_FORMAT_MP4: return "mp4";
case RECORDER_FORMAT_MKV: return "matroska"; case SC_RECORD_FORMAT_MKV: return "matroska";
default: return NULL; default: return NULL;
} }
} }

View file

@ -8,14 +8,9 @@
#include "config.h" #include "config.h"
#include "common.h" #include "common.h"
#include "scrcpy.h"
#include "util/queue.h" #include "util/queue.h"
enum recorder_format {
RECORDER_FORMAT_AUTO,
RECORDER_FORMAT_MP4,
RECORDER_FORMAT_MKV,
};
struct record_packet { struct record_packet {
AVPacket packet; AVPacket packet;
struct record_packet *next; struct record_packet *next;
@ -25,7 +20,7 @@ struct recorder_queue QUEUE(struct record_packet);
struct recorder { struct recorder {
char *filename; char *filename;
enum recorder_format format; enum sc_record_format format;
AVFormatContext *ctx; AVFormatContext *ctx;
struct size declared_frame_size; struct size declared_frame_size;
bool header_written; bool header_written;
@ -46,7 +41,7 @@ struct recorder {
bool bool
recorder_init(struct recorder *recorder, const char *filename, recorder_init(struct recorder *recorder, const char *filename,
enum recorder_format format, struct size declared_frame_size); enum sc_record_format format, struct size declared_frame_size);
void void
recorder_destroy(struct recorder *recorder); recorder_destroy(struct recorder *recorder);

View file

@ -8,6 +8,8 @@
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#ifdef _WIN32 #ifdef _WIN32
// not needed here, but winsock2.h must never be included AFTER windows.h
# include <winsock2.h>
# include <windows.h> # include <windows.h>
#endif #endif
@ -46,7 +48,14 @@ static struct input_manager input_manager = {
.controller = &controller, .controller = &controller,
.video_buffer = &video_buffer, .video_buffer = &video_buffer,
.screen = &screen, .screen = &screen,
.prefer_text = false, // initialized later .repeat = 0,
// initialized later
.prefer_text = false,
.sdl_shortcut_mods = {
.data = {0},
.count = 0,
},
}; };
#ifdef _WIN32 #ifdef _WIN32
@ -63,7 +72,8 @@ BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) {
// init SDL and set appropriate hints // init SDL and set appropriate hints
static bool static bool
sdl_init_and_configure(bool display, const char *render_driver) { sdl_init_and_configure(bool display, const char *render_driver,
bool disable_screensaver) {
uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS; uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS;
if (SDL_Init(flags)) { if (SDL_Init(flags)) {
LOGC("Could not initialize SDL: %s", SDL_GetError()); LOGC("Could not initialize SDL: %s", SDL_GetError());
@ -112,8 +122,13 @@ sdl_init_and_configure(bool display, const char *render_driver) {
LOGW("Could not disable minimize on focus loss"); LOGW("Could not disable minimize on focus loss");
} }
// Do not disable the screensaver when scrcpy is running if (disable_screensaver) {
SDL_EnableScreenSaver(); LOGD("Screensaver disabled");
SDL_DisableScreenSaver();
} else {
LOGD("Screensaver enabled");
SDL_EnableScreenSaver();
}
return true; return true;
} }
@ -155,7 +170,7 @@ enum event_result {
}; };
static enum event_result static enum event_result
handle_event(SDL_Event *event, bool control) { handle_event(SDL_Event *event, const struct scrcpy_options *options) {
switch (event->type) { switch (event->type) {
case EVENT_STREAM_STOPPED: case EVENT_STREAM_STOPPED:
LOGD("Video stream stopped"); LOGD("Video stream stopped");
@ -177,7 +192,7 @@ handle_event(SDL_Event *event, bool control) {
screen_handle_window_event(&screen, &event->window); screen_handle_window_event(&screen, &event->window);
break; break;
case SDL_TEXTINPUT: case SDL_TEXTINPUT:
if (!control) { if (!options->control) {
break; break;
} }
input_manager_process_text_input(&input_manager, &event->text); input_manager_process_text_input(&input_manager, &event->text);
@ -186,16 +201,16 @@ handle_event(SDL_Event *event, bool control) {
case SDL_KEYUP: case SDL_KEYUP:
// some key events do not interact with the device, so process the // some key events do not interact with the device, so process the
// event even if control is disabled // event even if control is disabled
input_manager_process_key(&input_manager, &event->key, control); input_manager_process_key(&input_manager, &event->key);
break; break;
case SDL_MOUSEMOTION: case SDL_MOUSEMOTION:
if (!control) { if (!options->control) {
break; break;
} }
input_manager_process_mouse_motion(&input_manager, &event->motion); input_manager_process_mouse_motion(&input_manager, &event->motion);
break; break;
case SDL_MOUSEWHEEL: case SDL_MOUSEWHEEL:
if (!control) { if (!options->control) {
break; break;
} }
input_manager_process_mouse_wheel(&input_manager, &event->wheel); input_manager_process_mouse_wheel(&input_manager, &event->wheel);
@ -204,8 +219,7 @@ handle_event(SDL_Event *event, bool control) {
case SDL_MOUSEBUTTONUP: case SDL_MOUSEBUTTONUP:
// some mouse events do not interact with the device, so process // some mouse events do not interact with the device, so process
// the event even if control is disabled // the event even if control is disabled
input_manager_process_mouse_button(&input_manager, &event->button, input_manager_process_mouse_button(&input_manager, &event->button);
control);
break; break;
case SDL_FINGERMOTION: case SDL_FINGERMOTION:
case SDL_FINGERDOWN: case SDL_FINGERDOWN:
@ -213,7 +227,7 @@ handle_event(SDL_Event *event, bool control) {
input_manager_process_touch(&input_manager, &event->tfinger); input_manager_process_touch(&input_manager, &event->tfinger);
break; break;
case SDL_DROPFILE: { case SDL_DROPFILE: {
if (!control) { if (!options->control) {
break; break;
} }
file_handler_action_t action; file_handler_action_t action;
@ -230,16 +244,15 @@ handle_event(SDL_Event *event, bool control) {
} }
static bool static bool
event_loop(bool display, bool control) { event_loop(const struct scrcpy_options *options) {
(void) display;
#ifdef CONTINUOUS_RESIZING_WORKAROUND #ifdef CONTINUOUS_RESIZING_WORKAROUND
if (display) { if (options->display) {
SDL_AddEventWatch(event_watcher, NULL); SDL_AddEventWatch(event_watcher, NULL);
} }
#endif #endif
SDL_Event event; SDL_Event event;
while (SDL_WaitEvent(&event)) { while (SDL_WaitEvent(&event)) {
enum event_result result = handle_event(&event, control); enum event_result result = handle_event(&event, options);
switch (result) { switch (result) {
case EVENT_RESULT_STOPPED_BY_USER: case EVENT_RESULT_STOPPED_BY_USER:
return true; return true;
@ -321,7 +334,8 @@ scrcpy(const struct scrcpy_options *options) {
bool controller_initialized = false; bool controller_initialized = false;
bool controller_started = false; bool controller_started = false;
if (!sdl_init_and_configure(options->display, options->render_driver)) { if (!sdl_init_and_configure(options->display, options->render_driver,
options->disable_screensaver)) {
goto end; goto end;
} }
@ -427,9 +441,9 @@ scrcpy(const struct scrcpy_options *options) {
} }
} }
input_manager.prefer_text = options->prefer_text; input_manager_init(&input_manager, options);
ret = event_loop(options->display, options->control); ret = event_loop(options);
LOGD("quit..."); LOGD("quit...");
screen_destroy(&screen); screen_destroy(&screen);

View file

@ -2,13 +2,46 @@
#define SCRCPY_H #define SCRCPY_H
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include "config.h" #include "config.h"
#include "common.h"
#include "input_manager.h" enum sc_log_level {
#include "recorder.h" SC_LOG_LEVEL_DEBUG,
#include "util/log.h" SC_LOG_LEVEL_INFO,
SC_LOG_LEVEL_WARN,
SC_LOG_LEVEL_ERROR,
};
enum sc_record_format {
SC_RECORD_FORMAT_AUTO,
SC_RECORD_FORMAT_MP4,
SC_RECORD_FORMAT_MKV,
};
#define SC_MAX_SHORTCUT_MODS 8
enum sc_shortcut_mod {
SC_MOD_LCTRL = 1 << 0,
SC_MOD_RCTRL = 1 << 1,
SC_MOD_LALT = 1 << 2,
SC_MOD_RALT = 1 << 3,
SC_MOD_LSUPER = 1 << 4,
SC_MOD_RSUPER = 1 << 5,
};
struct sc_shortcut_mods {
unsigned data[SC_MAX_SHORTCUT_MODS];
unsigned count;
};
struct sc_port_range {
uint16_t first;
uint16_t last;
};
#define SC_WINDOW_POSITION_UNDEFINED (-0x8000)
struct scrcpy_options { struct scrcpy_options {
const char *serial; const char *serial;
@ -19,15 +52,16 @@ struct scrcpy_options {
const char *render_driver; const char *render_driver;
const char *codec_options; const char *codec_options;
enum sc_log_level log_level; enum sc_log_level log_level;
enum recorder_format record_format; enum sc_record_format record_format;
struct port_range port_range; struct sc_port_range port_range;
struct sc_shortcut_mods shortcut_mods;
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps; uint16_t max_fps;
int8_t lock_video_orientation; int8_t lock_video_orientation;
uint8_t rotation; uint8_t rotation;
int16_t window_x; // WINDOW_POSITION_UNDEFINED for "auto" int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
uint16_t window_width; uint16_t window_width;
uint16_t window_height; uint16_t window_height;
uint16_t display_id; uint16_t display_id;
@ -43,6 +77,8 @@ struct scrcpy_options {
bool mipmaps; bool mipmaps;
bool stay_awake; bool stay_awake;
bool force_adb_forward; bool force_adb_forward;
bool disable_screensaver;
bool forward_key_repeat;
}; };
#define SCRCPY_OPTIONS_DEFAULT { \ #define SCRCPY_OPTIONS_DEFAULT { \
@ -54,18 +90,22 @@ struct scrcpy_options {
.render_driver = NULL, \ .render_driver = NULL, \
.codec_options = NULL, \ .codec_options = NULL, \
.log_level = SC_LOG_LEVEL_INFO, \ .log_level = SC_LOG_LEVEL_INFO, \
.record_format = RECORDER_FORMAT_AUTO, \ .record_format = SC_RECORD_FORMAT_AUTO, \
.port_range = { \ .port_range = { \
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
.last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \
}, \ }, \
.shortcut_mods = { \
.data = {SC_MOD_LALT, SC_MOD_LSUPER}, \
.count = 2, \
}, \
.max_size = DEFAULT_MAX_SIZE, \ .max_size = DEFAULT_MAX_SIZE, \
.bit_rate = DEFAULT_BIT_RATE, \ .bit_rate = DEFAULT_BIT_RATE, \
.max_fps = 0, \ .max_fps = 0, \
.lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \ .lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \
.rotation = 0, \ .rotation = 0, \
.window_x = WINDOW_POSITION_UNDEFINED, \ .window_x = SC_WINDOW_POSITION_UNDEFINED, \
.window_y = WINDOW_POSITION_UNDEFINED, \ .window_y = SC_WINDOW_POSITION_UNDEFINED, \
.window_width = 0, \ .window_width = 0, \
.window_height = 0, \ .window_height = 0, \
.display_id = 0, \ .display_id = 0, \
@ -81,6 +121,8 @@ struct scrcpy_options {
.mipmaps = true, \ .mipmaps = true, \
.stay_awake = false, \ .stay_awake = false, \
.force_adb_forward = false, \ .force_adb_forward = false, \
.disable_screensaver = false, \
.forward_key_repeat = true, \
} }
bool bool

View file

@ -8,6 +8,7 @@
#include "common.h" #include "common.h"
#include "compat.h" #include "compat.h"
#include "icon.xpm" #include "icon.xpm"
#include "scrcpy.h"
#include "tiny_xpm.h" #include "tiny_xpm.h"
#include "video_buffer.h" #include "video_buffer.h"
#include "util/lock.h" #include "util/lock.h"
@ -257,9 +258,9 @@ screen_init_rendering(struct screen *screen, const char *window_title,
window_flags |= SDL_WINDOW_BORDERLESS; window_flags |= SDL_WINDOW_BORDERLESS;
} }
int x = window_x != WINDOW_POSITION_UNDEFINED int x = window_x != SC_WINDOW_POSITION_UNDEFINED
? window_x : (int) SDL_WINDOWPOS_UNDEFINED; ? window_x : (int) SDL_WINDOWPOS_UNDEFINED;
int y = window_y != WINDOW_POSITION_UNDEFINED int y = window_y != SC_WINDOW_POSITION_UNDEFINED
? window_y : (int) SDL_WINDOWPOS_UNDEFINED; ? window_y : (int) SDL_WINDOWPOS_UNDEFINED;
screen->window = SDL_CreateWindow(window_title, x, y, screen->window = SDL_CreateWindow(window_title, x, y,
window_size.width, window_size.height, window_size.width, window_size.height,
@ -579,14 +580,14 @@ screen_handle_window_event(struct screen *screen,
} }
struct point struct point
screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { screen_convert_drawable_to_frame_coords(struct screen *screen,
int32_t x, int32_t y) {
unsigned rotation = screen->rotation; unsigned rotation = screen->rotation;
assert(rotation < 4); assert(rotation < 4);
int32_t w = screen->content_size.width; int32_t w = screen->content_size.width;
int32_t h = screen->content_size.height; int32_t h = screen->content_size.height;
screen_hidpi_scale_coords(screen, &x, &y);
x = (int64_t) (x - screen->rect.x) * w / screen->rect.w; x = (int64_t) (x - screen->rect.x) * w / screen->rect.w;
y = (int64_t) (y - screen->rect.y) * h / screen->rect.h; y = (int64_t) (y - screen->rect.y) * h / screen->rect.h;
@ -615,6 +616,13 @@ screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) {
return result; return result;
} }
struct point
screen_convert_window_to_frame_coords(struct screen *screen,
int32_t x, int32_t y) {
screen_hidpi_scale_coords(screen, &x, &y);
return screen_convert_drawable_to_frame_coords(screen, x, y);
}
void void
screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y) { screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y) {
// take the HiDPI scaling (dw/ww and dh/wh) into account // take the HiDPI scaling (dw/ww and dh/wh) into account

View file

@ -9,8 +9,6 @@
#include "common.h" #include "common.h"
#include "opengl.h" #include "opengl.h"
#define WINDOW_POSITION_UNDEFINED (-0x8000)
struct video_buffer; struct video_buffer;
struct screen { struct screen {
@ -76,7 +74,7 @@ void
screen_init(struct screen *screen); screen_init(struct screen *screen);
// initialize screen, create window, renderer and texture (window is hidden) // initialize screen, create window, renderer and texture (window is hidden)
// window_x and window_y accept WINDOW_POSITION_UNDEFINED // window_x and window_y accept SC_WINDOW_POSITION_UNDEFINED
bool bool
screen_init_rendering(struct screen *screen, const char *window_title, screen_init_rendering(struct screen *screen, const char *window_title,
struct size frame_size, bool always_on_top, struct size frame_size, bool always_on_top,
@ -126,7 +124,14 @@ screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event);
// convert point from window coordinates to frame coordinates // convert point from window coordinates to frame coordinates
// x and y are expressed in pixels // x and y are expressed in pixels
struct point struct point
screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y); screen_convert_window_to_frame_coords(struct screen *screen,
int32_t x, int32_t y);
// convert point from drawable coordinates to frame coordinates
// x and y are expressed in pixels
struct point
screen_convert_drawable_to_frame_coords(struct screen *screen,
int32_t x, int32_t y);
// Convert coordinates from window to drawable. // Convert coordinates from window to drawable.
// Events are expressed in window coordinates, but content is expressed in // Events are expressed in window coordinates, but content is expressed in

View file

@ -143,7 +143,7 @@ listen_on_port(uint16_t port) {
static bool static bool
enable_tunnel_reverse_any_port(struct server *server, enable_tunnel_reverse_any_port(struct server *server,
struct port_range port_range) { struct sc_port_range port_range) {
uint16_t port = port_range.first; uint16_t port = port_range.first;
for (;;) { for (;;) {
if (!enable_tunnel_reverse(server->serial, port)) { if (!enable_tunnel_reverse(server->serial, port)) {
@ -189,7 +189,7 @@ enable_tunnel_reverse_any_port(struct server *server,
static bool static bool
enable_tunnel_forward_any_port(struct server *server, enable_tunnel_forward_any_port(struct server *server,
struct port_range port_range) { struct sc_port_range port_range) {
server->tunnel_forward = true; server->tunnel_forward = true;
uint16_t port = port_range.first; uint16_t port = port_range.first;
for (;;) { for (;;) {
@ -217,7 +217,7 @@ enable_tunnel_forward_any_port(struct server *server,
} }
static bool static bool
enable_tunnel_any_port(struct server *server, struct port_range port_range, enable_tunnel_any_port(struct server *server, struct sc_port_range port_range,
bool force_adb_forward) { bool force_adb_forward) {
if (!force_adb_forward) { if (!force_adb_forward) {
// Attempt to use "adb reverse" // Attempt to use "adb reverse"

View file

@ -9,6 +9,7 @@
#include "config.h" #include "config.h"
#include "command.h" #include "command.h"
#include "common.h" #include "common.h"
#include "scrcpy.h"
#include "util/log.h" #include "util/log.h"
#include "util/net.h" #include "util/net.h"
@ -20,7 +21,7 @@ struct server {
socket_t server_socket; // only used if !tunnel_forward socket_t server_socket; // only used if !tunnel_forward
socket_t video_socket; socket_t video_socket;
socket_t control_socket; socket_t control_socket;
struct port_range port_range; struct sc_port_range port_range;
uint16_t local_port; // selected from port_range uint16_t local_port; // selected from port_range
bool tunnel_enabled; bool tunnel_enabled;
bool tunnel_forward; // use "adb forward" instead of "adb reverse" bool tunnel_forward; // use "adb forward" instead of "adb reverse"
@ -47,7 +48,7 @@ struct server_params {
enum sc_log_level log_level; enum sc_log_level log_level;
const char *crop; const char *crop;
const char *codec_options; const char *codec_options;
struct port_range port_range; struct sc_port_range port_range;
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps; uint16_t max_fps;

View file

@ -3,13 +3,6 @@
#include <SDL2/SDL_log.h> #include <SDL2/SDL_log.h>
enum sc_log_level {
SC_LOG_LEVEL_DEBUG,
SC_LOG_LEVEL_INFO,
SC_LOG_LEVEL_WARN,
SC_LOG_LEVEL_ERROR,
};
#define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
#define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
#define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)

View file

@ -65,7 +65,10 @@ static void test_buffer_read64be(void) {
assert(val == 0xABCD1234567890EF); assert(val == 0xABCD1234567890EF);
} }
int main(void) { int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_buffer_write16be(); test_buffer_write16be();
test_buffer_write32be(); test_buffer_write32be();
test_buffer_write64be(); test_buffer_write64be();

View file

@ -65,7 +65,10 @@ static void test_cbuf_push_take(void) {
assert(item == 35); assert(item == 35);
} }
int main(void) { int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_cbuf_empty(); test_cbuf_empty();
test_cbuf_full(); test_cbuf_full();
test_cbuf_push_take(); test_cbuf_push_take();

View file

@ -1,7 +1,9 @@
#include <assert.h> #include <assert.h>
#include <string.h>
#include "cli.h" #include "cli.h"
#include "common.h" #include "common.h"
#include "scrcpy.h"
static void test_flag_version(void) { static void test_flag_version(void) {
struct scrcpy_cli_args args = { struct scrcpy_cli_args args = {
@ -73,7 +75,6 @@ static void test_options(void) {
const struct scrcpy_options *opts = &args.opts; const struct scrcpy_options *opts = &args.opts;
assert(opts->always_on_top); assert(opts->always_on_top);
fprintf(stderr, "%d\n", (int) opts->bit_rate);
assert(opts->bit_rate == 5000000); assert(opts->bit_rate == 5000000);
assert(!strcmp(opts->crop, "100:200:300:400")); assert(!strcmp(opts->crop, "100:200:300:400"));
assert(opts->fullscreen); assert(opts->fullscreen);
@ -84,7 +85,7 @@ static void test_options(void) {
assert(opts->port_range.last == 1236); assert(opts->port_range.last == 1236);
assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->push_target, "/sdcard/Movies"));
assert(!strcmp(opts->record_filename, "file")); assert(!strcmp(opts->record_filename, "file"));
assert(opts->record_format == RECORDER_FORMAT_MKV); assert(opts->record_format == SC_RECORD_FORMAT_MKV);
assert(opts->render_expired_frames); assert(opts->render_expired_frames);
assert(!strcmp(opts->serial, "0123456789abcdef")); assert(!strcmp(opts->serial, "0123456789abcdef"));
assert(opts->show_touches); assert(opts->show_touches);
@ -119,13 +120,54 @@ static void test_options2(void) {
assert(!opts->control); assert(!opts->control);
assert(!opts->display); assert(!opts->display);
assert(!strcmp(opts->record_filename, "file.mp4")); assert(!strcmp(opts->record_filename, "file.mp4"));
assert(opts->record_format == RECORDER_FORMAT_MP4); assert(opts->record_format == SC_RECORD_FORMAT_MP4);
} }
int main(void) { static void test_parse_shortcut_mods(void) {
struct sc_shortcut_mods mods;
bool ok;
ok = sc_parse_shortcut_mods("lctrl", &mods);
assert(ok);
assert(mods.count == 1);
assert(mods.data[0] == SC_MOD_LCTRL);
ok = sc_parse_shortcut_mods("lctrl+lalt", &mods);
assert(ok);
assert(mods.count == 1);
assert(mods.data[0] == (SC_MOD_LCTRL | SC_MOD_LALT));
ok = sc_parse_shortcut_mods("rctrl,lalt", &mods);
assert(ok);
assert(mods.count == 2);
assert(mods.data[0] == SC_MOD_RCTRL);
assert(mods.data[1] == SC_MOD_LALT);
ok = sc_parse_shortcut_mods("lsuper,rsuper+lalt,lctrl+rctrl+ralt", &mods);
assert(ok);
assert(mods.count == 3);
assert(mods.data[0] == SC_MOD_LSUPER);
assert(mods.data[1] == (SC_MOD_RSUPER | SC_MOD_LALT));
assert(mods.data[2] == (SC_MOD_LCTRL | SC_MOD_RCTRL | SC_MOD_RALT));
ok = sc_parse_shortcut_mods("", &mods);
assert(!ok);
ok = sc_parse_shortcut_mods("lctrl+", &mods);
assert(!ok);
ok = sc_parse_shortcut_mods("lctrl,", &mods);
assert(!ok);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_flag_version(); test_flag_version();
test_flag_help(); test_flag_help();
test_options(); test_options();
test_options2(); test_options2();
test_parse_shortcut_mods();
return 0; return 0;
}; };

View file

@ -9,18 +9,20 @@ static void test_serialize_inject_keycode(void) {
.inject_keycode = { .inject_keycode = {
.action = AKEY_EVENT_ACTION_UP, .action = AKEY_EVENT_ACTION_UP,
.keycode = AKEYCODE_ENTER, .keycode = AKEYCODE_ENTER,
.repeat = 5,
.metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON, .metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON,
}, },
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 10); assert(size == 14);
const unsigned char expected[] = { const unsigned char expected[] = {
CONTROL_MSG_TYPE_INJECT_KEYCODE, CONTROL_MSG_TYPE_INJECT_KEYCODE,
0x01, // AKEY_EVENT_ACTION_UP 0x01, // AKEY_EVENT_ACTION_UP
0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER
0x00, 0x00, 0x00, 0X05, // repeat
0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON 0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
@ -34,13 +36,13 @@ static void test_serialize_inject_text(void) {
}, },
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 16); assert(size == 18);
const unsigned char expected[] = { const unsigned char expected[] = {
CONTROL_MSG_TYPE_INJECT_TEXT, CONTROL_MSG_TYPE_INJECT_TEXT,
0x00, 0x0d, // text length 0x00, 0x00, 0x00, 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
@ -54,15 +56,17 @@ static void test_serialize_inject_text_long(void) {
text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0';
msg.inject_text.text = text; msg.inject_text.text = text;
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); assert(size == 5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
unsigned char expected[3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; unsigned char expected[5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH];
expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT; expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT;
expected[1] = 0x01; expected[1] = 0x00;
expected[2] = 0x2c; // text length (16 bits) expected[2] = 0x00;
memset(&expected[3], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); expected[3] = 0x01;
expected[4] = 0x2c; // text length (32 bits)
memset(&expected[5], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
@ -88,7 +92,7 @@ static void test_serialize_inject_touch_event(void) {
}, },
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 28); assert(size == 28);
@ -123,7 +127,7 @@ static void test_serialize_inject_scroll_event(void) {
}, },
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 21); assert(size == 21);
@ -142,7 +146,7 @@ static void test_serialize_back_or_screen_on(void) {
.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, .type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
@ -157,7 +161,7 @@ static void test_serialize_expand_notification_panel(void) {
.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, .type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
@ -172,7 +176,7 @@ static void test_serialize_collapse_notification_panel(void) {
.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, .type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
@ -187,7 +191,7 @@ static void test_serialize_get_clipboard(void) {
.type = CONTROL_MSG_TYPE_GET_CLIPBOARD, .type = CONTROL_MSG_TYPE_GET_CLIPBOARD,
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
@ -206,14 +210,14 @@ static void test_serialize_set_clipboard(void) {
}, },
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 17); assert(size == 19);
const unsigned char expected[] = { const unsigned char expected[] = {
CONTROL_MSG_TYPE_SET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD,
1, // paste 1, // paste
0x00, 0x0d, // text length 0x00, 0x00, 0x00, 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
@ -227,7 +231,7 @@ static void test_serialize_set_screen_power_mode(void) {
}, },
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 2); assert(size == 2);
@ -243,7 +247,7 @@ static void test_serialize_rotate_device(void) {
.type = CONTROL_MSG_TYPE_ROTATE_DEVICE, .type = CONTROL_MSG_TYPE_ROTATE_DEVICE,
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
@ -253,7 +257,10 @@ static void test_serialize_rotate_device(void) {
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
int main(void) { int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_serialize_inject_keycode(); test_serialize_inject_keycode();
test_serialize_inject_text(); test_serialize_inject_text();
test_serialize_inject_text_long(); test_serialize_inject_text_long();

View file

@ -4,16 +4,17 @@
#include "device_msg.h" #include "device_msg.h"
#include <stdio.h> #include <stdio.h>
static void test_deserialize_clipboard(void) { static void test_deserialize_clipboard(void) {
const unsigned char input[] = { const unsigned char input[] = {
DEVICE_MSG_TYPE_CLIPBOARD, DEVICE_MSG_TYPE_CLIPBOARD,
0x00, 0x03, // text length 0x00, 0x00, 0x00, 0x03, // text length
0x41, 0x42, 0x43, // "ABC" 0x41, 0x42, 0x43, // "ABC"
}; };
struct device_msg msg; struct device_msg msg;
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); ssize_t r = device_msg_deserialize(input, sizeof(input), &msg);
assert(r == 6); assert(r == 8);
assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD);
assert(msg.clipboard.text); assert(msg.clipboard.text);
@ -22,7 +23,33 @@ static void test_deserialize_clipboard(void) {
device_msg_destroy(&msg); device_msg_destroy(&msg);
} }
int main(void) { static void test_deserialize_clipboard_big(void) {
unsigned char input[DEVICE_MSG_MAX_SIZE];
input[0] = DEVICE_MSG_TYPE_CLIPBOARD;
input[1] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0xff000000u) >> 24;
input[2] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x00ff0000u) >> 16;
input[3] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x0000ff00u) >> 8;
input[4] = DEVICE_MSG_TEXT_MAX_LENGTH & 0x000000ffu;
memset(input + 5, 'a', DEVICE_MSG_TEXT_MAX_LENGTH);
struct device_msg msg;
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg);
assert(r == DEVICE_MSG_MAX_SIZE);
assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD);
assert(msg.clipboard.text);
assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH);
assert(msg.clipboard.text[0] == 'a');
device_msg_destroy(&msg);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_deserialize_clipboard(); test_deserialize_clipboard();
test_deserialize_clipboard_big();
return 0; return 0;
} }

View file

@ -32,7 +32,10 @@ static void test_queue(void) {
assert(queue_is_empty(&queue)); assert(queue_is_empty(&queue));
} }
int main(void) { int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_queue(); test_queue();
return 0; return 0;
} }

View file

@ -286,7 +286,10 @@ static void test_parse_integer_with_suffix(void) {
assert(!ok); assert(!ok);
} }
int main(void) { int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_xstrncpy_simple(); test_xstrncpy_simple();
test_xstrncpy_just_fit(); test_xstrncpy_just_fit();
test_xstrncpy_truncated(); test_xstrncpy_truncated();

View file

@ -15,6 +15,6 @@ cpu = 'i686'
endian = 'little' endian = 'little'
[properties] [properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win32-shared' prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win32-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win32-dev' prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win32-dev'
prebuilt_sdl2 = 'SDL2-2.0.12/i686-w64-mingw32' prebuilt_sdl2 = 'SDL2-2.0.12/i686-w64-mingw32'

View file

@ -15,6 +15,6 @@ cpu = 'x86_64'
endian = 'little' endian = 'little'
[properties] [properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win64-shared' prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win64-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win64-dev' prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win64-dev'
prebuilt_sdl2 = 'SDL2-2.0.12/x86_64-w64-mingw32' prebuilt_sdl2 = 'SDL2-2.0.12/x86_64-w64-mingw32'

View file

@ -1,5 +1,5 @@
project('scrcpy', 'c', project('scrcpy', 'c',
version: '1.14', version: '1.15.1',
meson_version: '>= 0.48', meson_version: '>= 0.48',
default_options: [ default_options: [
'c_std=c11', 'c_std=c11',

View file

@ -10,24 +10,24 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32
prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
prepare-ffmpeg-shared-win32: prepare-ffmpeg-shared-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.2-win32-shared.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.3.1-win32-shared.zip \
ab5d603aaa54de360db2c2ffe378c82376b9343ea1175421dd644639aa07ee31 \ 357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2 \
ffmpeg-4.2.2-win32-shared ffmpeg-4.3.1-win32-shared
prepare-ffmpeg-dev-win32: prepare-ffmpeg-dev-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.2-win32-dev.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.3.1-win32-dev.zip \
8d224be567a2950cad4be86f4aabdd045bfa52ad758e87c72cedd278613bc6c8 \ 230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b \
ffmpeg-4.2.2-win32-dev ffmpeg-4.3.1-win32-dev
prepare-ffmpeg-shared-win64: prepare-ffmpeg-shared-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.2-win64-shared.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.3.1-win64-shared.zip \
5aedf268952b7d9f6541dbfcb47cd86a7e7881a3b7ba482fd3bc4ca33bda7bf5 \ dd29b7f92f48dead4dd940492c7509138c0f99db445076d0a597007298a79940 \
ffmpeg-4.2.2-win64-shared ffmpeg-4.3.1-win64-shared
prepare-ffmpeg-dev-win64: prepare-ffmpeg-dev-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.2-win64-dev.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.3.1-win64-dev.zip \
f4885f859c5b0d6663c2a0a4c1cf035b1c60b146402790b796bd3ad84f4f3ca2 \ 2e8038242cf8e1bd095c2978f196ff0462b122cc6ef7e74626a6af15459d8b81 \
ffmpeg-4.2.2-win64-dev ffmpeg-4.3.1-win64-dev
prepare-sdl2: prepare-sdl2:
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \ @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \
@ -35,6 +35,6 @@ prepare-sdl2:
SDL2-2.0.12 SDL2-2.0.12
prepare-adb: prepare-adb:
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.0-windows.zip \ @./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.4-windows.zip \
854305f9a702f5ea2c3de73edde402bd26afa0ee944c9b0c4380420f5a862e0d \ 413182fff6c5957911e231b9e97e6be4fc6a539035e3dfb580b5c54bd5950fee \
platform-tools platform-tools

View file

@ -6,8 +6,8 @@ android {
applicationId "com.genymobile.scrcpy" applicationId "com.genymobile.scrcpy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode 16 versionCode 18
versionName "1.14" versionName "1.15.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {

View file

@ -12,7 +12,7 @@
set -e set -e
SCRCPY_DEBUG=false SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=1.14 SCRCPY_VERSION_NAME=1.15.1
PLATFORM=${ANDROID_PLATFORM:-29} PLATFORM=${ANDROID_PLATFORM:-29}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}

View file

@ -19,18 +19,19 @@ public final class CleanUp {
// not instantiable // not instantiable
} }
public static void configure(boolean disableShowTouches, int restoreStayOn) throws IOException { public static void configure(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException {
boolean needProcess = disableShowTouches || restoreStayOn != -1; boolean needProcess = disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode;
if (needProcess) { if (needProcess) {
startProcess(disableShowTouches, restoreStayOn); startProcess(disableShowTouches, restoreStayOn, restoreNormalPowerMode);
} else { } else {
// There is no additional clean up to do when scrcpy dies // There is no additional clean up to do when scrcpy dies
unlinkSelf(); unlinkSelf();
} }
} }
private static void startProcess(boolean disableShowTouches, int restoreStayOn) throws IOException { private static void startProcess(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(restoreStayOn)}; String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(
restoreStayOn), String.valueOf(restoreNormalPowerMode)};
ProcessBuilder builder = new ProcessBuilder(cmd); ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", SERVER_PATH); builder.environment().put("CLASSPATH", SERVER_PATH);
@ -59,6 +60,7 @@ public final class CleanUp {
boolean disableShowTouches = Boolean.parseBoolean(args[0]); boolean disableShowTouches = Boolean.parseBoolean(args[0]);
int restoreStayOn = Integer.parseInt(args[1]); int restoreStayOn = Integer.parseInt(args[1]);
boolean restoreNormalPowerMode = Boolean.parseBoolean(args[2]);
if (disableShowTouches || restoreStayOn != -1) { if (disableShowTouches || restoreStayOn != -1) {
ServiceManager serviceManager = new ServiceManager(); ServiceManager serviceManager = new ServiceManager();
@ -73,5 +75,10 @@ public final class CleanUp {
} }
} }
} }
if (restoreNormalPowerMode) {
Ln.i("Restoring normal power mode");
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
}
} }
} }

View file

@ -17,8 +17,6 @@ public final class ControlMessage {
public static final int TYPE_SET_SCREEN_POWER_MODE = 9; public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
public static final int TYPE_ROTATE_DEVICE = 10; public static final int TYPE_ROTATE_DEVICE = 10;
public static final int FLAGS_PASTE = 1;
private int type; private int type;
private String text; private String text;
private int metaState; // KeyEvent.META_* private int metaState; // KeyEvent.META_*
@ -30,16 +28,18 @@ public final class ControlMessage {
private Position position; private Position position;
private int hScroll; private int hScroll;
private int vScroll; private int vScroll;
private int flags; private boolean paste;
private int repeat;
private ControlMessage() { private ControlMessage() {
} }
public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) { public static ControlMessage createInjectKeycode(int action, int keycode, int repeat, int metaState) {
ControlMessage msg = new ControlMessage(); ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_KEYCODE; msg.type = TYPE_INJECT_KEYCODE;
msg.action = action; msg.action = action;
msg.keycode = keycode; msg.keycode = keycode;
msg.repeat = repeat;
msg.metaState = metaState; msg.metaState = metaState;
return msg; return msg;
} }
@ -75,9 +75,7 @@ public final class ControlMessage {
ControlMessage msg = new ControlMessage(); ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_CLIPBOARD; msg.type = TYPE_SET_CLIPBOARD;
msg.text = text; msg.text = text;
if (paste) { msg.paste = paste;
msg.flags = FLAGS_PASTE;
}
return msg; return msg;
} }
@ -141,7 +139,11 @@ public final class ControlMessage {
return vScroll; return vScroll;
} }
public int getFlags() { public boolean getPaste() {
return flags; return paste;
}
public int getRepeat() {
return repeat;
} }
} }

View file

@ -8,20 +8,19 @@ import java.nio.charset.StandardCharsets;
public class ControlMessageReader { public class ControlMessageReader {
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1; static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1;
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4092; // 4096 - 1 (type) - 1 (parse flag) - 2 (length) private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 6; // type: 1 byte; paste flag: 1 byte; length: 4 bytes
public static final int INJECT_TEXT_MAX_LENGTH = 300; public static final int INJECT_TEXT_MAX_LENGTH = 300;
private static final int RAW_BUFFER_SIZE = 4096; private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH];
public ControlMessageReader() { public ControlMessageReader() {
// invariant: the buffer is always in "get" mode // invariant: the buffer is always in "get" mode
@ -99,20 +98,23 @@ public class ControlMessageReader {
} }
int action = toUnsigned(buffer.get()); int action = toUnsigned(buffer.get());
int keycode = buffer.getInt(); int keycode = buffer.getInt();
int repeat = buffer.getInt();
int metaState = buffer.getInt(); int metaState = buffer.getInt();
return ControlMessage.createInjectKeycode(action, keycode, metaState); return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState);
} }
private String parseString() { private String parseString() {
if (buffer.remaining() < 2) { if (buffer.remaining() < 4) {
return null; return null;
} }
int len = toUnsigned(buffer.getShort()); int len = buffer.getInt();
if (buffer.remaining() < len) { if (buffer.remaining() < len) {
return null; return null;
} }
buffer.get(textBuffer, 0, len); int position = buffer.position();
return new String(textBuffer, 0, len, StandardCharsets.UTF_8); // Move the buffer position to consume the text
buffer.position(position + len);
return new String(rawBuffer, position, len, StandardCharsets.UTF_8);
} }
private ControlMessage parseInjectText() { private ControlMessage parseInjectText() {
@ -152,12 +154,12 @@ public class ControlMessageReader {
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
return null; return null;
} }
boolean parse = buffer.get() != 0; boolean paste = buffer.get() != 0;
String text = parseString(); String text = parseString();
if (text == null) { if (text == null) {
return null; return null;
} }
return ControlMessage.createSetClipboard(text, parse); return ControlMessage.createSetClipboard(text, paste);
} }
private ControlMessage parseSetScreenPowerMode() { private ControlMessage parseSetScreenPowerMode() {

View file

@ -8,11 +8,16 @@ import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Controller { public class Controller {
private static final int DEVICE_ID_VIRTUAL = -1; private static final int DEVICE_ID_VIRTUAL = -1;
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
private final Device device; private final Device device;
private final DesktopConnection connection; private final DesktopConnection connection;
private final DeviceMessageSender sender; private final DeviceMessageSender sender;
@ -24,6 +29,8 @@ public class Controller {
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
private boolean keepPowerModeOff;
public Controller(Device device, DesktopConnection connection) { public Controller(Device device, DesktopConnection connection) {
this.device = device; this.device = device;
this.connection = connection; this.connection = connection;
@ -48,10 +55,10 @@ public class Controller {
public void control() throws IOException { public void control() throws IOException {
// on start, power on the device // on start, power on the device
if (!device.isScreenOn()) { if (!device.isScreenOn()) {
device.injectKeycode(KeyEvent.KEYCODE_POWER); device.injectKeycode(KeyEvent.KEYCODE_WAKEUP);
// dirty hack // dirty hack
// After POWER is injected, the device is powered on asynchronously. // After the keycode is injected, the device is powered on asynchronously.
// To turn the device screen off while mirroring, the client will send a message that // To turn the device screen off while mirroring, the client will send a message that
// would be handled before the device is actually powered on, so its effect would // would be handled before the device is actually powered on, so its effect would
// be "canceled" once the device is turned back on. // be "canceled" once the device is turned back on.
@ -74,7 +81,7 @@ public class Controller {
switch (msg.getType()) { switch (msg.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE: case ControlMessage.TYPE_INJECT_KEYCODE:
if (device.supportsInputEvents()) { if (device.supportsInputEvents()) {
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
} }
break; break;
case ControlMessage.TYPE_INJECT_TEXT: case ControlMessage.TYPE_INJECT_TEXT:
@ -110,14 +117,14 @@ public class Controller {
} }
break; break;
case ControlMessage.TYPE_SET_CLIPBOARD: case ControlMessage.TYPE_SET_CLIPBOARD:
boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0; setClipboard(msg.getText(), msg.getPaste());
setClipboard(msg.getText(), paste);
break; break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
if (device.supportsInputEvents()) { if (device.supportsInputEvents()) {
int mode = msg.getAction(); int mode = msg.getAction();
boolean setPowerModeOk = device.setScreenPowerMode(mode); boolean setPowerModeOk = Device.setScreenPowerMode(mode);
if (setPowerModeOk) { if (setPowerModeOk) {
keepPowerModeOff = mode == Device.POWER_MODE_OFF;
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
} }
} }
@ -130,8 +137,11 @@ public class Controller {
} }
} }
private boolean injectKeycode(int action, int keycode, int metaState) { private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
return device.injectKeyEvent(action, keycode, 0, metaState); if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) {
schedulePowerModeOff();
}
return device.injectKeyEvent(action, keycode, repeat, metaState);
} }
private boolean injectChar(char c) { private boolean injectChar(char c) {
@ -166,7 +176,7 @@ public class Controller {
Point point = device.getPhysicalPoint(position); Point point = device.getPhysicalPoint(position);
if (point == null) { if (point == null) {
// ignore event Ln.w("Ignore touch event, it was generated for a different device size");
return false; return false;
} }
@ -224,8 +234,24 @@ public class Controller {
return device.injectEvent(event); return device.injectEvent(event);
} }
/**
* Schedule a call to set power mode to off after a small delay.
*/
private static void schedulePowerModeOff() {
EXECUTOR.schedule(new Runnable() {
@Override
public void run() {
Ln.i("Forcing screen off");
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
}
}, 200, TimeUnit.MILLISECONDS);
}
private boolean pressBackOrTurnScreenOn() { private boolean pressBackOrTurnScreenOn() {
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_WAKEUP;
if (keepPowerModeOff && keycode == KeyEvent.KEYCODE_WAKEUP) {
schedulePowerModeOff();
}
return device.injectKeycode(keycode); return device.injectKeycode(keycode);
} }

View file

@ -1,5 +1,6 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
@ -80,23 +81,28 @@ public final class Device {
if (options.getControl()) { if (options.getControl()) {
// If control is enabled, synchronize Android clipboard to the computer automatically // If control is enabled, synchronize Android clipboard to the computer automatically
serviceManager.getClipboardManager().addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { ClipboardManager clipboardManager = serviceManager.getClipboardManager();
@Override if (clipboardManager != null) {
public void dispatchPrimaryClipChanged() { clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
if (isSettingClipboard.get()) { @Override
// This is a notification for the change we are currently applying, ignore it public void dispatchPrimaryClipChanged() {
return; if (isSettingClipboard.get()) {
} // This is a notification for the change we are currently applying, ignore it
synchronized (Device.this) { return;
if (clipboardListener != null) { }
String text = getClipboardText(); synchronized (Device.this) {
if (text != null) { if (clipboardListener != null) {
clipboardListener.onClipboardTextChanged(text); String text = getClipboardText();
if (text != null) {
clipboardListener.onClipboardTextChanged(text);
}
} }
} }
} }
} });
}); } else {
Ln.w("No clipboard manager, copy-paste between device and computer will not work");
}
} }
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
@ -199,7 +205,11 @@ public final class Device {
} }
public String getClipboardText() { public String getClipboardText() {
CharSequence s = serviceManager.getClipboardManager().getText(); ClipboardManager clipboardManager = serviceManager.getClipboardManager();
if (clipboardManager == null) {
return null;
}
CharSequence s = clipboardManager.getText();
if (s == null) { if (s == null) {
return null; return null;
} }
@ -207,16 +217,30 @@ public final class Device {
} }
public boolean setClipboardText(String text) { public boolean setClipboardText(String text) {
ClipboardManager clipboardManager = serviceManager.getClipboardManager();
if (clipboardManager == null) {
return false;
}
String currentClipboard = getClipboardText();
if (currentClipboard == null || currentClipboard.equals(text)) {
// The clipboard already contains the requested text.
// Since pasting text from the computer involves setting the device clipboard, it could be set twice on a copy-paste. This would cause
// the clipboard listeners to be notified twice, and that would flood the Android keyboard clipboard history. To workaround this
// problem, do not explicitly set the clipboard text if it already contains the expected content.
return false;
}
isSettingClipboard.set(true); isSettingClipboard.set(true);
boolean ok = serviceManager.getClipboardManager().setText(text); boolean ok = clipboardManager.setText(text);
isSettingClipboard.set(false); isSettingClipboard.set(false);
return ok; return ok;
} }
/** /**
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants * @param mode one of the {@code POWER_MODE_*} constants
*/ */
public boolean setScreenPowerMode(int mode) { public static boolean setScreenPowerMode(int mode) {
IBinder d = SurfaceControl.getBuiltInDisplay(); IBinder d = SurfaceControl.getBuiltInDisplay();
if (d == null) { if (d == null) {
Ln.e("Could not get built-in display"); Ln.e("Could not get built-in display");

View file

@ -7,10 +7,10 @@ import java.nio.charset.StandardCharsets;
public class DeviceMessageWriter { public class DeviceMessageWriter {
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3; public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes
private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE]; private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
@ -21,7 +21,7 @@ public class DeviceMessageWriter {
String text = msg.getText(); String text = msg.getText();
byte[] raw = text.getBytes(StandardCharsets.UTF_8); byte[] raw = text.getBytes(StandardCharsets.UTF_8);
int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH);
buffer.putShort((short) len); buffer.putInt(len);
buffer.put(raw, 0, len); buffer.put(raw, 0, len);
output.write(rawBuffer, 0, buffer.position()); output.write(rawBuffer, 0, buffer.position());
break; break;

View file

@ -49,7 +49,7 @@ public final class Server {
} }
} }
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn); CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn, true);
boolean tunnelForward = options.isTunnelForward(); boolean tunnelForward = options.isTunnelForward();

View file

@ -77,7 +77,14 @@ public final class ServiceManager {
public ClipboardManager getClipboardManager() { public ClipboardManager getClipboardManager() {
if (clipboardManager == null) { if (clipboardManager == null) {
clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard")); IInterface clipboard = getService("clipboard", "android.content.IClipboard");
if (clipboard == null) {
// Some devices have no clipboard manager
// <https://github.com/Genymobile/scrcpy/issues/1440>
// <https://github.com/Genymobile/scrcpy/issues/1556>
return null;
}
clipboardManager = new ClipboardManager(clipboard);
} }
return clipboardManager; return clipboardManager;
} }

View file

@ -25,6 +25,7 @@ public class ControlMessageReaderTest {
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(KeyEvent.ACTION_UP); dos.writeByte(KeyEvent.ACTION_UP);
dos.writeInt(KeyEvent.KEYCODE_ENTER); dos.writeInt(KeyEvent.KEYCODE_ENTER);
dos.writeInt(5); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeInt(KeyEvent.META_CTRL_ON);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@ -37,6 +38,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(5, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
} }
@ -48,7 +50,7 @@ public class ControlMessageReaderTest {
DataOutputStream dos = new DataOutputStream(bos); DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
byte[] text = "testé".getBytes(StandardCharsets.UTF_8); byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeShort(text.length); dos.writeInt(text.length);
dos.write(text); dos.write(text);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@ -68,7 +70,7 @@ public class ControlMessageReaderTest {
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH]; byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH];
Arrays.fill(text, (byte) 'a'); Arrays.fill(text, (byte) 'a');
dos.writeShort(text.length); dos.writeInt(text.length);
dos.write(text); dos.write(text);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@ -218,7 +220,7 @@ public class ControlMessageReaderTest {
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
dos.writeByte(1); // paste dos.writeByte(1); // paste
byte[] text = "testé".getBytes(StandardCharsets.UTF_8); byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeShort(text.length); dos.writeInt(text.length);
dos.write(text); dos.write(text);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@ -228,9 +230,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals("testé", event.getText()); Assert.assertEquals("testé", event.getText());
Assert.assertTrue(event.getPaste());
boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
Assert.assertTrue(parse);
} }
@Test @Test
@ -246,7 +246,7 @@ public class ControlMessageReaderTest {
Arrays.fill(rawText, (byte) 'a'); Arrays.fill(rawText, (byte) 'a');
String text = new String(rawText, 0, rawText.length); String text = new String(rawText, 0, rawText.length);
dos.writeShort(rawText.length); dos.writeInt(rawText.length);
dos.write(rawText); dos.write(rawText);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@ -256,9 +256,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals(text, event.getText()); Assert.assertEquals(text, event.getText());
Assert.assertTrue(event.getPaste());
boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
Assert.assertTrue(parse);
} }
@Test @Test
@ -308,11 +306,13 @@ public class ControlMessageReaderTest {
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(KeyEvent.ACTION_UP); dos.writeByte(KeyEvent.ACTION_UP);
dos.writeInt(KeyEvent.KEYCODE_ENTER); dos.writeInt(KeyEvent.KEYCODE_ENTER);
dos.writeInt(0); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeInt(KeyEvent.META_CTRL_ON);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(MotionEvent.ACTION_DOWN); dos.writeByte(MotionEvent.ACTION_DOWN);
dos.writeInt(MotionEvent.BUTTON_PRIMARY); dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(1); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeInt(KeyEvent.META_CTRL_ON);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@ -322,12 +322,14 @@ public class ControlMessageReaderTest {
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(0, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
event = reader.next(); event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
Assert.assertEquals(1, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
} }
@ -341,6 +343,7 @@ public class ControlMessageReaderTest {
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(KeyEvent.ACTION_UP); dos.writeByte(KeyEvent.ACTION_UP);
dos.writeInt(KeyEvent.KEYCODE_ENTER); dos.writeInt(KeyEvent.KEYCODE_ENTER);
dos.writeInt(4); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeInt(KeyEvent.META_CTRL_ON);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
@ -353,6 +356,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(4, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
event = reader.next(); event = reader.next();
@ -360,6 +364,7 @@ public class ControlMessageReaderTest {
bos.reset(); bos.reset();
dos.writeInt(MotionEvent.BUTTON_PRIMARY); dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(5); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeInt(KeyEvent.META_CTRL_ON);
packet = bos.toByteArray(); packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet)); reader.readFrom(new ByteArrayInputStream(packet));
@ -369,6 +374,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
Assert.assertEquals(5, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
} }
} }

View file

@ -19,7 +19,7 @@ public class DeviceMessageWriterTest {
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos); DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(DeviceMessage.TYPE_CLIPBOARD); dos.writeByte(DeviceMessage.TYPE_CLIPBOARD);
dos.writeShort(data.length); dos.writeInt(data.length);
dos.write(data); dos.write(data);
byte[] expected = bos.toByteArray(); byte[] expected = bos.toByteArray();