diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index f94a70a603..0c85431077 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -21,6 +21,7 @@ _scrcpy() { --disable-screensaver --display-buffer= --display-id= + --display-orientation= -e --select-tcpip -f --fullscreen --force-adb-forward @@ -50,6 +51,7 @@ _scrcpy() { --no-power-on --no-video --no-video-playback + --orientation= --otg -p --port= --pause-on-exit @@ -61,6 +63,7 @@ _scrcpy() { -r --record= --raw-key-events --record-format= + --record-orientation= --render-driver= --require-audio --rotation= @@ -112,8 +115,17 @@ _scrcpy() { COMPREPLY=($(compgen -W 'front back external' -- "$cur")) return ;; + --orientation + --display-orientation) + COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) + return + ;; + --record-orientation) + COMPREPLY=($(compgen -> '0 90 180 270' -- "$cur")) + return + ;; --lock-video-orientation) - COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) + COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur")) return ;; --pause-on-exit) @@ -132,10 +144,6 @@ _scrcpy() { COMPREPLY=($(compgen -W 'direct3d opengl opengles2 opengles metal software' -- "$cur")) return ;; - --rotation) - COMPREPLY=($(compgen -W '0 1 2 3' -- "$cur")) - return - ;; --shortcut-mod) # Only auto-complete a single key COMPREPLY=($(compgen -W 'lctrl rctrl lalt ralt lsuper rsuper' -- "$cur")) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index cc58b86666..3c7ca2178a 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -28,6 +28,7 @@ arguments=( '--disable-screensaver[Disable screensaver while scrcpy is running]' '--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]' '--display-id=[Specify the display id to mirror]' + '--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' @@ -40,7 +41,7 @@ arguments=( '--list-cameras[List cameras available on the device]' '--list-displays[List displays available on the device]' '--list-encoders[List video and audio encoders available on the device]' - '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)' + '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)' {-m,--max-size=}'[Limit both the width and height of the video to value]' {-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]' '--max-fps=[Limit the frame rate of screen capture]' @@ -56,6 +57,7 @@ arguments=( '--no-power-on[Do not power on the device on start]' '--no-video[Disable video forwarding]' '--no-video-playback[Disable video playback]' + '--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' '--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]' {-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]' '--pause-on-exit=[Make scrcpy pause before exiting]:mode:(true false if-error)' @@ -66,9 +68,9 @@ arguments=( {-r,--record=}'[Record screen to file]:record file:_files' '--raw-key-events[Inject key events for all input keys, and ignore text events]' '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac wav)' + '--record-orientation=[Set the record orientation]:orientation values:(0 90 180 270)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' - '--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)' {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' {-S,--turn-screen-off}'[Turn the device screen off immediately]' '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' diff --git a/app/meson.build b/app/meson.build index e0d9205006..b1233c6b2b 100644 --- a/app/meson.build +++ b/app/meson.build @@ -289,6 +289,10 @@ if get_option('buildtype') == 'debug' 'tests/test_device_msg_deserialize.c', 'src/device_msg.c', ]], + ['test_orientation', [ + 'tests/test_orientation.c', + 'src/options.c', + ]], ['test_strbuf', [ 'tests/test_strbuf.c', 'src/util/strbuf.c', diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1894119007..0c34b4e2a7 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -141,6 +141,14 @@ The available display ids can be listed by \fB\-\-list\-displays\fR. Default is 0. +.TP +.BI "\-\-display\-orientation " value +Set the initial display orientation. + +Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270. The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation. + +Default is 0. + .TP .B \-e, \-\-select\-tcpip Use TCP/IP device (if there is exactly one, like adb -e). @@ -207,7 +215,9 @@ List displays available on the device. .TP \fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR] -Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise. +Lock capture video orientation to \fIvalue\fR. + +Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 90, 180, and 270. The values represent the clockwise rotation from the natural device orientation, in degrees. Default is "unlocked". @@ -289,6 +299,10 @@ Disable video forwarding. .B \-\-no\-video\-playback Disable video playback on the computer. +.TP +.BI "\-\-orientation " value +Same as --display-orientation=value --record-orientation=value. + .TP .B \-\-otg Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. @@ -357,6 +371,14 @@ Inject key events for all input keys, and ignore text events. .BI "\-\-record\-format " format Force recording format (mp4, mkv, m4a, mka, opus, aac, flac or wav). +.TP +.BI "\-\-record\-orientation " value +Set the record orientation. + +Possible values are 0, 90, 180 and 270. The number represents the clockwise rotation in degrees. + +Default is 0. + .TP .BI "\-\-render\-driver " name Request SDL to use the given render driver (this is just a hint). @@ -369,10 +391,6 @@ Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "me .B \-\-require\-audio By default, scrcpy mirrors only the video if audio capture fails on the device. This option makes scrcpy fail if audio is enabled but does not work. -.TP -.BI "\-\-rotation " value -Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise. - .TP .BI "\-s, \-\-serial " number The device serial number. Mandatory only if several devices are connected to adb. @@ -534,6 +552,14 @@ Rotate display left .B MOD+Right Rotate display right +.TP +.B MOD+Shift+Left, MOD+Shift+Right +Flip display horizontally + +.TP +.B MOD+Shift+Up, MOD+Shift+Down +Flip display vertically + .TP .B MOD+g Resize window to 1:1 (pixel\-perfect) diff --git a/app/src/cli.c b/app/src/cli.c index 12ed8111d3..8b3ae8af22 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -90,6 +90,9 @@ enum { OPT_CAMERA_AR, OPT_CAMERA_FPS, OPT_CAMERA_HIGH_SPEED, + OPT_DISPLAY_ORIENTATION, + OPT_RECORD_ORIENTATION, + OPT_ORIENTATION, }; struct sc_option { @@ -309,6 +312,17 @@ static const struct sc_option options[] = { " scrcpy --list-displays\n" "Default is 0.", }, + { + .longopt_id = OPT_DISPLAY_ORIENTATION, + .longopt = "display-orientation", + .argdesc = "value", + .text = "Set the initial display orientation.\n" + "Possible values are 0, 90, 180, 270, flip0, flip90, flip180 " + "and flip270. The number represents the clockwise rotation " + "in degrees; the \"flip\" keyword applies a horizontal flip " + "before the rotation.\n" + "Default is 0.", + }, { .shortopt = 'e', .longopt = "select-tcpip", @@ -399,11 +413,11 @@ static const struct sc_option options[] = { .longopt = "lock-video-orientation", .argdesc = "value", .optional_arg = true, - .text = "Lock video orientation to value.\n" + .text = "Lock capture video orientation to value.\n" "Possible values are \"unlocked\", \"initial\" (locked to the " - "initial orientation), 0, 1, 2 and 3. Natural device " - "orientation is 0, and each increment adds a 90 degrees " - "rotation counterclockwise.\n" + "initial orientation), 0, 90, 180 and 270. The values " + "represent the clockwise rotation from the natural device " + "orientation, in degrees.\n" "Default is \"unlocked\".\n" "Passing the option without argument is equivalent to passing " "\"initial\".", @@ -512,6 +526,13 @@ static const struct sc_option options[] = { .longopt = "no-video-playback", .text = "Disable video playback on the computer.", }, + { + .longopt_id = OPT_ORIENTATION, + .longopt = "orientation", + .argdesc = "value", + .text = "Same as --display-orientation=value " + "--record-orientation=value.", + }, { .longopt_id = OPT_OTG, .longopt = "otg", @@ -597,6 +618,15 @@ static const struct sc_option options[] = { .text = "Force recording format (mp4, mkv, m4a, mka, opus, aac, flac " "or wav).", }, + { + .longopt_id = OPT_RECORD_ORIENTATION, + .longopt = "record-orientation", + .argdesc = "value", + .text = "Set the record orientation.\n" + "Possible values are 0, 90, 180 and 270. The number represents " + "the clockwise rotation in degrees.\n" + "Default is 0.", + }, { .longopt_id = OPT_RENDER_DRIVER, .longopt = "render-driver", @@ -615,12 +645,10 @@ static const struct sc_option options[] = { "is enabled but does not work." }, { + // deprecated .longopt_id = OPT_ROTATION, .longopt = "rotation", .argdesc = "value", - .text = "Set the initial display rotation.\n" - "Possible values are 0, 1, 2 and 3. Each increment adds a 90 " - "degrees rotation counterclockwise.", }, { .shortopt = 's', @@ -824,6 +852,14 @@ static const struct sc_shortcut shortcuts[] = { .shortcuts = { "MOD+Right" }, .text = "Rotate display right", }, + { + .shortcuts = { "MOD+Shift+Left", "MOD+Shift+Right" }, + .text = "Flip display horizontally", + }, + { + .shortcuts = { "MOD+Shift+Up", "MOD+Shift+Down" }, + .text = "Flip display vertically", + }, { .shortcuts = { "MOD+g" }, .text = "Resize window to 1:1 (pixel-perfect)", @@ -1382,15 +1418,50 @@ parse_lock_video_orientation(const char *s, return true; } - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 3, - "lock video orientation"); - if (!ok) { - return false; + if (!strcmp(s, "0")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_0; + return true; } - *lock_mode = (enum sc_lock_video_orientation) value; - return true; + if (!strcmp(s, "90")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_90; + return true; + } + + if (!strcmp(s, "180")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_180; + return true; + } + + if (!strcmp(s, "270")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_270; + return true; + } + + if (!strcmp(s, "1")) { + LOGW("--lock-video-orientation=1 is deprecated, use " + "--lock-video-orientation=270 instead."); + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_270; + return true; + } + + if (!strcmp(s, "2")) { + LOGW("--lock-video-orientation=2 is deprecated, use " + "--lock-video-orientation=180 instead."); + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_180; + return true; + } + + if (!strcmp(s, "3")) { + LOGW("--lock-video-orientation=3 is deprecated, use " + "--lock-video-orientation=90 instead."); + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_90; + return true; + } + + LOGE("Unsupported --lock-video-orientation value: %s (expected initial, " + "unlocked, 0, 90, 180 or 270).", s); + return false; } static bool @@ -1405,6 +1476,45 @@ parse_rotation(const char *s, uint8_t *rotation) { return true; } +static bool +parse_orientation(const char *s, enum sc_orientation *orientation) { + if (!strcmp(s, "0")) { + *orientation = SC_ORIENTATION_0; + return true; + } + if (!strcmp(s, "90")) { + *orientation = SC_ORIENTATION_90; + return true; + } + if (!strcmp(s, "180")) { + *orientation = SC_ORIENTATION_180; + return true; + } + if (!strcmp(s, "270")) { + *orientation = SC_ORIENTATION_270; + return true; + } + if (!strcmp(s, "flip0")) { + *orientation = SC_ORIENTATION_FLIP_0; + return true; + } + if (!strcmp(s, "flip90")) { + *orientation = SC_ORIENTATION_FLIP_90; + return true; + } + if (!strcmp(s, "flip180")) { + *orientation = SC_ORIENTATION_FLIP_180; + return true; + } + if (!strcmp(s, "flip270")) { + *orientation = SC_ORIENTATION_FLIP_270; + return true; + } + LOGE("Unsupported orientation: %s (expected 0, 90, 180, 270, flip0, " + "flip90, flip180 or flip270)", optarg); + return false; +} + static bool parse_window_position(const char *s, int16_t *position) { // special value for "auto" @@ -2008,9 +2118,49 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW; break; case OPT_ROTATION: - if (!parse_rotation(optarg, &opts->rotation)) { + LOGW("--rotation is deprecated, use --display-orientation " + "instead."); + uint8_t rotation; + if (!parse_rotation(optarg, &rotation)) { + return false; + } + assert(rotation <= 3); + switch (rotation) { + case 0: + opts->display_orientation = SC_ORIENTATION_0; + break; + case 1: + // rotation 1 was 90° counterclockwise, but orientation + // is expressed clockwise + opts->display_orientation = SC_ORIENTATION_270; + break; + case 2: + opts->display_orientation = SC_ORIENTATION_180; + break; + case 3: + // rotation 1 was 270° counterclockwise, but orientation + // is expressed clockwise + opts->display_orientation = SC_ORIENTATION_90; + break; + } + break; + case OPT_DISPLAY_ORIENTATION: + if (!parse_orientation(optarg, &opts->display_orientation)) { + return false; + } + break; + case OPT_RECORD_ORIENTATION: + if (!parse_orientation(optarg, &opts->record_orientation)) { + return false; + } + break; + case OPT_ORIENTATION: + enum sc_orientation orientation; + if (!parse_orientation(optarg, &orientation)) { return false; } + opts->display_orientation = orientation; + opts->record_orientation = orientation; break; case OPT_RENDER_DRIVER: opts->render_driver = optarg; @@ -2378,6 +2528,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->record_orientation != SC_ORIENTATION_0) { + if (sc_orientation_is_mirror(opts->record_orientation)) { + LOGE("Record orientation only supports rotation, not " + "flipping: %s", optarg); + return false; + } + } + if (opts->video && sc_record_format_is_audio_only(opts->record_format)) { LOGE("Audio container does not support video stream"); diff --git a/app/src/compat.h b/app/src/compat.h index e80a9dd26e..fd610c023e 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -3,7 +3,9 @@ #include "config.h" +#include #include +#include #include #ifndef __WIN32 @@ -50,6 +52,15 @@ # define SCRCPY_LAVU_HAS_CHLAYOUT #endif +// In ffmpeg/doc/APIchanges: +// 2023-10-06 - 5432d2aacad - lavc 60.15.100 - avformat.h +// Deprecate AVFormatContext.{nb_,}side_data, av_stream_add_side_data(), +// av_stream_new_side_data(), and av_stream_get_side_data(). Side data fields +// from AVFormatContext.codecpar should be used from now on. +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(60, 15, 100) +# define SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA +#endif + #if SDL_VERSION_ATLEAST(2, 0, 6) // # define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS diff --git a/app/src/display.c b/app/src/display.c index cf26e776d2..906b5d657b 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -234,7 +234,7 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame) { enum sc_display_result sc_display_render(struct sc_display *display, const SDL_Rect *geometry, - unsigned rotation) { + enum sc_orientation orientation) { SDL_RenderClear(display->renderer); if (display->pending.flags) { @@ -247,33 +247,33 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry, SDL_Renderer *renderer = display->renderer; SDL_Texture *texture = display->texture; - if (rotation == 0) { + if (orientation == SC_ORIENTATION_0) { int ret = SDL_RenderCopy(renderer, texture, NULL, geometry); if (ret) { LOGE("Could not render texture: %s", SDL_GetError()); return SC_DISPLAY_RESULT_ERROR; } } else { - // rotation in RenderCopyEx() is clockwise, while screen->rotation is - // counterclockwise (to be consistent with --lock-video-orientation) - int cw_rotation = (4 - rotation) % 4; + unsigned cw_rotation = sc_orientation_get_rotation(orientation); double angle = 90 * cw_rotation; const SDL_Rect *dstrect = NULL; SDL_Rect rect; - if (rotation & 1) { + if (sc_orientation_is_swap(orientation)) { rect.x = geometry->x + (geometry->w - geometry->h) / 2; rect.y = geometry->y + (geometry->h - geometry->w) / 2; rect.w = geometry->h; rect.h = geometry->w; dstrect = ▭ } else { - assert(rotation == 2); dstrect = geometry; } + SDL_RendererFlip flip = sc_orientation_is_mirror(orientation) + ? SDL_FLIP_HORIZONTAL : 0; + int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle, - NULL, 0); + NULL, flip); if (ret) { LOGE("Could not render texture: %s", SDL_GetError()); return SC_DISPLAY_RESULT_ERROR; diff --git a/app/src/display.h b/app/src/display.h index 6b83a5c9a4..643ce73c63 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -9,6 +9,7 @@ #include "coords.h" #include "opengl.h" +#include "options.h" #ifdef __APPLE__ # define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE @@ -54,6 +55,6 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame); enum sc_display_result sc_display_render(struct sc_display *display, const SDL_Rect *geometry, - unsigned rotation); + enum sc_orientation orientation); #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index c9e83d48c1..9a4878363f 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -293,15 +293,11 @@ rotate_device(struct sc_controller *controller) { } static void -rotate_client_left(struct sc_screen *screen) { - unsigned new_rotation = (screen->rotation + 1) % 4; - sc_screen_set_rotation(screen, new_rotation); -} - -static void -rotate_client_right(struct sc_screen *screen) { - unsigned new_rotation = (screen->rotation + 3) % 4; - sc_screen_set_rotation(screen, new_rotation); +apply_orientation_transform(struct sc_screen *screen, + enum sc_orientation transform) { + enum sc_orientation new_orientation = + sc_orientation_apply(screen->orientation, transform); + sc_screen_set_orientation(screen, new_orientation); } static void @@ -421,25 +417,47 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_DOWN: - if (controller && !shift) { + if (shift) { + if (!repeat & down) { + apply_orientation_transform(im->screen, + SC_ORIENTATION_FLIP_180); + } + } else if (controller) { // forward repeated events action_volume_down(controller, action); } return; case SDLK_UP: - if (controller && !shift) { + if (shift) { + if (!repeat & down) { + apply_orientation_transform(im->screen, + SC_ORIENTATION_FLIP_180); + } + } else if (controller) { // forward repeated events action_volume_up(controller, action); } return; case SDLK_LEFT: - if (!shift && !repeat && down) { - rotate_client_left(im->screen); + if (!repeat && down) { + if (shift) { + apply_orientation_transform(im->screen, + SC_ORIENTATION_FLIP_0); + } else { + apply_orientation_transform(im->screen, + SC_ORIENTATION_270); + } } return; case SDLK_RIGHT: - if (!shift && !repeat && down) { - rotate_client_right(im->screen); + if (!repeat && down) { + if (shift) { + apply_orientation_transform(im->screen, + SC_ORIENTATION_FLIP_0); + } else { + apply_orientation_transform(im->screen, + SC_ORIENTATION_90); + } } return; case SDLK_c: diff --git a/app/src/options.c b/app/src/options.c index 092fbd5613..23a5c5d889 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -39,7 +39,8 @@ const struct scrcpy_options scrcpy_options_default = { .audio_bit_rate = 0, .max_fps = 0, .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, - .rotation = 0, + .display_orientation = SC_ORIENTATION_0, + .record_orientation = SC_ORIENTATION_0, .window_x = SC_WINDOW_POSITION_UNDEFINED, .window_y = SC_WINDOW_POSITION_UNDEFINED, .window_width = 0, @@ -89,3 +90,39 @@ const struct scrcpy_options scrcpy_options_default = { .camera_high_speed = false, .list = 0, }; + +enum sc_orientation +sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform) { + assert(!(src & ~7)); + assert(!(transform & ~7)); + + unsigned transform_hflip = transform & 4; + unsigned transform_rotation = transform & 3; + unsigned src_hflip = src & 4; + unsigned src_rotation = src & 3; + unsigned src_swap = src & 1; + if (src_swap && transform_hflip) { + // If the src is rotated by 90 or 270 degrees, applying a flipped + // transformation requires an additional 180 degrees rotation to + // compensate for the inversion of the order of multiplication: + // + // hflip1 × rotate1 × hflip2 × rotate2 + // `--------------' `--------------' + // src transform + // + // In the final result, we want all the hflips then all the rotations, + // so we must move hflip2 to the left: + // + // hflip1 × hflip2 × f(rotate1) × rotate2 + // + // with f(rotate1) = | rotate1 if src is 0 or 180 + // | rotate1 + 180 if src is 90 or 270 + + src_rotation += 2; + } + + unsigned result_hflip = src_hflip ^ transform_hflip; + unsigned result_rotation = (transform_rotation + src_rotation) % 4; + enum sc_orientation result = result_hflip | result_rotation; + return result; +} diff --git a/app/src/options.h b/app/src/options.h index c702ceeb2b..11e64fa19e 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #include #include @@ -67,14 +68,75 @@ enum sc_camera_facing { SC_CAMERA_FACING_EXTERNAL, }; + // ,----- hflip (applied before the rotation) + // | ,--- 180° + // | | ,- 90° clockwise + // | | | +enum sc_orientation { // v v v + SC_ORIENTATION_0, // 0 0 0 + SC_ORIENTATION_90, // 0 0 1 + SC_ORIENTATION_180, // 0 1 0 + SC_ORIENTATION_270, // 0 1 1 + SC_ORIENTATION_FLIP_0, // 1 0 0 + SC_ORIENTATION_FLIP_90, // 1 0 1 + SC_ORIENTATION_FLIP_180, // 1 1 0 + SC_ORIENTATION_FLIP_270, // 1 1 1 +}; + +static inline bool +sc_orientation_is_mirror(enum sc_orientation orientation) { + assert(!(orientation & ~7)); + return orientation & 4; +} + +// Does the orientation swap width and height? +static inline bool +sc_orientation_is_swap(enum sc_orientation orientation) { + assert(!(orientation & ~7)); + return orientation & 1; +} + +static inline enum sc_orientation +sc_orientation_get_rotation(enum sc_orientation orientation) { + assert(!(orientation & ~7)); + return orientation & 3; +} + +enum sc_orientation +sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform); + +static inline const char * +sc_orientation_get_name(enum sc_orientation orientation) { + switch (orientation) { + case SC_ORIENTATION_0: + return "0"; + case SC_ORIENTATION_90: + return "90"; + case SC_ORIENTATION_180: + return "180"; + case SC_ORIENTATION_270: + return "270"; + case SC_ORIENTATION_FLIP_0: + return "flip0"; + case SC_ORIENTATION_FLIP_90: + return "flip90"; + case SC_ORIENTATION_FLIP_180: + return "flip180"; + case SC_ORIENTATION_FLIP_270: + return "flip270"; + default: + return "(unknown)"; + } +} + enum sc_lock_video_orientation { SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, // lock the current orientation when scrcpy starts SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, SC_LOCK_VIDEO_ORIENTATION_0 = 0, - SC_LOCK_VIDEO_ORIENTATION_1, - SC_LOCK_VIDEO_ORIENTATION_2, - SC_LOCK_VIDEO_ORIENTATION_3, + SC_LOCK_VIDEO_ORIENTATION_90 = 3, + SC_LOCK_VIDEO_ORIENTATION_180 = 2, + SC_LOCK_VIDEO_ORIENTATION_270 = 1, }; enum sc_keyboard_input_mode { @@ -157,7 +219,8 @@ struct scrcpy_options { uint32_t audio_bit_rate; uint16_t max_fps; enum sc_lock_video_orientation lock_video_orientation; - uint8_t rotation; + enum sc_orientation display_orientation; + enum sc_orientation record_orientation; int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; diff --git a/app/src/recorder.c b/app/src/recorder.c index c9d5f13183..9e0b33959a 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -4,6 +4,7 @@ #include #include #include +#include #include "util/log.h" #include "util/str.h" @@ -493,6 +494,42 @@ run_recorder(void *data) { return 0; } +static bool +sc_recorder_set_orientation(AVStream *stream, enum sc_orientation orientation) { + assert(!sc_orientation_is_mirror(orientation)); + + uint8_t *raw_data; +#ifdef SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA + AVPacketSideData *sd = + av_packet_side_data_new(&stream->codecpar->coded_side_data, + &stream->codecpar->nb_coded_side_data, + AV_PKT_DATA_DISPLAYMATRIX, + sizeof(int32_t) * 9, 0); + if (!sd) { + LOG_OOM(); + return false; + } + + raw_data = sd->data; +#else + raw_data = av_stream_new_side_data(stream, AV_PKT_DATA_DISPLAYMATRIX, + sizeof(int32_t) * 9); + if (!raw_data) { + LOG_OOM(); + return false; + } +#endif + + int32_t *matrix = (int32_t *) raw_data; + + unsigned rotation = orientation; + unsigned angle = rotation * 90; + + av_display_rotation_set(matrix, angle); + + return true; +} + static bool sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx) { @@ -520,6 +557,16 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, recorder->video_stream.index = stream->index; + if (recorder->orientation != SC_ORIENTATION_0) { + if (!sc_recorder_set_orientation(stream, recorder->orientation)) { + sc_mutex_unlock(&recorder->mutex); + return false; + } + + LOGI("Record orientation set to %s", + sc_orientation_get_name(recorder->orientation)); + } + recorder->video_init = true; sc_cond_signal(&recorder->cond); sc_mutex_unlock(&recorder->mutex); @@ -689,7 +736,10 @@ sc_recorder_stream_init(struct sc_recorder_stream *stream) { bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, enum sc_record_format format, bool video, bool audio, + enum sc_orientation orientation, const struct sc_recorder_callbacks *cbs, void *cbs_userdata) { + assert(!sc_orientation_is_mirror(orientation)); + recorder->filename = strdup(filename); if (!recorder->filename) { LOG_OOM(); @@ -710,6 +760,8 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->video = video; recorder->audio = audio; + recorder->orientation = orientation; + sc_vecdeque_init(&recorder->video_queue); sc_vecdeque_init(&recorder->audio_queue); recorder->stopped = false; diff --git a/app/src/recorder.h b/app/src/recorder.h index 163275845b..d096e79a18 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -34,6 +34,8 @@ struct sc_recorder { bool audio; bool video; + enum sc_orientation orientation; + char *filename; enum sc_record_format format; AVFormatContext *ctx; @@ -67,6 +69,7 @@ struct sc_recorder_callbacks { bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, enum sc_record_format format, bool video, bool audio, + enum sc_orientation orientation, const struct sc_recorder_callbacks *cbs, void *cbs_userdata); bool diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index ac2b8e33cc..d62a5f520f 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -507,7 +507,8 @@ scrcpy(struct scrcpy_options *options) { }; if (!sc_recorder_init(&s->recorder, options->record_filename, options->record_format, options->video, - options->audio, &recorder_cbs, NULL)) { + options->audio, options->record_orientation, + &recorder_cbs, NULL)) { goto end; } recorder_initialized = true; @@ -691,7 +692,7 @@ scrcpy(struct scrcpy_options *options) { .window_width = options->window_width, .window_height = options->window_height, .window_borderless = options->window_borderless, - .rotation = options->rotation, + .orientation = options->display_orientation, .mipmaps = options->mipmaps, .fullscreen = options->fullscreen, .start_fps_counter = options->start_fps_counter, diff --git a/app/src/screen.c b/app/src/screen.c index 5b7a880894..091001bcbf 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -14,16 +14,16 @@ #define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink) static inline struct sc_size -get_rotated_size(struct sc_size size, int rotation) { - struct sc_size rotated_size; - if (rotation & 1) { - rotated_size.width = size.height; - rotated_size.height = size.width; +get_oriented_size(struct sc_size size, enum sc_orientation orientation) { + struct sc_size oriented_size; + if (sc_orientation_is_swap(orientation)) { + oriented_size.width = size.height; + oriented_size.height = size.width; } else { - rotated_size.width = size.width; - rotated_size.height = size.height; + oriented_size.width = size.width; + oriented_size.height = size.height; } - return rotated_size; + return oriented_size; } // get the window size in a struct sc_size @@ -251,7 +251,7 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) { } enum sc_display_result res = - sc_display_render(&screen->display, &screen->rect, screen->rotation); + sc_display_render(&screen->display, &screen->rect, screen->orientation); (void) res; // any error already logged } @@ -379,9 +379,10 @@ sc_screen_init(struct sc_screen *screen, goto error_destroy_frame_buffer; } - screen->rotation = params->rotation; - if (screen->rotation) { - LOGI("Initial display rotation set to %u", screen->rotation); + screen->orientation = params->orientation; + if (screen->orientation != SC_ORIENTATION_0) { + LOGI("Initial display orientation set to %s", + sc_orientation_get_name(screen->orientation)); } uint32_t window_flags = SDL_WINDOW_HIDDEN @@ -559,19 +560,19 @@ apply_pending_resize(struct sc_screen *screen) { } void -sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation) { - assert(rotation < 4); - if (rotation == screen->rotation) { +sc_screen_set_orientation(struct sc_screen *screen, + enum sc_orientation orientation) { + if (orientation == screen->orientation) { return; } struct sc_size new_content_size = - get_rotated_size(screen->frame_size, rotation); + get_oriented_size(screen->frame_size, orientation); set_content_size(screen, new_content_size); - screen->rotation = rotation; - LOGI("Display rotation set to %u", rotation); + screen->orientation = orientation; + LOGI("Display orientation set to %s", sc_orientation_get_name(orientation)); sc_screen_render(screen, true); } @@ -584,7 +585,7 @@ sc_screen_init_size(struct sc_screen *screen) { // The requested size is passed via screen->frame_size struct sc_size content_size = - get_rotated_size(screen->frame_size, screen->rotation); + get_oriented_size(screen->frame_size, screen->orientation); screen->content_size = content_size; enum sc_display_result res = @@ -604,7 +605,7 @@ prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { screen->frame_size = new_frame_size; struct sc_size new_content_size = - get_rotated_size(new_frame_size, screen->rotation); + get_oriented_size(new_frame_size, screen->orientation); set_content_size(screen, new_content_size); sc_screen_update_content_rect(screen); @@ -843,8 +844,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { struct sc_point sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, int32_t x, int32_t y) { - unsigned rotation = screen->rotation; - assert(rotation < 4); + enum sc_orientation orientation = screen->orientation; int32_t w = screen->content_size.width; int32_t h = screen->content_size.height; @@ -855,27 +855,43 @@ sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, x = (int64_t) (x - screen->rect.x) * w / screen->rect.w; y = (int64_t) (y - screen->rect.y) * h / screen->rect.h; - // rotate struct sc_point result; - switch (rotation) { - case 0: + switch (orientation) { + case SC_ORIENTATION_0: result.x = x; result.y = y; break; - case 1: + case SC_ORIENTATION_90: + result.x = y; + result.y = w - x; + break; + case SC_ORIENTATION_180: + result.x = w - x; + result.y = h - y; + break; + case SC_ORIENTATION_270: result.x = h - y; result.y = x; break; - case 2: + case SC_ORIENTATION_FLIP_0: result.x = w - x; + result.y = y; + break; + case SC_ORIENTATION_FLIP_90: + result.x = h - y; + result.y = w - x; + break; + case SC_ORIENTATION_FLIP_180: + result.x = x; result.y = h - y; break; default: - assert(rotation == 3); + assert(orientation == SC_ORIENTATION_FLIP_270); result.x = y; - result.y = w - x; + result.y = x; break; } + return result; } diff --git a/app/src/screen.h b/app/src/screen.h index acbaab4b0b..46591be5cc 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -14,6 +14,7 @@ #include "frame_buffer.h" #include "input_manager.h" #include "opengl.h" +#include "options.h" #include "trait/key_processor.h" #include "trait/frame_sink.h" #include "trait/mouse_processor.h" @@ -49,8 +50,8 @@ struct sc_screen { // fullscreen (meaningful only when resize_pending is true) struct sc_size windowed_content_size; - // client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise) - unsigned rotation; + // client orientation + enum sc_orientation orientation; // rectangle of the content (excluding black borders) struct SDL_Rect rect; bool has_frame; @@ -86,7 +87,7 @@ struct sc_screen_params { bool window_borderless; - uint8_t rotation; + enum sc_orientation orientation; bool mipmaps; bool fullscreen; @@ -129,9 +130,10 @@ sc_screen_resize_to_fit(struct sc_screen *screen); void sc_screen_resize_to_pixel_perfect(struct sc_screen *screen); -// set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise) +// set the display orientation void -sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation); +sc_screen_set_orientation(struct sc_screen *screen, + enum sc_orientation orientation); // react to SDL events // If this function returns false, scrcpy must exit with an error. diff --git a/app/src/server.c b/app/src/server.c index d4726c2af8..7efdb48fb0 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -18,7 +18,6 @@ #define SC_SERVER_FILENAME "scrcpy-server" #define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME -#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" #define SC_ADB_PORT_DEFAULT 5555 #define SC_SOCKET_NAME_PREFIX "scrcpy_" @@ -117,7 +116,7 @@ sc_server_params_copy(struct sc_server_params *dst, } static bool -push_server(struct sc_intr *intr, const char *serial) { +push_server(struct sc_intr *intr, uint32_t scid, const char *serial) { char *server_path = get_server_path(); if (!server_path) { return false; @@ -127,7 +126,16 @@ push_server(struct sc_intr *intr, const char *serial) { free(server_path); return false; } - bool ok = sc_adb_push(intr, serial, server_path, SC_DEVICE_SERVER_PATH, 0); + + char *device_server_path; + if (asprintf(&device_server_path, "/data/local/tmp/scrcpy-server-%08x.jar", + scid) == -1) { + LOG_OOM(); + free(server_path); + return false; + } + bool ok = sc_adb_push(intr, serial, server_path, device_server_path, 0); + free(device_server_path); free(server_path); return ok; } @@ -209,13 +217,20 @@ execute_server(struct sc_server *server, const char *serial = server->serial; assert(serial); + char *classpath; + if (asprintf(&classpath, "CLASSPATH=/data/local/tmp/scrcpy-server-%08x.jar", + params->scid) == -1) { + LOG_OOM(); + return SC_PROCESS_NONE; + } + const char *cmd[128]; unsigned count = 0; cmd[count++] = sc_adb_get_executable(); cmd[count++] = "-s"; cmd[count++] = serial; cmd[count++] = "shell"; - cmd[count++] = "CLASSPATH=" SC_DEVICE_SERVER_PATH; + cmd[count++] = classpath; cmd[count++] = "app_process"; #ifdef SERVER_DEBUGGER @@ -388,6 +403,7 @@ execute_server(struct sc_server *server, for (unsigned i = dyn_idx; i < count; ++i) { free((char *) cmd[i]); } + free(classpath); return pid; } @@ -937,7 +953,7 @@ run_server(void *data) { assert(serial); LOGD("Device serial: %s", serial); - ok = push_server(&server->intr, serial); + ok = push_server(&server->intr, params->scid, serial); if (!ok) { goto error_connection_failed; } diff --git a/app/tests/test_orientation.c b/app/tests/test_orientation.c new file mode 100644 index 0000000000..153211fa94 --- /dev/null +++ b/app/tests/test_orientation.c @@ -0,0 +1,91 @@ +#include "common.h" + +#include + +#include "options.h" + +static void test_transforms(void) { + #define O(X) SC_ORIENTATION_ ## X + #define ASSERT_TRANSFORM(SRC, TR, RES) \ + assert(sc_orientation_apply(O(SRC), O(TR)) == O(RES)); + + ASSERT_TRANSFORM(0, 0, 0); + ASSERT_TRANSFORM(0, 90, 90); + ASSERT_TRANSFORM(0, 180, 180); + ASSERT_TRANSFORM(0, 270, 270); + ASSERT_TRANSFORM(0, FLIP_0, FLIP_0); + ASSERT_TRANSFORM(0, FLIP_90, FLIP_90); + ASSERT_TRANSFORM(0, FLIP_180, FLIP_180); + ASSERT_TRANSFORM(0, FLIP_270, FLIP_270); + + ASSERT_TRANSFORM(90, 0, 90); + ASSERT_TRANSFORM(90, 90, 180); + ASSERT_TRANSFORM(90, 180, 270); + ASSERT_TRANSFORM(90, 270, 0); + ASSERT_TRANSFORM(90, FLIP_0, FLIP_270); + ASSERT_TRANSFORM(90, FLIP_90, FLIP_0); + ASSERT_TRANSFORM(90, FLIP_180, FLIP_90); + ASSERT_TRANSFORM(90, FLIP_270, FLIP_180); + + ASSERT_TRANSFORM(180, 0, 180); + ASSERT_TRANSFORM(180, 90, 270); + ASSERT_TRANSFORM(180, 180, 0); + ASSERT_TRANSFORM(180, 270, 90); + ASSERT_TRANSFORM(180, FLIP_0, FLIP_180); + ASSERT_TRANSFORM(180, FLIP_90, FLIP_270); + ASSERT_TRANSFORM(180, FLIP_180, FLIP_0); + ASSERT_TRANSFORM(180, FLIP_270, FLIP_90); + + ASSERT_TRANSFORM(270, 0, 270); + ASSERT_TRANSFORM(270, 90, 0); + ASSERT_TRANSFORM(270, 180, 90); + ASSERT_TRANSFORM(270, 270, 180); + ASSERT_TRANSFORM(270, FLIP_0, FLIP_90); + ASSERT_TRANSFORM(270, FLIP_90, FLIP_180); + ASSERT_TRANSFORM(270, FLIP_180, FLIP_270); + ASSERT_TRANSFORM(270, FLIP_270, FLIP_0); + + ASSERT_TRANSFORM(FLIP_0, 0, FLIP_0); + ASSERT_TRANSFORM(FLIP_0, 90, FLIP_90); + ASSERT_TRANSFORM(FLIP_0, 180, FLIP_180); + ASSERT_TRANSFORM(FLIP_0, 270, FLIP_270); + ASSERT_TRANSFORM(FLIP_0, FLIP_0, 0); + ASSERT_TRANSFORM(FLIP_0, FLIP_90, 90); + ASSERT_TRANSFORM(FLIP_0, FLIP_180, 180); + ASSERT_TRANSFORM(FLIP_0, FLIP_270, 270); + + ASSERT_TRANSFORM(FLIP_90, 0, FLIP_90); + ASSERT_TRANSFORM(FLIP_90, 90, FLIP_180); + ASSERT_TRANSFORM(FLIP_90, 180, FLIP_270); + ASSERT_TRANSFORM(FLIP_90, 270, FLIP_0); + ASSERT_TRANSFORM(FLIP_90, FLIP_0, 270); + ASSERT_TRANSFORM(FLIP_90, FLIP_90, 0); + ASSERT_TRANSFORM(FLIP_90, FLIP_180, 90); + ASSERT_TRANSFORM(FLIP_90, FLIP_270, 180); + + ASSERT_TRANSFORM(FLIP_180, 0, FLIP_180); + ASSERT_TRANSFORM(FLIP_180, 90, FLIP_270); + ASSERT_TRANSFORM(FLIP_180, 180, FLIP_0); + ASSERT_TRANSFORM(FLIP_180, 270, FLIP_90); + ASSERT_TRANSFORM(FLIP_180, FLIP_0, 180); + ASSERT_TRANSFORM(FLIP_180, FLIP_90, 270); + ASSERT_TRANSFORM(FLIP_180, FLIP_180, 0); + ASSERT_TRANSFORM(FLIP_180, FLIP_270, 90); + + ASSERT_TRANSFORM(FLIP_270, 0, FLIP_270); + ASSERT_TRANSFORM(FLIP_270, 90, FLIP_0); + ASSERT_TRANSFORM(FLIP_270, 180, FLIP_90); + ASSERT_TRANSFORM(FLIP_270, 270, FLIP_180); + ASSERT_TRANSFORM(FLIP_270, FLIP_0, 90); + ASSERT_TRANSFORM(FLIP_270, FLIP_90, 180); + ASSERT_TRANSFORM(FLIP_270, FLIP_180, 270); + ASSERT_TRANSFORM(FLIP_270, FLIP_270, 0); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_transforms(); + return 0; +} diff --git a/cross_win32.txt b/cross_win32.txt index e24f37226a..bf3c118e1d 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -6,7 +6,7 @@ c = 'i686-w64-mingw32-gcc' cpp = 'i686-w64-mingw32-g++' ar = 'i686-w64-mingw32-ar' strip = 'i686-w64-mingw32-strip' -pkgconfig = 'i686-w64-mingw32-pkg-config' +pkg-config = 'i686-w64-mingw32-pkg-config' windres = 'i686-w64-mingw32-windres' [host_machine] diff --git a/cross_win64.txt b/cross_win64.txt index 39e7994475..81bb03095b 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -6,7 +6,7 @@ c = 'x86_64-w64-mingw32-gcc' cpp = 'x86_64-w64-mingw32-g++' ar = 'x86_64-w64-mingw32-ar' strip = 'x86_64-w64-mingw32-strip' -pkgconfig = 'x86_64-w64-mingw32-pkg-config' +pkg-config = 'x86_64-w64-mingw32-pkg-config' windres = 'x86_64-w64-mingw32-windres' [host_machine] diff --git a/doc/camera.md b/doc/camera.md index d1008bda4c..fcc410fa95 100644 --- a/doc/camera.md +++ b/doc/camera.md @@ -101,6 +101,16 @@ scrcpy --video-source=camera --camera-size=1920x1080 -m3000 # error ``` +## Rotation + +To rotate the captured video, use the [video orientation](video.md#orientation) +option: + +``` +scrcpy --video-source=camera --camera-size=1920x1080 --orientation=90 +``` + + ## Frame rate By default, camera is captured at Android's default frame rate (30 fps). diff --git a/doc/recording.md b/doc/recording.md index c1a8445e7b..216542e995 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -50,6 +50,12 @@ scrcpy --record=file --record-format=mkv ``` +## Rotation + +The video can be recorded rotated. See [video +orientation](video.md#orientation). + + ## No playback To disable playback while recording: diff --git a/doc/shortcuts.md b/doc/shortcuts.md index 5e706402ba..c0fc28421b 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -26,6 +26,8 @@ _[Super] is typically the Windows or Cmd key._ | Switch fullscreen mode | MOD+f | Rotate display left | MOD+ _(left)_ | Rotate display right | MOD+ _(right)_ + | Flip display horizontally | MOD+Shift+ _(left)_ \| MOD+Shift+ _(right)_ + | Flip display vertically | MOD+Shift+ _(up)_ \| MOD+Shift+ _(down)_ | Resize window to 1:1 (pixel-perfect) | MOD+g | Resize window to remove black borders | MOD+w \| _Double-left-click¹_ | Click on `HOME` | MOD+h \| _Middle-click_ diff --git a/doc/video.md b/doc/video.md index 512e0aba79..c0437269d7 100644 --- a/doc/video.md +++ b/doc/video.md @@ -97,39 +97,50 @@ scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc' ``` -## Rotation +## Orientation -The rotation may be applied at 3 different levels: +The orientation may be applied at 3 different levels: - The [shortcut](shortcuts.md) MOD+r requests the device to switch between portrait and landscape (the current running app may refuse, if it does not support the requested orientation). - `--lock-video-orientation` changes the mirroring orientation (the orientation of the video sent from the device to the computer). This affects the recording. - - `--rotation` rotates only the window content. This only affects the display, - not the recording. It may be changed dynamically at any time using the - [shortcuts](shortcuts.md) MOD+ and - MOD+. + - `--orientation` is applied on the client side, and affects display and + recording. For the display, it can be changed dynamically using + [shortcuts](shortcuts.md). -To lock the mirroring orientation: +To lock the mirroring orientation (on the capture side): ```bash -scrcpy --lock-video-orientation # initial (current) orientation -scrcpy --lock-video-orientation=0 # natural orientation -scrcpy --lock-video-orientation=1 # 90° counterclockwise -scrcpy --lock-video-orientation=2 # 180° -scrcpy --lock-video-orientation=3 # 90° clockwise +scrcpy --lock-video-orientation # initial (current) orientation +scrcpy --lock-video-orientation=0 # natural orientation +scrcpy --lock-video-orientation=90 # 90° clockwise +scrcpy --lock-video-orientation=180 # 180° +scrcpy --lock-video-orientation=270 # 270° clockwise ``` -To set an initial window rotation: +To orient the video (on the rendering side): ```bash -scrcpy --rotation=0 # no rotation -scrcpy --rotation=1 # 90 degrees counterclockwise -scrcpy --rotation=2 # 180 degrees -scrcpy --rotation=3 # 90 degrees clockwise +scrcpy --orientation=0 +scrcpy --orientation=90 # 90° clockwise +scrcpy --orientation=180 # 180° +scrcpy --orientation=270 # 270° clockwise +scrcpy --orientation=flip0 # hflip +scrcpy --orientation=flip90 # hflip + 90° clockwise +scrcpy --orientation=flip180 # vflip (hflip + 180°) +scrcpy --orientation=flip270 # hflip + 270° ``` +The orientation can be set separately for display and record if necessary, via +`--display-orientation` and `--record-orientation`. + +The rotation is applied to a recorded file by writing a display transformation +to the MP4 or MKV target file. Flipping is not supported, so only the 4 first +values are allowed when recording. + + ## Crop The device screen may be cropped to mirror only part of the screen. diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 0bcd1a5470..2c395bd916 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -14,8 +14,6 @@ */ public final class CleanUp { - public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; - // A simple struct to be passed from the main process to the cleanup process public static class Config implements Parcelable { @@ -135,21 +133,19 @@ private static void startProcess(Config config) throws IOException { String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()}; ProcessBuilder builder = new ProcessBuilder(cmd); - builder.environment().put("CLASSPATH", SERVER_PATH); + builder.environment().put("CLASSPATH", Server.SERVER_PATH); builder.start(); } public static void unlinkSelf() { try { - new File(SERVER_PATH).delete(); + new File(Server.SERVER_PATH).delete(); } catch (Exception e) { Ln.e("Could not unlink server", e); } } public static void main(String... args) { - unlinkSelf(); - try { // Wait for the server to die System.in.read(); @@ -187,7 +183,11 @@ public static void main(String... args) { } else if (config.restoreNormalPowerMode) { Ln.i("Restoring normal power mode"); Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); + // Make sure the request is performed before exiting + DisplayPowerMode.stopAndJoin(); } } + + unlinkSelf(); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 4ab689b04e..777fa4ef9e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -315,6 +315,14 @@ public boolean setClipboardText(String text) { */ public static boolean setScreenPowerMode(int mode) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasPhysicalDisplayIdsMethod()) { + // On Android 14+, these internal methods have been moved to system server classes. + // Run a separate process with the correct classpath and LD_PRELOAD to change the display power mode. + DisplayPowerMode.setRemoteDisplayPowerMode(mode); + // The call is asynchronous (we don't want to block) + return true; + } + // Change the power mode for all physical displays long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds(); if (physicalDisplayIds == null) { diff --git a/server/src/main/java/com/genymobile/scrcpy/DisplayPowerMode.java b/server/src/main/java/com/genymobile/scrcpy/DisplayPowerMode.java new file mode 100644 index 0000000000..68bef2b830 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/DisplayPowerMode.java @@ -0,0 +1,161 @@ +package com.genymobile.scrcpy; + +import android.annotation.SuppressLint; +import android.os.IBinder; + +import java.io.IOException; +import java.lang.reflect.Method; + +/** + * On Android 14, the methods used to turn the device screen off have been moved from SurfaceControl (in framework.jar) to DisplayControl (a system + * server class). As a consequence, they could not be called directly. See {@url https://github.com/Genymobile/scrcpy/issues/3927}. + *

+ * Instead, run a separate process with a different classpath and LD_PRELOAD just to set the display power mode. + *

+ * Since the client must not block, and calling/joining a process is blocking (this specific one takes a few hundred milliseconds to complete), + * this class uses an internal thread to execute the requests asynchronously, and serialize them (so that two successive requests are guaranteed to + * be executed in order). In addition, it only executes the last pending request (setting power mode to value X then to value Y is equivalent to + * just setting it to value Y). + */ +public final class DisplayPowerMode { + + private static final Proxy PROXY = new Proxy(); + + private static final class Proxy implements Runnable { + + private Thread thread; + private int requestedMode = -1; + private boolean stopped; + + synchronized void requestMode(int mode) { + if (thread == null) { + thread = new Thread(this, "DisplayPowerModeProxy"); + thread.setDaemon(true); + thread.start(); + } + requestedMode = mode; + notify(); + } + + void stopAndJoin() { + boolean hasThread; + synchronized (this) { + hasThread = thread != null; + if (thread != null) { + stopped = true; + notify(); + } + } + + if (hasThread) { + // Join the thread without holding the mutex (that would cause a deadlock) + try { + thread.join(); + } catch (InterruptedException e) { + Ln.e("Thread join interrupted", e); + } + } + } + + @Override + public void run() { + try { + int mode; + while (true) { + synchronized (this) { + while (!stopped && requestedMode == -1) { + wait(); + } + mode = requestedMode; + requestedMode = -1; + } + + // Even if stopped, the last request must be executed to restore the display power mode to normal + if (mode == -1) { + return; + } + + try { + Process process = executeSystemServerSetDisplayPowerMode(mode); + int status = process.waitFor(); + if (status != 0) { + Ln.e("Set display power mode failed remotely: status=" + status); + } + } catch (Exception e) { + Ln.e("Failed to execute process", e); + } + } + } catch (InterruptedException e) { + // end of thread + } + } + } + + private DisplayPowerMode() { + // not instantiable + } + + // Called from the scrcpy process + public static void setRemoteDisplayPowerMode(int mode) { + PROXY.requestMode(mode); + } + + public static void stopAndJoin() { + PROXY.stopAndJoin(); + } + + // Called from the proxy thread in the scrcpy process + private static Process executeSystemServerSetDisplayPowerMode(int mode) throws IOException { + String[] ldPreloadLibs = {"/system/lib64/libandroid_servers.so"}; + String[] cmd = {"app_process", "/", DisplayPowerMode.class.getName(), String.valueOf(mode)}; + + ProcessBuilder builder = new ProcessBuilder(cmd); + builder.environment().put("LD_PRELOAD", String.join(" ", ldPreloadLibs)); + builder.environment().put("CLASSPATH", Server.SERVER_PATH + ":/system/framework/services.jar"); + return builder.start(); + } + + // Executed in the DisplayPowerMode-specific process + @SuppressLint({"PrivateApi", "SoonBlockedPrivateApi"}) + private static void setDisplayPowerModeUsingDisplayControl(int mode) throws Exception { + System.loadLibrary("android_servers"); + + @SuppressLint("PrivateApi") + Class displayControlClass = Class.forName("com.android.server.display.DisplayControl"); + Method getPhysicalDisplayIdsMethod = displayControlClass.getDeclaredMethod("getPhysicalDisplayIds"); + Method getPhysicalDisplayTokenMethod = displayControlClass.getDeclaredMethod("getPhysicalDisplayToken", long.class); + + Class surfaceControlClass = Class.forName("android.view.SurfaceControl"); + Method setDisplayPowerModeMethod = surfaceControlClass.getDeclaredMethod("setDisplayPowerMode", IBinder.class, int.class); + + long[] displayIds = (long[]) getPhysicalDisplayIdsMethod.invoke(null); + for (long displayId : displayIds) { + Object token = getPhysicalDisplayTokenMethod.invoke(null, displayId); + setDisplayPowerModeMethod.invoke(null, token, mode); + } + } + + public static void main(String... args) { + Ln.disableSystemStreams(); + Ln.initLogLevel(Ln.Level.DEBUG); + + int status = run(args) ? 0 : 1; + System.exit(status); + } + + private static boolean run(String... args) { + if (args.length != 1) { + Ln.e("Exactly one argument expected: the value of one of the SurfaceControl.POWER_MODE_* constants (typically 0 or 2)"); + return false; + } + + try { + int mode = Integer.parseInt(args[0]); + setDisplayPowerModeUsingDisplayControl(mode); + return true; + } catch (Throwable e) { + Ln.e("Could not set display power mode", e); + return false; + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 61d3497bec..2a8387e0a5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -3,12 +3,20 @@ import android.os.BatteryManager; import android.os.Build; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; public final class Server { + public static final String SERVER_PATH; + static { + String[] classPaths = System.getProperty("java.class.path").split(File.pathSeparator); + // By convention, scrcpy is always executed with the absolute path of scrcpy-server.jar as the first item in the classpath + SERVER_PATH = classPaths[0]; + } + private static class Completion { private int running; private boolean fatalError; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 595ee6d477..98259e7ff5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -139,6 +139,15 @@ private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodExcept return getPhysicalDisplayIdsMethod; } + public static boolean hasPhysicalDisplayIdsMethod() { + try { + getGetPhysicalDisplayIdsMethod(); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + public static long[] getPhysicalDisplayIds() { try { Method method = getGetPhysicalDisplayIdsMethod();