From 878bfb1d8b3badbd991539c332859e0eaf677b37 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Nov 2023 01:06:59 +0100 Subject: [PATCH 01/10] Add --display-orientation Deprecate the option --rotation and introduce a new option --display-orientation with the 8 possible orientations (0, 90, 180, 270, flip0, flip90, flip180 and flip270). New shortcuts MOD+Shift+(arrow) dynamically change the display (horizontal or vertical) flip. Fixes #1380 Fixes #3819 --- app/data/bash-completion/scrcpy | 9 ++-- app/data/zsh-completion/_scrcpy | 2 +- app/meson.build | 4 ++ app/scrcpy.1 | 20 +++++-- app/src/cli.c | 92 +++++++++++++++++++++++++++++++-- app/src/display.c | 16 +++--- app/src/display.h | 3 +- app/src/input_manager.c | 48 +++++++++++------ app/src/options.c | 38 +++++++++++++- app/src/options.h | 64 ++++++++++++++++++++++- app/src/scrcpy.c | 2 +- app/src/screen.c | 74 +++++++++++++++----------- app/src/screen.h | 12 +++-- app/tests/test_orientation.c | 91 ++++++++++++++++++++++++++++++++ doc/shortcuts.md | 2 + 15 files changed, 403 insertions(+), 74 deletions(-) create mode 100644 app/tests/test_orientation.c diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index f94a70a603..5e359f4f5b 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 @@ -112,6 +113,10 @@ _scrcpy() { COMPREPLY=($(compgen -W 'front back external' -- "$cur")) return ;; + --display-orientation) + COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) + return + ;; --lock-video-orientation) COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) return @@ -132,10 +137,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..16729d2a1b 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]' @@ -68,7 +69,6 @@ arguments=( '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac wav)' '--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..08a366eeab 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). @@ -369,10 +377,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 +538,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..8976794c29 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -90,6 +90,7 @@ enum { OPT_CAMERA_AR, OPT_CAMERA_FPS, OPT_CAMERA_HIGH_SPEED, + OPT_DISPLAY_ORIENTATION, }; struct sc_option { @@ -309,6 +310,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", @@ -615,12 +627,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 +834,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)", @@ -1405,6 +1423,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,7 +2065,34 @@ 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; 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..245ab4afb3 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -39,7 +39,7 @@ 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, .window_x = SC_WINDOW_POSITION_UNDEFINED, .window_y = SC_WINDOW_POSITION_UNDEFINED, .window_width = 0, @@ -89,3 +89,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..5a6c32765f 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #include #include @@ -67,6 +68,67 @@ 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 @@ -157,7 +219,7 @@ 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; 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/scrcpy.c b/app/src/scrcpy.c index ac2b8e33cc..9bbe14b84a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -691,7 +691,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/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/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_ From dac0d54c5caaa7c81307904c25cc8d9d42b3772d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 13:49:14 +0100 Subject: [PATCH 02/10] Pass --lock-video-orientation argument in degrees For consistency with the new --display-orientation option, express the --lock-video-orientation in degrees clockwise: * --lock-video-orientation=0 -> --lock-video-orientation=0 * --lock-video-orientation=3 -> --lock-video-orientation=90 * --lock-video-orientation=2 -> --lock-video-orientation=180 * --lock-video-orientation=1 -> --lock-video-orientation=270 --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 4 ++- app/src/cli.c | 57 ++++++++++++++++++++++++++------- app/src/options.h | 6 ++-- 5 files changed, 54 insertions(+), 17 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 5e359f4f5b..0ecace96ad 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -118,7 +118,7 @@ _scrcpy() { 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) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 16729d2a1b..3f65cb4e7f 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -41,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]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 08a366eeab..266ba1f4a1 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -215,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". diff --git a/app/src/cli.c b/app/src/cli.c index 8976794c29..8079fbb71f 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -411,11 +411,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\".", @@ -1400,15 +1400,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 diff --git a/app/src/options.h b/app/src/options.h index 5a6c32765f..4fb45840c2 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -134,9 +134,9 @@ enum sc_lock_video_orientation { // 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 { From 70baf3c3847bcb4560a9b66375e539ed52b1ccba Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 14:02:46 +0100 Subject: [PATCH 03/10] Add --record-orientation Add an option to store the orientation to apply in a recorded file. Only rotations are supported (not flip), and most players correctly handle it only for MP4 files (not MKV files). --- app/data/bash-completion/scrcpy | 5 ++++ app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 8 +++++ app/src/cli.c | 23 +++++++++++++++ app/src/compat.h | 11 +++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/recorder.c | 52 +++++++++++++++++++++++++++++++++ app/src/recorder.h | 3 ++ app/src/scrcpy.c | 3 +- 10 files changed, 107 insertions(+), 1 deletion(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 0ecace96ad..f08df9963a 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -62,6 +62,7 @@ _scrcpy() { -r --record= --raw-key-events --record-format= + --record-orientation= --render-driver= --require-audio --rotation= @@ -117,6 +118,10 @@ _scrcpy() { 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 90 180 270' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 3f65cb4e7f..0e39f96bab 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -67,6 +67,7 @@ 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]' {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 266ba1f4a1..1a9386ee29 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -367,6 +367,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). diff --git a/app/src/cli.c b/app/src/cli.c index 8079fbb71f..233663d989 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -91,6 +91,7 @@ enum { OPT_CAMERA_FPS, OPT_CAMERA_HIGH_SPEED, OPT_DISPLAY_ORIENTATION, + OPT_RECORD_ORIENTATION, }; struct sc_option { @@ -609,6 +610,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", @@ -2131,6 +2141,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_RECORD_ORIENTATION: + if (!parse_orientation(optarg, &opts->record_orientation)) { + return false; + } + break; case OPT_RENDER_DRIVER: opts->render_driver = optarg; break; @@ -2497,6 +2512,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/options.c b/app/src/options.c index 245ab4afb3..23a5c5d889 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -40,6 +40,7 @@ const struct scrcpy_options scrcpy_options_default = { .max_fps = 0, .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, .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, diff --git a/app/src/options.h b/app/src/options.h index 4fb45840c2..11e64fa19e 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -220,6 +220,7 @@ struct scrcpy_options { uint16_t max_fps; enum sc_lock_video_orientation lock_video_orientation; 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 9bbe14b84a..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; From c546293f35b6cc814d94bcd9eb923eb339465bc8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 17:49:04 +0100 Subject: [PATCH 04/10] Add --orientation Add a shortcut to set both the display and record orientations. --- app/data/bash-completion/scrcpy | 2 ++ app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++++ app/src/cli.c | 16 ++++++++++++++++ 4 files changed, 23 insertions(+) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index f08df9963a..0c85431077 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -51,6 +51,7 @@ _scrcpy() { --no-power-on --no-video --no-video-playback + --orientation= --otg -p --port= --pause-on-exit @@ -114,6 +115,7 @@ _scrcpy() { COMPREPLY=($(compgen -W 'front back external' -- "$cur")) return ;; + --orientation --display-orientation) COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 0e39f96bab..3c7ca2178a 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -57,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)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1a9386ee29..0c34b4e2a7 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -299,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. diff --git a/app/src/cli.c b/app/src/cli.c index 233663d989..8b3ae8af22 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -92,6 +92,7 @@ enum { OPT_CAMERA_HIGH_SPEED, OPT_DISPLAY_ORIENTATION, OPT_RECORD_ORIENTATION, + OPT_ORIENTATION, }; struct sc_option { @@ -525,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", @@ -2146,6 +2154,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], 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; break; From cdd78273dce9c20def7c0d1f2129be5b1df24338 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 20:56:36 +0100 Subject: [PATCH 05/10] Update documentation about video orientation --- doc/camera.md | 10 ++++++++++ doc/recording.md | 6 ++++++ doc/video.md | 45 ++++++++++++++++++++++++++++----------------- 3 files changed, 44 insertions(+), 17 deletions(-) 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/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. From ec80be1eda384624b04b8a5c75e95e1c45fd122a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 21 Nov 2023 14:59:24 +0100 Subject: [PATCH 06/10] Fix meson deprecated 'pkgconfig' to 'pkg-config' When running ./release.sh: > DEPRECATION: "pkgconfig" entry is deprecated and should be replaced by > "pkg-config" --- cross_win32.txt | 2 +- cross_win64.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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] From e76ff25e31368ab45cb230f8dd7c0cb1dba351c6 Mon Sep 17 00:00:00 2001 From: sam80180 Date: Sat, 11 Nov 2023 02:01:51 +0800 Subject: [PATCH 07/10] Do not hardcode server path on the device The path can be retrieved from the classpath. PR #4416 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- server/src/main/java/com/genymobile/scrcpy/CleanUp.java | 6 ++---- server/src/main/java/com/genymobile/scrcpy/Server.java | 8 ++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 0bcd1a5470..b3a1aac13e 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,13 +133,13 @@ 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); } 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; From 798727aa58a472a492895641546d099d7e8cf81b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 21 Nov 2023 15:00:42 +0100 Subject: [PATCH 08/10] Use a different server path for each session When scrcpy is run, a server is pushed to /data/local/tmp/scrcpy-server.jar. Running simultaneous scrcpy instances on the same device was not a problem, because the file was unlinked (removed) almost immediately once it started, avoiding any conflicts. In order to support executing new process using the scrcpy-server.jar at any time (to change the display power mode from a separate process), the server file must not be unlinked, so using different names are necessary to avoid conflicts. Reuse the scid mechanism already used for generating device socket names. Refs 4315be164823d2c8fc44b475b52af79bfee98ff1 Refs 439a1fd4ed3d7ada4c5098a6d52893dd4e87082a --- app/src/server.c | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) 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; } From 890ba529c38c8a803c8a1b65fa4d2d2a34bae7b5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 21 Nov 2023 15:08:04 +0100 Subject: [PATCH 09/10] Keep server file on the device until clean up The server was unlinked (removed) just after it started. In order to execute a new process using the server jarfile at any time (typically to set the display power mode from another process), keep the file until the server closes. --- server/src/main/java/com/genymobile/scrcpy/CleanUp.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index b3a1aac13e..8be413ca21 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -146,8 +146,6 @@ public static void unlinkSelf() { } public static void main(String... args) { - unlinkSelf(); - try { // Wait for the server to die System.in.read(); @@ -187,5 +185,7 @@ public static void main(String... args) { Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); } } + + unlinkSelf(); } } From 67fa766f09cdab155ff1fd7ff724f67fe3d1e757 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Nov 2023 17:42:00 +0100 Subject: [PATCH 10/10] Fix turn screen off on Android 14 On Android 14, execute a separate process with a different classpath and LD_PRELOAD to execute the methods required to turn the device screen off. Fixes #3927 Refs #3927 comment Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- .../java/com/genymobile/scrcpy/CleanUp.java | 2 + .../java/com/genymobile/scrcpy/Device.java | 8 + .../genymobile/scrcpy/DisplayPowerMode.java | 161 ++++++++++++++++++ .../scrcpy/wrappers/SurfaceControl.java | 9 + 4 files changed, 180 insertions(+) create mode 100644 server/src/main/java/com/genymobile/scrcpy/DisplayPowerMode.java diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 8be413ca21..2c395bd916 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -183,6 +183,8 @@ 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(); } } 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/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();