diff --git a/BUILD.md b/BUILD.md index 475580f82d..d03af618be 100644 --- a/BUILD.md +++ b/BUILD.md @@ -195,8 +195,7 @@ Then, build: ```bash meson x --buildtype release --strip -Db_lto=true -cd x -ninja +ninja -Cx ``` _Note: `ninja` [must][ninja-user] be run as a non-root user (only `ninja @@ -219,13 +218,13 @@ To run without installing: After a successful build, you can install _scrcpy_ on the system: ```bash -sudo ninja install # without sudo on Windows +sudo ninja -Cx install # without sudo on Windows ``` This installs two files: - `/usr/local/bin/scrcpy` - - `/usr/local/share/scrcpy/scrcpy-server.jar` + - `/usr/local/share/scrcpy/scrcpy-server` Just remove them to "uninstall" the application. @@ -244,8 +243,7 @@ configuration: ```bash meson x --buildtype release --strip -Db_lto=true \ - -Dprebuilt_server=/path/to/scrcpy-server.jar -cd x -ninja -sudo ninja install + -Dprebuilt_server=/path/to/scrcpy-server +ninja -Cx +sudo ninja -Cx install ``` diff --git a/DEVELOP.md b/DEVELOP.md index dea8137d08..92c3ce8794 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -3,7 +3,7 @@ ## Overview This application is composed of two parts: - - the server (`scrcpy-server.jar`), to be executed on the device, + - the server (`scrcpy-server`), to be executed on the device, - the client (the `scrcpy` binary), executed on the host computer. The client is responsible to push the server to the device and start its @@ -49,7 +49,7 @@ application may not replace the server just before the client executes it._ Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing `classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle build system, the server is built to an (unsigned) APK (renamed to -`scrcpy-server.jar`). +`scrcpy-server`). [dex]: https://en.wikipedia.org/wiki/Dalvik_(software) [apk]: https://en.wikipedia.org/wiki/Android_application_package @@ -268,3 +268,33 @@ For more details, go read the code! If you find a bug, or have an awesome idea to implement, please discuss and contribute ;-) + + +### Debug the server + +The server is pushed to the device by the client on startup. + +To debug it, enable the server debugger during configuration: + +```bash +meson x -Dserver_debugger=true +# or, if x is already configured +meson configure x -Dserver_debugger=true +``` + +Then recompile. + +When you start scrcpy, it will start a debugger on port 5005 on the device. +Redirect that port to the computer: + +```bash +adb forward tcp:5005 tcp:5005 +``` + +In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on +`+`, _Remote_, and fill the form: + + - Host: `localhost` + - Port: `5005` + +Then click on _Debug_. diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index c07cb24f79..59f5a30250 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -3,7 +3,7 @@ # # Here, "portable" means that the client and server binaries are expected to be # anywhere, but in the same directory, instead of well-defined separate -# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server.jar). +# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server). # # In particular, this implies to change the location from where the client push # the server to the device. @@ -97,7 +97,7 @@ build-win64-noconsole: prepare-deps-win64 dist-win32: build-server build-win32 build-win32-noconsole mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(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_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -112,7 +112,7 @@ dist-win32: build-server build-win32 build-win32-noconsole dist-win64: build-server build-win64 build-win64-noconsole mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(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_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" diff --git a/README.md b/README.md index f698cb4c9b..f0717c2a07 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,6 @@ This is useful for example to mirror only one eye of the Oculus Go: ```bash scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) -scrcpy -c 1224:1440:0:0 # short version ``` If `--max-size` is also specified, resizing is applied after cropping. @@ -226,7 +225,6 @@ The window of app can always be above others by: ```bash scrcpy --always-on-top -scrcpy -T # short version ``` @@ -358,7 +356,7 @@ To use a specific _adb_ binary, configure its path in the environment variable ADB=/path/to/adb scrcpy -To override the path of the `scrcpy-server.jar` file, configure its path in +To override the path of the `scrcpy-server` file, configure its path in `SCRCPY_SERVER_PATH`. [useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 diff --git a/app/meson.build b/app/meson.build index ccd05fee42..145e0ef61c 100644 --- a/app/meson.build +++ b/app/meson.build @@ -93,7 +93,7 @@ conf.set_quoted('SCRCPY_VERSION', meson.project_version()) # the prefix used during configuration (meson --prefix=PREFIX) conf.set_quoted('PREFIX', get_option('prefix')) -# build a "portable" version (with scrcpy-server.jar accessible from the same +# build a "portable" version (with scrcpy-server accessible from the same # directory as the executable) conf.set('PORTABLE', get_option('portable')) @@ -115,6 +115,9 @@ conf.set('HIDPI_SUPPORT', get_option('hidpi_support')) # disable console on Windows conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole')) +# run a server debugger and wait for a client to be attached +conf.set('SERVER_DEBUGGER', get_option('server_debugger')) + configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') @@ -134,6 +137,8 @@ executable('scrcpy', src, c_args: c_args, link_args: link_args) +install_man('scrcpy.1') + ### TESTS diff --git a/app/scrcpy.1 b/app/scrcpy.1 new file mode 100644 index 0000000000..203395a49f --- /dev/null +++ b/app/scrcpy.1 @@ -0,0 +1,238 @@ +.TH "scrcpy" "1" +.SH NAME +scrcpy \- Display and control your Android device + + +.SH SYNOPSIS +.B scrcpy +.RI [ options ] + + +.SH DESCRIPTION +.B scrcpy +provides display and control of Android devices connected on USB (or over TCP/IP). It does not require any root access. + + +.SH OPTIONS + +.TP +.BI "\-b, \-\-bit\-rate " value +Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). + +Default is 8000000. + +.TP +.BI "\-c, \-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy +Crop the device screen on the server. + +The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any +.B \-\-max\-size +value is computed on the cropped size. + +.TP +.B \-f, \-\-fullscreen +Start in fullscreen. + +.TP +.BI "\-F, \-\-record\-format " format +Force recording format (either mp4 or mkv). + +.TP +.B \-h, \-\-help +Print this help. + +.TP +.BI "\-m, \-\-max\-size " value +Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. + +Default is 0 (unlimited). + +.TP +.B \-n, \-\-no\-control +Disable device control (mirror the device in read\-only). + +.TP +.B \-N, \-\-no\-display +Do not display device (only when screen recording is enabled). + +.TP +.BI "\-p, \-\-port " port +Set the TCP port the client listens on. + +Default is 27183. + +.TP +.B \-\-prefer\-text +Inject alpha characters and space as text events instead of key events. + +This avoids issues when combining multiple keys to enter special characters, +but breaks the expected behavior of alpha keys in games (typically WASD). + +.TP +.BI "\-\-push\-target " path +Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push". + +Default is "/sdcard/". + +.TP +.BI "\-r, \-\-record " file +Record screen to +.IR file . + +The format is determined by the +.B \-F/\-\-record\-format +option if set, or by the file extension (.mp4 or .mkv). + +.TP +.B \-\-render\-expired\-frames +By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency. + +.TP +.BI "\-s, \-\-serial " number +The device serial number. Mandatory only if several devices are connected to adb. + +.TP +.B \-S, \-\-turn\-screen\-off +Turn the device screen off immediately. + +.TP +.B \-t, \-\-show\-touches +Enable "show touches" on start, disable on quit. + +It only shows physical touches (not clicks from scrcpy). + +.TP +.B \-T, \-\-always\-on\-top +Make scrcpy window always on top (above other windows). + +.TP +.B \-v, \-\-version +Print the version of scrcpy. + +.TP +.BI \-\-window\-title " text +Set a custom window title. + + +.SH SHORTCUTS + +.TP +.B Ctrl+f +switch fullscreen mode + +.TP +.B Ctrl+g +resize window to 1:1 (pixel\-perfect) + +.TP +.B Ctrl+x, Double\-click on black borders +resize window to remove black borders + +.TP +.B Ctrl+h, Home, Middle\-click +Click on HOME + +.TP +.B Ctrl+b, Ctrl+Backspace, Right\-click (when screen is on) +Click on BACK + +.TP +.B Ctrl+s +Click on APP_SWITCH + +.TP +.B Ctrl+m +Click on MENU + +.TP +.B Ctrl+Up +Click on VOLUME_UP + +.TP +.B Ctrl+Down +Click on VOLUME_DOWN + +.TP +.B Ctrl+p +Click on POWER (turn screen on/off) + +.TP +.B Right\-click (when screen is off) +turn screen on + +.TP +.B Ctrl+o +turn device screen off (keep mirroring) + +.TP +.B Ctrl+n +expand notification panel + +.TP +.B Ctrl+Shift+n +collapse notification panel + +.TP +.B Ctrl+c +copy device clipboard to computer + +.TP +.B Ctrl+v +paste computer clipboard to device + +.TP +.B Ctrl+Shift+v +copy computer clipboard to device + +.TP +.B Ctrl+i +enable/disable FPS counter (print frames/second in logs) + +.TP +.B Drag & drop APK file +install APK from computer + + +.SH Environment variables + +.TP +.B ADB +Specify the path to adb. + +.TP +.B SCRCPY_SERVER_PATH +Specify the path to server binary. + + +.SH AUTHORS +.B scrcpy +is written by Romain Vimont. + +This manual page was written by +.MT mmyangfl@gmail.com +Yangfl +.ME +for the Debian Project (and may be used by others). + + +.SH "REPORTING BUGS" +Report bugs to +.UR https://github.com/Genymobile/scrcpy/issues +.UE . + +.SH COPYRIGHT +Copyright \(co 2018 Genymobile +.UR https://www.genymobile.com +Genymobile +.UE + +Copyright \(co 2018\-2019 +.MT rom@rom1v.com +Romain Vimont +.ME + +Licensed under the Apache License, Version 2.0. + +.SH WWW +.UR https://github.com/Genymobile/scrcpy +.UE diff --git a/app/src/event_converter.c b/app/src/event_converter.c index a634614ee7..80ead6153d 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -5,7 +5,7 @@ #define MAP(FROM, TO) case FROM: *to = TO; return true #define FAIL default: return false -static bool +bool convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { switch (from) { MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN); @@ -33,7 +33,7 @@ autocomplete_metastate(enum android_metastate metastate) { return metastate; } -static enum android_metastate +enum android_metastate convert_meta_state(SDL_Keymod mod) { enum android_metastate metastate = 0; if (mod & KMOD_LSHIFT) { @@ -74,8 +74,9 @@ convert_meta_state(SDL_Keymod mod) { return autocomplete_metastate(metastate); } -static bool -convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { +bool +convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, + bool prefer_text) { switch (from) { MAP(SDLK_RETURN, AKEYCODE_ENTER); MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER); @@ -92,6 +93,12 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN); MAP(SDLK_UP, AKEYCODE_DPAD_UP); } + + if (prefer_text) { + // do not forward alpha and space key events + return false; + } + if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) { return false; } @@ -128,7 +135,7 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { } } -static enum android_motionevent_buttons +enum android_motionevent_buttons convert_mouse_buttons(uint32_t state) { enum android_motionevent_buttons buttons = 0; if (state & SDL_BUTTON_LMASK) { @@ -150,24 +157,6 @@ convert_mouse_buttons(uint32_t state) { } bool -convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; - - if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { - return false; - } - - uint16_t mod = from->keysym.mod; - if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) { - return false; - } - - to->inject_keycode.metastate = convert_meta_state(mod); - - return true; -} - -static bool convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { switch (from) { MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); @@ -177,41 +166,6 @@ convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { } bool -convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - - if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) { - return false; - } - - to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; - to->inject_touch_event.position.screen_size = screen_size; - to->inject_touch_event.position.point.x = from->x; - to->inject_touch_event.position.point.y = from->y; - to->inject_touch_event.pressure = 1.f; - to->inject_touch_event.buttons = - convert_mouse_buttons(SDL_BUTTON(from->button)); - - return true; -} - -bool -convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; - to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; - to->inject_touch_event.position.screen_size = screen_size; - to->inject_touch_event.position.point.x = from->x; - to->inject_touch_event.position.point.y = from->y; - to->inject_touch_event.pressure = 1.f; - to->inject_touch_event.buttons = convert_mouse_buttons(from->state); - - return true; -} - -static bool convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { switch (from) { MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE); @@ -220,39 +174,3 @@ convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { FAIL; } } - -bool -convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - - if (!convert_touch_action(from->type, &to->inject_touch_event.action)) { - return false; - } - - to->inject_touch_event.pointer_id = from->fingerId; - to->inject_touch_event.position.screen_size = screen_size; - // SDL touch event coordinates are normalized in the range [0; 1] - to->inject_touch_event.position.point.x = from->x * screen_size.width; - to->inject_touch_event.position.point.y = from->y * screen_size.height; - to->inject_touch_event.pressure = from->pressure; - to->inject_touch_event.buttons = 0; - return true; -} - -bool -convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; - - to->inject_scroll_event.position = position; - - int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1; - // SDL behavior seems inconsistent between horizontal and vertical scrolling - // so reverse the horizontal - // - to->inject_scroll_event.hscroll = -mul * from->x; - to->inject_scroll_event.vscroll = mul * from->y; - - return true; -} diff --git a/app/src/event_converter.h b/app/src/event_converter.h index f6f136a360..c41887e1e5 100644 --- a/app/src/event_converter.h +++ b/app/src/event_converter.h @@ -7,36 +7,23 @@ #include "config.h" #include "control_msg.h" -struct complete_mouse_motion_event { - SDL_MouseMotionEvent *mouse_motion_event; - struct size screen_size; -}; - -struct complete_mouse_wheel_event { - SDL_MouseWheelEvent *mouse_wheel_event; - struct point position; -}; - bool -convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to); +convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to); -bool -convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, - struct control_msg *to); +enum android_metastate +convert_meta_state(SDL_Keymod mod); -// the video size may be different from the real device size, so we need the -// size to which the absolute position apply, to scale it accordingly bool -convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, - struct control_msg *to); +convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, + bool prefer_text); + +enum android_motionevent_buttons +convert_mouse_buttons(uint32_t state); bool -convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, - struct control_msg *to); +convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to); -// on Android, a scroll event requires the current mouse position bool -convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, - struct control_msg *to); +convert_touch_action(SDL_EventType from, enum android_motionevent_action *to); #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index a3d3854379..5fc1e5dcca 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -224,8 +224,17 @@ clipboard_paste(struct controller *controller) { } void -input_manager_process_text_input(struct input_manager *input_manager, +input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { + if (!im->prefer_text) { + char c = event->text[0]; + if (isalpha(c) || c == ' ') { + SDL_assert(event->text[1] == '\0'); + // letters and space are handled as raw key event + return; + } + } + struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = SDL_strdup(event->text); @@ -233,14 +242,34 @@ input_manager_process_text_input(struct input_manager *input_manager, LOGW("Could not strdup input text"); return; } - if (!controller_push_msg(input_manager->controller, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { SDL_free(msg.inject_text.text); LOGW("Could not request 'inject text'"); } } +static bool +convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, + bool prefer_text) { + to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; + + if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { + return false; + } + + uint16_t mod = from->keysym.mod; + if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod, + prefer_text)) { + return false; + } + + to->inject_keycode.metastate = convert_meta_state(mod); + + return true; +} + void -input_manager_process_key(struct input_manager *input_manager, +input_manager_process_key(struct input_manager *im, const SDL_KeyboardEvent *event, bool control) { // control: indicates the state of the command-line option --no-control @@ -267,7 +296,7 @@ input_manager_process_key(struct input_manager *input_manager, return; } - struct controller *controller = input_manager->controller; + struct controller *controller = im->controller; // capture all Ctrl events if (ctrl || cmd) { @@ -342,23 +371,23 @@ input_manager_process_key(struct input_manager *input_manager, return; case SDLK_f: if (!shift && cmd && !repeat && down) { - screen_switch_fullscreen(input_manager->screen); + screen_switch_fullscreen(im->screen); } return; case SDLK_x: if (!shift && cmd && !repeat && down) { - screen_resize_to_fit(input_manager->screen); + screen_resize_to_fit(im->screen); } return; case SDLK_g: if (!shift && cmd && !repeat && down) { - screen_resize_to_pixel_perfect(input_manager->screen); + screen_resize_to_pixel_perfect(im->screen); } return; case SDLK_i: if (!shift && cmd && !repeat && down) { struct fps_counter *fps_counter = - input_manager->video_buffer->fps_counter; + im->video_buffer->fps_counter; switch_fps_counter_state(fps_counter); } return; @@ -390,15 +419,30 @@ input_manager_process_key(struct input_manager *input_manager, } struct control_msg msg; - if (convert_input_key(event, &msg)) { + if (convert_input_key(event, &msg, im->prefer_text)) { if (!controller_push_msg(controller, &msg)) { LOGW("Could not request 'inject keycode'"); } } } +static bool +convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; + to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; + to->inject_touch_event.position.screen_size = screen->frame_size; + to->inject_touch_event.position.point.x = from->x; + to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.pressure = 1.f; + to->inject_touch_event.buttons = convert_mouse_buttons(from->state); + + return true; +} + void -input_manager_process_mouse_motion(struct input_manager *input_manager, +input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event) { if (!event->state) { // do not send motion events when no button is pressed @@ -409,33 +453,74 @@ input_manager_process_mouse_motion(struct input_manager *input_manager, return; } struct control_msg msg; - if (convert_mouse_motion(event, input_manager->screen->frame_size, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (convert_mouse_motion(event, im->screen, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse motion event'"); } } } +static bool +convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + + if (!convert_touch_action(from->type, &to->inject_touch_event.action)) { + return false; + } + + struct size frame_size = screen->frame_size; + + to->inject_touch_event.pointer_id = from->fingerId; + to->inject_touch_event.position.screen_size = frame_size; + // SDL touch event coordinates are normalized in the range [0; 1] + to->inject_touch_event.position.point.x = from->x * frame_size.width; + to->inject_touch_event.position.point.y = from->y * frame_size.height; + to->inject_touch_event.pressure = from->pressure; + to->inject_touch_event.buttons = 0; + return true; +} + void -input_manager_process_touch(struct input_manager *input_manager, +input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event) { struct control_msg msg; - if (convert_touch(event, input_manager->screen->frame_size, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (convert_touch(event, im->screen, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject touch event'"); } } } static bool -is_outside_device_screen(struct input_manager *input_manager, int x, int y) +is_outside_device_screen(struct input_manager *im, int x, int y) { - return x < 0 || x >= input_manager->screen->frame_size.width || - y < 0 || y >= input_manager->screen->frame_size.height; + return x < 0 || x >= im->screen->frame_size.width || + y < 0 || y >= im->screen->frame_size.height; +} + +static bool +convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + + if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) { + return false; + } + + to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; + to->inject_touch_event.position.screen_size = screen->frame_size; + to->inject_touch_event.position.point.x = from->x; + to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.pressure = 1.f; + to->inject_touch_event.buttons = + convert_mouse_buttons(SDL_BUTTON(from->button)); + + return true; } void -input_manager_process_mouse_button(struct input_manager *input_manager, +input_manager_process_mouse_button(struct input_manager *im, const SDL_MouseButtonEvent *event, bool control) { if (event->which == SDL_TOUCH_MOUSEID) { @@ -444,19 +529,19 @@ input_manager_process_mouse_button(struct input_manager *input_manager, } if (event->type == SDL_MOUSEBUTTONDOWN) { if (control && event->button == SDL_BUTTON_RIGHT) { - press_back_or_turn_screen_on(input_manager->controller); + press_back_or_turn_screen_on(im->controller); return; } if (control && event->button == SDL_BUTTON_MIDDLE) { - action_home(input_manager->controller, ACTION_DOWN | ACTION_UP); + action_home(im->controller, ACTION_DOWN | ACTION_UP); return; } // double-click on black borders resize to fit the device screen if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { bool outside = - is_outside_device_screen(input_manager, event->x, event->y); + is_outside_device_screen(im, event->x, event->y); if (outside) { - screen_resize_to_fit(input_manager->screen); + screen_resize_to_fit(im->screen); return; } } @@ -468,23 +553,41 @@ input_manager_process_mouse_button(struct input_manager *input_manager, } struct control_msg msg; - if (convert_mouse_button(event, input_manager->screen->frame_size, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (convert_mouse_button(event, im->screen, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse button event'"); } } } -void -input_manager_process_mouse_wheel(struct input_manager *input_manager, - const SDL_MouseWheelEvent *event) { +static bool +convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen, + struct control_msg *to) { struct position position = { - .screen_size = input_manager->screen->frame_size, - .point = get_mouse_point(input_manager->screen), + .screen_size = screen->frame_size, + .point = get_mouse_point(screen), }; + + to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; + + to->inject_scroll_event.position = position; + + int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1; + // SDL behavior seems inconsistent between horizontal and vertical scrolling + // so reverse the horizontal + // + to->inject_scroll_event.hscroll = -mul * from->x; + to->inject_scroll_event.vscroll = mul * from->y; + + return true; +} + +void +input_manager_process_mouse_wheel(struct input_manager *im, + const SDL_MouseWheelEvent *event) { struct control_msg msg; - if (convert_mouse_wheel(event, position, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (convert_mouse_wheel(event, im->screen, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse wheel event'"); } } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 0009cb814f..43fc0eeb38 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -14,32 +14,33 @@ struct input_manager { struct controller *controller; struct video_buffer *video_buffer; struct screen *screen; + bool prefer_text; }; void -input_manager_process_text_input(struct input_manager *input_manager, +input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event); void -input_manager_process_key(struct input_manager *input_manager, +input_manager_process_key(struct input_manager *im, const SDL_KeyboardEvent *event, bool control); void -input_manager_process_mouse_motion(struct input_manager *input_manager, +input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event); void -input_manager_process_touch(struct input_manager *input_manager, +input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event); void -input_manager_process_mouse_button(struct input_manager *input_manager, +input_manager_process_mouse_button(struct input_manager *im, const SDL_MouseButtonEvent *event, bool control); void -input_manager_process_mouse_wheel(struct input_manager *input_manager, +input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event); #endif diff --git a/app/src/main.c b/app/src/main.c index c00bf41995..12c65ed474 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -14,24 +14,9 @@ #include "recorder.h" struct args { - const char *serial; - const char *crop; - const char *record_filename; - const char *window_title; - const char *push_target; - enum recorder_format record_format; - bool fullscreen; - bool no_control; - bool no_display; + struct scrcpy_options opts; bool help; bool version; - bool show_touches; - uint16_t port; - uint16_t max_size; - uint32_t bit_rate; - bool always_on_top; - bool turn_screen_off; - bool render_expired_frames; }; static void usage(const char *arg0) { @@ -59,7 +44,7 @@ static void usage(const char *arg0) { " -f, --fullscreen\n" " Start in fullscreen.\n" "\n" - " -F, --record-format\n" + " -F, --record-format format\n" " Force recording format (either mp4 or mkv).\n" "\n" " -h, --help\n" @@ -82,6 +67,13 @@ static void usage(const char *arg0) { " Set the TCP port the client listens on.\n" " Default is %d.\n" "\n" + " --prefer-text\n" + " Inject alpha characters and space as text events instead of\n" + " key events.\n" + " This avoids issues when combining multiple keys to enter a\n" + " special character, but breaks the expected behavior of alpha\n" + " keys in games (typically WASD).\n" + "\n" " --push-target path\n" " Set the target directory for pushing files to the device by\n" " drag & drop. It is passed as-is to \"adb push\".\n" @@ -312,50 +304,63 @@ guess_record_format(const char *filename) { #define OPT_RENDER_EXPIRED_FRAMES 1000 #define OPT_WINDOW_TITLE 1001 #define OPT_PUSH_TARGET 1002 +#define OPT_ALWAYS_ON_TOP 1003 +#define OPT_CROP 1004 +#define OPT_RECORD_FORMAT 1005 +#define OPT_PREFER_TEXT 1006 static bool parse_args(struct args *args, int argc, char *argv[]) { static const struct option long_options[] = { - {"always-on-top", no_argument, NULL, 'T'}, + {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, {"bit-rate", required_argument, NULL, 'b'}, - {"crop", required_argument, NULL, 'c'}, + {"crop", required_argument, NULL, OPT_CROP}, {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, {"max-size", required_argument, NULL, 'm'}, {"no-control", no_argument, NULL, 'n'}, {"no-display", no_argument, NULL, 'N'}, {"port", required_argument, NULL, 'p'}, - {"push-target", required_argument, NULL, - OPT_PUSH_TARGET}, + {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"record", required_argument, NULL, 'r'}, - {"record-format", required_argument, NULL, 'F'}, + {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, {"render-expired-frames", no_argument, NULL, - OPT_RENDER_EXPIRED_FRAMES}, + OPT_RENDER_EXPIRED_FRAMES}, {"serial", required_argument, NULL, 's'}, {"show-touches", no_argument, NULL, 't'}, {"turn-screen-off", no_argument, NULL, 'S'}, + {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, {"version", no_argument, NULL, 'v'}, {"window-title", required_argument, NULL, - OPT_WINDOW_TITLE}, + OPT_WINDOW_TITLE}, {NULL, 0, NULL, 0 }, }; + + struct scrcpy_options *opts = &args->opts; + int c; while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, NULL)) != -1) { switch (c) { case 'b': - if (!parse_bit_rate(optarg, &args->bit_rate)) { + if (!parse_bit_rate(optarg, &opts->bit_rate)) { return false; } break; case 'c': - args->crop = optarg; + LOGW("Deprecated option -c. Use --crop instead."); + // fall through + case OPT_CROP: + opts->crop = optarg; break; case 'f': - args->fullscreen = true; + opts->fullscreen = true; break; case 'F': - if (!parse_record_format(optarg, &args->record_format)) { + LOGW("Deprecated option -F. Use --record-format instead."); + // fall through + case OPT_RECORD_FORMAT: + if (!parse_record_format(optarg, &opts->record_format)) { return false; } break; @@ -363,47 +368,53 @@ parse_args(struct args *args, int argc, char *argv[]) { args->help = true; break; case 'm': - if (!parse_max_size(optarg, &args->max_size)) { + if (!parse_max_size(optarg, &opts->max_size)) { return false; } break; case 'n': - args->no_control = true; + opts->control = false; break; case 'N': - args->no_display = true; + opts->display = false; break; case 'p': - if (!parse_port(optarg, &args->port)) { + if (!parse_port(optarg, &opts->port)) { return false; } break; case 'r': - args->record_filename = optarg; + opts->record_filename = optarg; break; case 's': - args->serial = optarg; + opts->serial = optarg; break; case 'S': - args->turn_screen_off = true; + opts->turn_screen_off = true; break; case 't': - args->show_touches = true; + opts->show_touches = true; break; case 'T': - args->always_on_top = true; + LOGW("Deprecated option -T. Use --always-on-top instead."); + // fall through + case OPT_ALWAYS_ON_TOP: + opts->always_on_top = true; break; case 'v': args->version = true; break; case OPT_RENDER_EXPIRED_FRAMES: - args->render_expired_frames = true; + opts->render_expired_frames = true; break; case OPT_WINDOW_TITLE: - args->window_title = optarg; + opts->window_title = optarg; break; case OPT_PUSH_TARGET: - args->push_target = optarg; + opts->push_target = optarg; + break; + case OPT_PREFER_TEXT: + opts->prefer_text = true; break; default: // getopt prints the error message on stderr @@ -411,12 +422,12 @@ parse_args(struct args *args, int argc, char *argv[]) { } } - if (args->no_display && !args->record_filename) { + if (!opts->display && !opts->record_filename) { LOGE("-N/--no-display requires screen recording (-r/--record)"); return false; } - if (args->no_display && args->fullscreen) { + if (!opts->display && opts->fullscreen) { LOGE("-f/--fullscreen-window is incompatible with -N/--no-display"); return false; } @@ -427,21 +438,21 @@ parse_args(struct args *args, int argc, char *argv[]) { return false; } - if (args->record_format && !args->record_filename) { + if (opts->record_format && !opts->record_filename) { LOGE("Record format specified without recording"); return false; } - if (args->record_filename && !args->record_format) { - args->record_format = guess_record_format(args->record_filename); - if (!args->record_format) { + if (opts->record_filename && !opts->record_format) { + opts->record_format = guess_record_format(opts->record_filename); + if (!opts->record_format) { LOGE("No format specified for \"%s\" (try with -F mkv)", - args->record_filename); + opts->record_filename); return false; } } - if (args->no_control && args->turn_screen_off) { + if (!opts->control && opts->turn_screen_off) { LOGE("Could not request to turn screen off if control is disabled"); return false; } @@ -458,24 +469,11 @@ main(int argc, char *argv[]) { setbuf(stderr, NULL); #endif struct args args = { - .serial = NULL, - .crop = NULL, - .record_filename = NULL, - .window_title = NULL, - .push_target = NULL, - .record_format = 0, + .opts = SCRCPY_OPTIONS_DEFAULT, .help = false, .version = false, - .show_touches = false, - .port = DEFAULT_LOCAL_PORT, - .max_size = DEFAULT_MAX_SIZE, - .bit_rate = DEFAULT_BIT_RATE, - .always_on_top = false, - .no_control = false, - .no_display = false, - .turn_screen_off = false, - .render_expired_frames = false, }; + if (!parse_args(&args, argc, argv)) { return 1; } @@ -504,25 +502,7 @@ main(int argc, char *argv[]) { SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); #endif - struct scrcpy_options options = { - .serial = args.serial, - .crop = args.crop, - .port = args.port, - .record_filename = args.record_filename, - .window_title = args.window_title, - .push_target = args.push_target, - .record_format = args.record_format, - .max_size = args.max_size, - .bit_rate = args.bit_rate, - .show_touches = args.show_touches, - .fullscreen = args.fullscreen, - .always_on_top = args.always_on_top, - .control = !args.no_control, - .display = !args.no_display, - .turn_screen_off = args.turn_screen_off, - .render_expired_frames = args.render_expired_frames, - }; - int res = scrcpy(&options) ? 0 : 1; + int res = scrcpy(&args.opts) ? 0 : 1; avformat_network_deinit(); // ignore failure diff --git a/app/src/recorder.c b/app/src/recorder.c index 7718635005..f6f6fd964b 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -135,6 +135,9 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { // recorder->ctx->oformat = (AVOutputFormat *) format; + av_dict_set(&recorder->ctx->metadata, "comment", + "Recorded by scrcpy " SCRCPY_VERSION, 0); + AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); if (!ostream) { avformat_free_context(recorder->ctx); @@ -171,9 +174,14 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { void recorder_close(struct recorder *recorder) { - int ret = av_write_trailer(recorder->ctx); - if (ret < 0) { - LOGE("Failed to write trailer to %s", recorder->filename); + if (recorder->header_written) { + int ret = av_write_trailer(recorder->ctx); + if (ret < 0) { + LOGE("Failed to write trailer to %s", recorder->filename); + recorder->failed = true; + } + } else { + // the recorded file is empty recorder->failed = true; } avio_close(recorder->ctx->pb); @@ -293,8 +301,12 @@ run_recorder(void *data) { continue; } - // we now know the duration of the previous packet - previous->packet.duration = rec->packet.pts - previous->packet.pts; + // config packets have no PTS, we must ignore them + if (rec->packet.pts != AV_NOPTS_VALUE + && previous->packet.pts != AV_NOPTS_VALUE) { + // we now know the duration of the previous packet + previous->packet.duration = rec->packet.pts - previous->packet.pts; + } bool ok = recorder_write(recorder, &previous->packet); record_packet_delete(previous); diff --git a/app/src/recorder.h b/app/src/recorder.h index b1953fcbf3..4ad77197c3 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -11,7 +11,8 @@ #include "queue.h" enum recorder_format { - RECORDER_FORMAT_MP4 = 1, + RECORDER_FORMAT_AUTO, + RECORDER_FORMAT_MP4, RECORDER_FORMAT_MKV, }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 25a0d6983e..ed20338140 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -42,6 +42,7 @@ static struct input_manager input_manager = { .controller = &controller, .video_buffer = &video_buffer, .screen = &screen, + .prefer_text = false, // initialized later }; // init SDL and set appropriate hints @@ -143,12 +144,7 @@ handle_event(SDL_Event *event, bool control) { } break; case SDL_WINDOWEVENT: - switch (event->window.event) { - case SDL_WINDOWEVENT_EXPOSED: - case SDL_WINDOWEVENT_SIZE_CHANGED: - screen_render(&screen); - break; - } + screen_handle_window_event(&screen, &event->window); break; case SDL_TEXTEDITING: break; @@ -223,6 +219,7 @@ event_loop(bool display, bool control) { case EVENT_RESULT_STOPPED_BY_USER: return true; case EVENT_RESULT_STOPPED_BY_EOS: + LOGW("Device disconnected"); return false; case EVENT_RESULT_CONTINUE: break; @@ -420,6 +417,8 @@ scrcpy(const struct scrcpy_options *options) { show_touches_waited = true; } + input_manager.prefer_text = options->prefer_text; + ret = event_loop(options->display, options->control); LOGD("quit..."); diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 1593fb1e02..70a41ec1ba 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -3,9 +3,10 @@ #include #include -#include #include "config.h" +#include "input_manager.h" +#include "recorder.h" struct scrcpy_options { const char *serial; @@ -24,8 +25,29 @@ struct scrcpy_options { bool display; bool turn_screen_off; bool render_expired_frames; + bool prefer_text; }; +#define SCRCPY_OPTIONS_DEFAULT { \ + .serial = NULL, \ + .crop = NULL, \ + .record_filename = NULL, \ + .window_title = NULL, \ + .push_target = NULL, \ + .record_format = RECORDER_FORMAT_AUTO, \ + .port = DEFAULT_LOCAL_PORT, \ + .max_size = DEFAULT_LOCAL_PORT, \ + .bit_rate = DEFAULT_BIT_RATE, \ + .show_touches = false, \ + .fullscreen = false, \ + .always_on_top = false, \ + .control = true, \ + .display = true, \ + .turn_screen_off = false, \ + .render_expired_frames = false, \ + .prefer_text = false, \ +} + bool scrcpy(const struct scrcpy_options *options); diff --git a/app/src/screen.c b/app/src/screen.c index 4bc4c5c5ac..7de5703129 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -30,23 +30,28 @@ get_window_size(SDL_Window *window) { // get the windowed window size static struct size get_windowed_window_size(const struct screen *screen) { - if (screen->fullscreen) { + if (screen->fullscreen || screen->maximized) { return screen->windowed_window_size; } return get_window_size(screen->window); } +// apply the windowed window size if fullscreen and maximized are disabled +static void +apply_windowed_size(struct screen *screen) { + if (!screen->fullscreen && !screen->maximized) { + SDL_SetWindowSize(screen->window, screen->windowed_window_size.width, + screen->windowed_window_size.height); + } +} + // set the window size to be applied when fullscreen is disabled static void set_window_size(struct screen *screen, struct size new_size) { // setting the window size during fullscreen is implementation defined, // so apply the resize only after fullscreen is disabled - if (screen->fullscreen) { - // SDL_SetWindowSize will be called when fullscreen will be disabled - screen->windowed_window_size = new_size; - } else { - SDL_SetWindowSize(screen->window, new_size.width, new_size.height); - } + screen->windowed_window_size = new_size; + apply_windowed_size(screen); } // get the preferred display bounds (i.e. the screen bounds with some margins) @@ -194,6 +199,8 @@ screen_init_rendering(struct screen *screen, const char *window_title, return false; } + screen->windowed_window_size = window_size; + return true; } @@ -287,10 +294,6 @@ screen_render(struct screen *screen) { void screen_switch_fullscreen(struct screen *screen) { - if (!screen->fullscreen) { - // going to fullscreen, store the current windowed window size - screen->windowed_window_size = get_window_size(screen->window); - } uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; if (SDL_SetWindowFullscreen(screen->window, new_mode)) { LOGW("Could not switch fullscreen mode: %s", SDL_GetError()); @@ -298,11 +301,7 @@ screen_switch_fullscreen(struct screen *screen) { } screen->fullscreen = !screen->fullscreen; - if (!screen->fullscreen) { - // fullscreen disabled, restore expected windowed window size - SDL_SetWindowSize(screen->window, screen->windowed_window_size.width, - screen->windowed_window_size.height); - } + apply_windowed_size(screen); LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); screen_render(screen); @@ -310,20 +309,75 @@ screen_switch_fullscreen(struct screen *screen) { void screen_resize_to_fit(struct screen *screen) { - if (!screen->fullscreen) { - struct size optimal_size = get_optimal_window_size(screen, - screen->frame_size); - SDL_SetWindowSize(screen->window, optimal_size.width, - optimal_size.height); - LOGD("Resized to optimal size"); + if (screen->fullscreen) { + return; + } + + if (screen->maximized) { + SDL_RestoreWindow(screen->window); + screen->maximized = false; } + + struct size optimal_size = + get_optimal_window_size(screen, screen->frame_size); + SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); + LOGD("Resized to optimal size"); } void screen_resize_to_pixel_perfect(struct screen *screen) { - if (!screen->fullscreen) { - SDL_SetWindowSize(screen->window, screen->frame_size.width, - screen->frame_size.height); - LOGD("Resized to pixel-perfect"); + if (screen->fullscreen) { + return; + } + + if (screen->maximized) { + SDL_RestoreWindow(screen->window); + screen->maximized = false; + } + + SDL_SetWindowSize(screen->window, screen->frame_size.width, + screen->frame_size.height); + LOGD("Resized to pixel-perfect"); +} + +void +screen_handle_window_event(struct screen *screen, + const SDL_WindowEvent *event) { + switch (event->event) { + case SDL_WINDOWEVENT_EXPOSED: + screen_render(screen); + break; + case SDL_WINDOWEVENT_SIZE_CHANGED: + if (!screen->fullscreen && !screen->maximized) { + // Backup the previous size: if we receive the MAXIMIZED event, + // then the new size must be ignored (it's the maximized size). + // We could not rely on the window flags due to race conditions + // (they could be updated asynchronously, at least on X11). + screen->windowed_window_size_backup = + screen->windowed_window_size; + + // Save the windowed size, so that it is available once the + // window is maximized or fullscreen is enabled. + screen->windowed_window_size = get_window_size(screen->window); + } + screen_render(screen); + break; + case SDL_WINDOWEVENT_MAXIMIZED: + // The backup size must be non-nul. + SDL_assert(screen->windowed_window_size_backup.width); + SDL_assert(screen->windowed_window_size_backup.height); + // Revert the last size, it was updated while screen was maximized. + screen->windowed_window_size = screen->windowed_window_size_backup; +#ifdef DEBUG + // Reset the backup to invalid values to detect unexpected usage + screen->windowed_window_size_backup.width = 0; + screen->windowed_window_size_backup.height = 0; +#endif + screen->maximized = true; + break; + case SDL_WINDOWEVENT_RESTORED: + screen->maximized = false; + apply_windowed_size(screen); + break; } } diff --git a/app/src/screen.h b/app/src/screen.h index bc18918967..275609ba72 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -15,28 +15,37 @@ struct screen { SDL_Renderer *renderer; SDL_Texture *texture; struct size frame_size; - //used only in fullscreen mode to know the windowed window size + // The window size the last time it was not maximized or fullscreen. struct size windowed_window_size; + // Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be + // able to revert the size to its non-maximized value. + struct size windowed_window_size_backup; bool has_frame; bool fullscreen; + bool maximized; bool no_window; }; -#define SCREEN_INITIALIZER { \ - .window = NULL, \ - .renderer = NULL, \ - .texture = NULL, \ - .frame_size = { \ - .width = 0, \ - .height = 0, \ - }, \ +#define SCREEN_INITIALIZER { \ + .window = NULL, \ + .renderer = NULL, \ + .texture = NULL, \ + .frame_size = { \ + .width = 0, \ + .height = 0, \ + }, \ .windowed_window_size = { \ - .width = 0, \ - .height = 0, \ - }, \ - .has_frame = false, \ - .fullscreen = false, \ - .no_window = false, \ + .width = 0, \ + .height = 0, \ + }, \ + .windowed_window_size_backup = { \ + .width = 0, \ + .height = 0, \ + }, \ + .has_frame = false, \ + .fullscreen = false, \ + .maximized = false, \ + .no_window = false, \ } // initialize default values @@ -76,4 +85,8 @@ screen_resize_to_fit(struct screen *screen); void screen_resize_to_pixel_perfect(struct screen *screen); +// react to window events +void +screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event); + #endif diff --git a/app/src/server.c b/app/src/server.c index 85b1b6b835..b40b065b71 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -13,7 +13,7 @@ #include "net.h" #define SOCKET_NAME "scrcpy" -#define SERVER_FILENAME "scrcpy-server.jar" +#define SERVER_FILENAME "scrcpy-server" #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME #define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME @@ -32,7 +32,7 @@ get_server_path(void) { // the absolute path is hardcoded return DEFAULT_SERVER_PATH; #else - // use scrcpy-server.jar in the same directory as the executable + // use scrcpy-server in the same directory as the executable char *executable_path = get_executable_path(); if (!executable_path) { LOGE("Could not get executable path, " @@ -124,8 +124,14 @@ execute_server(struct server *server, const struct server_params *params) { "shell", "CLASSPATH=/data/local/tmp/" SERVER_FILENAME, "app_process", +#ifdef SERVER_DEBUGGER +# define SERVER_DEBUGGER_PORT "5005" + "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" + SERVER_DEBUGGER_PORT, +#endif "/", // unused "com.genymobile.scrcpy.Server", + SCRCPY_VERSION, max_size_string, bit_rate_string, server->tunnel_forward ? "true" : "false", @@ -133,6 +139,17 @@ execute_server(struct server *server, const struct server_params *params) { "true", // always send frame meta (packet boundaries + timestamp) params->control ? "true" : "false", }; +#ifdef SERVER_DEBUGGER + LOGI("Server debugger waiting for a client on device port " + SERVER_DEBUGGER_PORT "..."); + // From the computer, run + // adb forward tcp:5005 tcp:5005 + // Then, from Android Studio: Run > Debug > Edit configurations... + // On the left, click on '+', "Remote", with: + // Host: localhost + // Port: 5005 + // Then click on "Debug" +#endif return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); } diff --git a/meson_options.txt b/meson_options.txt index d93161e33a..4cf4a8bfb8 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,5 +3,6 @@ option('compile_server', type: 'boolean', value: true, description: 'Build the s option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') -option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable') +option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') +option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') diff --git a/release.sh b/release.sh index fbd1eb5416..4c5afbf138 100755 --- a/release.sh +++ b/release.sh @@ -23,21 +23,21 @@ cd - make -f Makefile.CrossWindows # the generated server must be the same everywhere -cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win32/scrcpy-server.jar -cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win64/scrcpy-server.jar +cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win32/scrcpy-server +cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win64/scrcpy-server # get version name TAG=$(git describe --tags --always) # create release directory mkdir -p "release-$TAG" -cp "$BUILDDIR/server/scrcpy-server.jar" "release-$TAG/scrcpy-server-$TAG.jar" +cp "$BUILDDIR/server/scrcpy-server" "release-$TAG/scrcpy-server-$TAG" cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/" cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/" # generate checksums cd "release-$TAG" -sha256sum "scrcpy-server-$TAG.jar" \ +sha256sum "scrcpy-server-$TAG" \ "scrcpy-win32-$TAG.zip" \ "scrcpy-win64-$TAG.zip" > SHA256SUMS.txt diff --git a/run b/run index 7abeca059a..bfb499aed1 100755 --- a/run +++ b/run @@ -20,4 +20,4 @@ then exit 1 fi -SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server.jar" "$BUILDDIR/app/scrcpy" "$@" +SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" "$BUILDDIR/app/scrcpy" "$@" diff --git a/scripts/run-scrcpy.sh b/scripts/run-scrcpy.sh index fa6d7c8f43..f3130ee93a 100755 --- a/scripts/run-scrcpy.sh +++ b/scripts/run-scrcpy.sh @@ -1,2 +1,2 @@ #!/bin/bash -SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server.jar" "$MESON_BUILD_ROOT/app/scrcpy" +SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy" diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh new file mode 100755 index 0000000000..daf85008d9 --- /dev/null +++ b/server/build_without_gradle.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# +# This script generates the scrcpy binary "manually" (without gradle). +# +# Adapt Android platform and build tools versions (via ANDROID_PLATFORM and +# ANDROID_BUILD_TOOLS environment variables). +# +# Then execute: +# +# BUILD_DIR=my_build_dir ./build_without_gradle.sh + +set -e + +PLATFORM=${ANDROID_PLATFORM:-29} +BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} + +BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" +CLASSES_DIR="$BUILD_DIR/classes" +SERVER_DIR=$(dirname "$0") +SERVER_BINARY=scrcpy-server + +echo "Platform: android-$PLATFORM" +echo "Build-tools: $BUILD_TOOLS" +echo "Build dir: $BUILD_DIR" + +rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex +mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy" + +<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java" +package com.genymobile.scrcpy; + +public final class BuildConfig { + public static final boolean DEBUG = false; +} +EOF + +echo "Generating java from aidl..." +cd "$SERVER_DIR/src/main/aidl" +"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o "$CLASSES_DIR" \ + android/view/IRotationWatcher.aidl + +echo "Compiling java sources..." +cd ../java +javac -bootclasspath "$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" \ + -cp "$CLASSES_DIR" -d "$CLASSES_DIR" -source 1.8 -target 1.8 \ + com/genymobile/scrcpy/*.java \ + com/genymobile/scrcpy/wrappers/*.java + +echo "Dexing..." +cd "$CLASSES_DIR" +"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \ + --output "$BUILD_DIR/classes.dex" \ + android/view/*.class \ + com/genymobile/scrcpy/*.class \ + com/genymobile/scrcpy/wrappers/*.class + +echo "Archiving..." +cd "$BUILD_DIR" +jar cvf "$SERVER_BINARY" classes.dex +rm -rf classes.dex classes + +echo "Server generated in $BUILD_DIR/$SERVER_BINARY" diff --git a/server/meson.build b/server/meson.build index 0348144950..683a52c339 100644 --- a/server/meson.build +++ b/server/meson.build @@ -4,7 +4,7 @@ prebuilt_server = get_option('prebuilt_server') if prebuilt_server == '' custom_target('scrcpy-server', build_by_default: true, # gradle is responsible for tracking source changes - output: 'scrcpy-server.jar', + output: 'scrcpy-server', command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], console: true, install: true, @@ -16,7 +16,7 @@ else endif custom_target('scrcpy-server-prebuilt', input: prebuilt_server, - output: 'scrcpy-server.jar', + output: 'scrcpy-server', command: ['cp', '@INPUT@', '@OUTPUT@'], install: true, install_dir: 'share/scrcpy') diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 8357b06150..52f6f26b97 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -7,6 +7,7 @@ import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.os.IBinder; +import android.os.Looper; import android.view.Surface; import java.io.FileDescriptor; @@ -54,6 +55,16 @@ public boolean consumeRotationChange() { } public void streamScreen(Device device, FileDescriptor fd) throws IOException { + // Some devices internally create a Handler when creating an input Surface, causing an exception: + // "Can't create handler inside thread that has not called Looper.prepare()" + // + // + // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: + // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' + // on a null object reference" + // + Looper.prepareMainLooper(); + MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval); device.setRotationListener(this); boolean alive; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 3bd2fcdc87..ba44d07c28 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -7,7 +7,7 @@ public final class Server { - private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; + private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server"; private Server() { // not instantiable @@ -67,29 +67,39 @@ public void run() { @SuppressWarnings("checkstyle:MagicNumber") private static Options createOptions(String... args) { - if (args.length != 6) { - throw new IllegalArgumentException("Expecting 6 parameters"); + if (args.length < 1) { + throw new IllegalArgumentException("Missing client version"); + } + + String clientVersion = args[0]; + if (!clientVersion.equals(BuildConfig.VERSION_NAME)) { + throw new IllegalArgumentException("The server version (" + clientVersion + ") does not match the client " + + "(" + BuildConfig.VERSION_NAME + ")"); + } + + if (args.length != 7) { + throw new IllegalArgumentException("Expecting 7 parameters"); } Options options = new Options(); - int maxSize = Integer.parseInt(args[0]) & ~7; // multiple of 8 + int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8 options.setMaxSize(maxSize); - int bitRate = Integer.parseInt(args[1]); + int bitRate = Integer.parseInt(args[2]); options.setBitRate(bitRate); // use "adb forward" instead of "adb tunnel"? (so the server must listen) - boolean tunnelForward = Boolean.parseBoolean(args[2]); + boolean tunnelForward = Boolean.parseBoolean(args[3]); options.setTunnelForward(tunnelForward); - Rect crop = parseCrop(args[3]); + Rect crop = parseCrop(args[4]); options.setCrop(crop); - boolean sendFrameMeta = Boolean.parseBoolean(args[4]); + boolean sendFrameMeta = Boolean.parseBoolean(args[5]); options.setSendFrameMeta(sendFrameMeta); - boolean control = Boolean.parseBoolean(args[5]); + boolean control = Boolean.parseBoolean(args[6]); options.setControl(control); return options;