Add --capture-orientation

Deprecate --lock-video-orientation in favor of a more general option
--capture-orientation, which supports all possible orientations
(0, 90, 180, 270, flip0, flip90, flip180, flip270), and a "locked" flag
via a '@' prefix.

All the old "locked video orientations" are supported:
 - --lock-video-orientation      ->  --capture-orientation=@
 - --lock-video-orientation=0    ->  --capture-orientation=@0
 - --lock-video-orientation=90   ->  --capture-orientation=@90
 - --lock-video-orientation=180  ->  --capture-orientation=@180
 - --lock-video-orientation=270  ->  --capture-orientation=@270

In addition, --capture-orientation can rotate/flip the display without
locking, so that it follows the physical device rotation.

For example:

    scrcpy --capture-orientation=flip90

always flips and rotates the capture by 90° clockwise.

The arguments are consistent with --orientation (which provides a
separate client-side orientation).
This commit is contained in:
Romain Vimont 2024-11-14 20:19:40 +01:00
parent d785ba6ad3
commit 5a56f7aa79
16 changed files with 229 additions and 138 deletions

View File

@ -17,6 +17,7 @@ _scrcpy() {
--camera-fps= --camera-fps=
--camera-high-speed --camera-high-speed
--camera-size= --camera-size=
--capture-orientation=
--crop= --crop=
-d --select-usb -d --select-usb
--disable-screensaver --disable-screensaver
@ -37,8 +38,6 @@ _scrcpy() {
--list-cameras --list-cameras
--list-displays --list-displays
--list-encoders --list-encoders
--lock-video-orientation
--lock-video-orientation=
-m --max-size= -m --max-size=
-M -M
--max-fps= --max-fps=

View File

@ -24,6 +24,7 @@ arguments=(
'--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)' '--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)'
'--camera-fps=[Specify the camera capture frame rate]' '--camera-fps=[Specify the camera capture frame rate]'
'--camera-size=[Specify an explicit camera capture size]' '--camera-size=[Specify an explicit camera capture size]'
'--capture-orientation=[Set the capture video orientation]:orientation:(0 90 180 270 flip0 flip90 flip180 flip270 #0 #90 #180 #270 #flip0 #flip90 #flip180 #flip270)'
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
{-d,--select-usb}'[Use USB device]' {-d,--select-usb}'[Use USB device]'
'--disable-screensaver[Disable screensaver while scrcpy is running]' '--disable-screensaver[Disable screensaver while scrcpy is running]'
@ -44,7 +45,6 @@ arguments=(
'--list-cameras[List cameras available on the device]' '--list-cameras[List cameras available on the device]'
'--list-displays[List displays available on the device]' '--list-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders 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 90 180 270)'
{-m,--max-size=}'[Limit both the width and height of the video to value]' {-m,--max-size=}'[Limit both the width and height of the video to value]'
'-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]' '-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]'
'--max-fps=[Limit the frame rate of screen capture]' '--max-fps=[Limit the frame rate of screen capture]'

View File

@ -121,6 +121,18 @@ If not specified, Android's default frame rate (30 fps) is used.
.BI "\-\-camera\-size " width\fRx\fIheight .BI "\-\-camera\-size " width\fRx\fIheight
Specify an explicit camera capture size. Specify an explicit camera capture size.
.TP
.BI "\-\-capture\-orientation " value
Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'.
The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation.
If a leading '@' is passed (@90) for display capture, then the rotation is locked, and is relative to the natural device orientation.
If '@' is passed alone, then the rotation is locked to the initial device orientation.
Default is 0.
.TP .TP
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
Crop the device screen on the server. Crop the device screen on the server.
@ -241,16 +253,6 @@ List video and audio encoders available on the device.
.B \-\-list\-displays .B \-\-list\-displays
List displays available on the device. List displays available on the device.
.TP
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR]
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".
Passing the option without argument is equivalent to passing "initial".
.TP .TP
.BI "\-m, \-\-max\-size " value .BI "\-m, \-\-max\-size " value
Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved.

View File

@ -107,6 +107,7 @@ enum {
OPT_LIST_APPS, OPT_LIST_APPS,
OPT_START_APP, OPT_START_APP,
OPT_SCREEN_OFF_TIMEOUT, OPT_SCREEN_OFF_TIMEOUT,
OPT_CAPTURE_ORIENTATION,
}; };
struct sc_option { struct sc_option {
@ -471,18 +472,27 @@ static const struct sc_option options[] = {
.text = "List video and audio encoders available on the device.", .text = "List video and audio encoders available on the device.",
}, },
{ {
.longopt_id = OPT_CAPTURE_ORIENTATION,
.longopt = "capture-orientation",
.argdesc = "value",
.text = "Set the capture video orientation.\n"
"Possible values are 0, 90, 180, 270, flip0, flip90, flip180 "
"and flip270, possibly prefixed by '@'.\n"
"The number represents the clockwise rotation in degrees; the "
"flip\" keyword applies a horizontal flip before the "
"rotation.\n"
"If a leading '@' is passed (@90) for display capture, then "
"the rotation is locked, and is relative to the natural device "
"orientation.\n"
"If '@' is passed alone, then the rotation is locked to the "
"initial device orientation.\n"
"Default is 0.",
},
{
// deprecated
.longopt_id = OPT_LOCK_VIDEO_ORIENTATION, .longopt_id = OPT_LOCK_VIDEO_ORIENTATION,
.longopt = "lock-video-orientation", .longopt = "lock-video-orientation",
.argdesc = "value", .argdesc = "value",
.optional_arg = true,
.text = "Lock capture video orientation to value.\n"
"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.\n"
"Default is \"unlocked\".\n"
"Passing the option without argument is equivalent to passing "
"\"initial\".",
}, },
{ {
.shortopt = 'm', .shortopt = 'm',
@ -1582,66 +1592,6 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) {
return true; return true;
} }
static bool
parse_lock_video_orientation(const char *s,
enum sc_lock_video_orientation *lock_mode) {
if (!s || !strcmp(s, "initial")) {
// Without argument, lock the initial orientation
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
return true;
}
if (!strcmp(s, "unlocked")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED;
return true;
}
if (!strcmp(s, "0")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_0;
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 static bool
parse_rotation(const char *s, uint8_t *rotation) { parse_rotation(const char *s, uint8_t *rotation) {
long value; long value;
@ -1693,6 +1643,32 @@ parse_orientation(const char *s, enum sc_orientation *orientation) {
return false; return false;
} }
static bool
parse_capture_orientation(const char *s, enum sc_orientation *orientation,
enum sc_orientation_lock *lock) {
if (*s == '\0') {
LOGE("Capture orientation may not be empty (expected 0, 90, 180, 270, "
"flip0, flip90, flip180 or flip270, possibly prefixed by '@')");
return false;
}
// Lock the orientation by a leading '@'
if (s[0] == '@') {
// Consume '@'
++s;
if (*s == '\0') {
// Only '@': lock to the initial orientation (orientation is unused)
*lock = SC_ORIENTATION_LOCKED_INITIAL;
return true;
}
*lock = SC_ORIENTATION_LOCKED_VALUE;
} else {
*lock = SC_ORIENTATION_UNLOCKED;
}
return parse_orientation(s, orientation);
}
static bool static bool
parse_window_position(const char *s, int16_t *position) { parse_window_position(const char *s, int16_t *position) {
// special value for "auto" // special value for "auto"
@ -2367,8 +2343,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
"--mouse=uhid instead."); "--mouse=uhid instead.");
return false; return false;
case OPT_LOCK_VIDEO_ORIENTATION: case OPT_LOCK_VIDEO_ORIENTATION:
if (!parse_lock_video_orientation(optarg, LOGE("--lock-video-orientation has been removed, use "
&opts->lock_video_orientation)) { "--capture-orientation instead.");
return false;
case OPT_CAPTURE_ORIENTATION:
if (!parse_capture_orientation(optarg,
&opts->capture_orientation,
&opts->capture_orientation_lock)) {
return false; return false;
} }
break; break;
@ -2852,13 +2833,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false; return false;
} }
if (opts->lock_video_orientation ==
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
LOGI("Video orientation is locked for v4l2 sink. "
"See --lock-video-orientation.");
opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
}
// V4L2 could not handle size change. // V4L2 could not handle size change.
// Do not log because downsizing on error is the default behavior, // Do not log because downsizing on error is the default behavior,
// not an explicit request from the user. // not an explicit request from the user.

View File

@ -50,7 +50,8 @@ const struct scrcpy_options scrcpy_options_default = {
.video_bit_rate = 0, .video_bit_rate = 0,
.audio_bit_rate = 0, .audio_bit_rate = 0,
.max_fps = NULL, .max_fps = NULL,
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, .capture_orientation = SC_ORIENTATION_0,
.capture_orientation_lock = SC_ORIENTATION_UNLOCKED,
.display_orientation = SC_ORIENTATION_0, .display_orientation = SC_ORIENTATION_0,
.record_orientation = SC_ORIENTATION_0, .record_orientation = SC_ORIENTATION_0,
.window_x = SC_WINDOW_POSITION_UNDEFINED, .window_x = SC_WINDOW_POSITION_UNDEFINED,

View File

@ -84,6 +84,12 @@ enum sc_orientation { // v v v
SC_ORIENTATION_FLIP_270, // 1 1 1 SC_ORIENTATION_FLIP_270, // 1 1 1
}; };
enum sc_orientation_lock {
SC_ORIENTATION_UNLOCKED,
SC_ORIENTATION_LOCKED_VALUE, // lock to specified orientation
SC_ORIENTATION_LOCKED_INITIAL, // lock to initial device orientation
};
static inline bool static inline bool
sc_orientation_is_mirror(enum sc_orientation orientation) { sc_orientation_is_mirror(enum sc_orientation orientation) {
assert(!(orientation & ~7)); assert(!(orientation & ~7));
@ -130,16 +136,6 @@ sc_orientation_get_name(enum sc_orientation orientation) {
} }
} }
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_90 = 3,
SC_LOCK_VIDEO_ORIENTATION_180 = 2,
SC_LOCK_VIDEO_ORIENTATION_270 = 1,
};
enum sc_keyboard_input_mode { enum sc_keyboard_input_mode {
SC_KEYBOARD_INPUT_MODE_AUTO, SC_KEYBOARD_INPUT_MODE_AUTO,
SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode
@ -251,7 +247,8 @@ struct scrcpy_options {
uint32_t video_bit_rate; uint32_t video_bit_rate;
uint32_t audio_bit_rate; uint32_t audio_bit_rate;
const char *max_fps; // float to be parsed by the server const char *max_fps; // float to be parsed by the server
enum sc_lock_video_orientation lock_video_orientation; enum sc_orientation capture_orientation;
enum sc_orientation_lock capture_orientation_lock;
enum sc_orientation display_orientation; enum sc_orientation display_orientation;
enum sc_orientation record_orientation; enum sc_orientation record_orientation;
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"

View File

@ -429,7 +429,8 @@ scrcpy(struct scrcpy_options *options) {
.audio_bit_rate = options->audio_bit_rate, .audio_bit_rate = options->audio_bit_rate,
.max_fps = options->max_fps, .max_fps = options->max_fps,
.screen_off_timeout = options->screen_off_timeout, .screen_off_timeout = options->screen_off_timeout,
.lock_video_orientation = options->lock_video_orientation, .capture_orientation = options->capture_orientation,
.capture_orientation_lock = options->capture_orientation_lock,
.control = options->control, .control = options->control,
.display_id = options->display_id, .display_id = options->display_id,
.new_display = options->new_display, .new_display = options->new_display,

View File

@ -274,9 +274,17 @@ execute_server(struct sc_server *server,
VALIDATE_STRING(params->max_fps); VALIDATE_STRING(params->max_fps);
ADD_PARAM("max_fps=%s", params->max_fps); ADD_PARAM("max_fps=%s", params->max_fps);
} }
if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { if (params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED
ADD_PARAM("lock_video_orientation=%" PRIi8, || params->capture_orientation != SC_ORIENTATION_0) {
params->lock_video_orientation); if (params->capture_orientation_lock == SC_ORIENTATION_LOCKED_INITIAL) {
ADD_PARAM("capture_orientation=@");
} else {
const char *orient =
sc_orientation_get_name(params->capture_orientation);
bool locked =
params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED;
ADD_PARAM("capture_orientation=%s%s", locked ? "@" : "", orient);
}
} }
if (server->tunnel.forward) { if (server->tunnel.forward) {
ADD_PARAM("tunnel_forward=true"); ADD_PARAM("tunnel_forward=true");

View File

@ -46,7 +46,8 @@ struct sc_server_params {
uint32_t audio_bit_rate; uint32_t audio_bit_rate;
const char *max_fps; // float to be parsed by the server const char *max_fps; // float to be parsed by the server
sc_tick screen_off_timeout; sc_tick screen_off_timeout;
int8_t lock_video_orientation; enum sc_orientation capture_orientation;
enum sc_orientation_lock capture_orientation_lock;
bool control; bool control;
uint32_t display_id; uint32_t display_id;
const char *new_display; const char *new_display;

View File

@ -51,7 +51,6 @@ static void test_options(void) {
"--fullscreen", "--fullscreen",
"--max-fps", "30", "--max-fps", "30",
"--max-size", "1024", "--max-size", "1024",
"--lock-video-orientation=2", // optional arguments require '='
// "--no-control" is not compatible with "--turn-screen-off" // "--no-control" is not compatible with "--turn-screen-off"
// "--no-playback" is not compatible with "--fulscreen" // "--no-playback" is not compatible with "--fulscreen"
"--port", "1234:1236", "--port", "1234:1236",
@ -80,7 +79,6 @@ static void test_options(void) {
assert(opts->fullscreen); assert(opts->fullscreen);
assert(!strcmp(opts->max_fps, "30")); assert(!strcmp(opts->max_fps, "30"));
assert(opts->max_size == 1024); assert(opts->max_size == 1024);
assert(opts->lock_video_orientation == 2);
assert(opts->port_range.first == 1234); assert(opts->port_range.first == 1234);
assert(opts->port_range.last == 1236); assert(opts->port_range.last == 1236);
assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->push_target, "/sdcard/Movies"));

View File

@ -103,21 +103,39 @@ The orientation may be applied at 3 different levels:
- The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the - The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the
device to switch between portrait and landscape (the current running app may device to switch between portrait and landscape (the current running app may
refuse, if it does not support the requested orientation). refuse, if it does not support the requested orientation).
- `--lock-video-orientation` changes the mirroring orientation (the orientation - `--capture-orientation` changes the mirroring orientation (the orientation
of the video sent from the device to the computer). This affects the of the video sent from the device to the computer). This affects the
recording. recording.
- `--orientation` is applied on the client side, and affects display and - `--orientation` is applied on the client side, and affects display and
recording. For the display, it can be changed dynamically using recording. For the display, it can be changed dynamically using
[shortcuts](shortcuts.md). [shortcuts](shortcuts.md).
To lock the mirroring orientation (on the capture side): To capture the video with a specific orientation:
```bash ```bash
scrcpy --lock-video-orientation # initial (current) orientation scrcpy --capture-orientation=0
scrcpy --lock-video-orientation=0 # natural orientation scrcpy --capture-orientation=90 # 90° clockwise
scrcpy --lock-video-orientation=90 # 90° clockwise scrcpy --capture-orientation=180 # 180°
scrcpy --lock-video-orientation=180 # 180° scrcpy --capture-orientation=270 # 270° clockwise
scrcpy --lock-video-orientation=270 # 270° clockwise scrcpy --capture-orientation=flip0 # hflip
scrcpy --capture-orientation=flip90 # hflip + 90° clockwise
scrcpy --capture-orientation=flip180 # hflip + 180°
scrcpy --capture-orientation=flip270 # hflip + 270° clockwise
```
The capture orientation can be locked by using `@`, so that a physical device
rotation does not change the captured video orientation:
```bash
scrcpy --capture-orientation=@ # locked to the initial orientation
scrcpy --capture-orientation=@0 # locked to 0°
scrcpy --capture-orientation=@90 # locked to 90° clockwise
scrcpy --capture-orientation=@180 # locked to 180°
scrcpy --capture-orientation=@270 # locked to 270° clockwise
scrcpy --capture-orientation=@flip0 # locked to hflip
scrcpy --capture-orientation=@flip90 # locked to hflip + 90° clockwise
scrcpy --capture-orientation=@flip180 # locked to hflip + 180°
scrcpy --capture-orientation=@flip270 # locked to hflip + 270° clockwise
``` ```
To orient the video (on the rendering side): To orient the video (on the rendering side):

View File

@ -4,6 +4,7 @@ import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.audio.AudioSource; import com.genymobile.scrcpy.audio.AudioSource;
import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.NewDisplay;
import com.genymobile.scrcpy.device.Orientation;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.CodecOption; import com.genymobile.scrcpy.util.CodecOption;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
@ -13,6 +14,7 @@ import com.genymobile.scrcpy.video.VideoCodec;
import com.genymobile.scrcpy.video.VideoSource; import com.genymobile.scrcpy.video.VideoSource;
import android.graphics.Rect; import android.graphics.Rect;
import android.util.Pair;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -32,7 +34,6 @@ public class Options {
private int videoBitRate = 8000000; private int videoBitRate = 8000000;
private int audioBitRate = 128000; private int audioBitRate = 128000;
private float maxFps; private float maxFps;
private int lockVideoOrientation = -1;
private boolean tunnelForward; private boolean tunnelForward;
private Rect crop; private Rect crop;
private boolean control = true; private boolean control = true;
@ -59,6 +60,9 @@ public class Options {
private NewDisplay newDisplay; private NewDisplay newDisplay;
private Orientation.Lock captureOrientationLock = Orientation.Lock.Unlocked;
private Orientation captureOrientation = Orientation.Orient0;
private boolean listEncoders; private boolean listEncoders;
private boolean listDisplays; private boolean listDisplays;
private boolean listCameras; private boolean listCameras;
@ -123,10 +127,6 @@ public class Options {
return maxFps; return maxFps;
} }
public int getLockVideoOrientation() {
return lockVideoOrientation;
}
public boolean isTunnelForward() { public boolean isTunnelForward() {
return tunnelForward; return tunnelForward;
} }
@ -219,6 +219,14 @@ public class Options {
return newDisplay; return newDisplay;
} }
public Orientation getCaptureOrientation() {
return captureOrientation;
}
public Orientation.Lock getCaptureOrientationLock() {
return captureOrientationLock;
}
public boolean getList() { public boolean getList() {
return listEncoders || listDisplays || listCameras || listCameraSizes || listApps; return listEncoders || listDisplays || listCameras || listCameraSizes || listApps;
} }
@ -341,9 +349,6 @@ public class Options {
case "max_fps": case "max_fps":
options.maxFps = parseFloat("max_fps", value); options.maxFps = parseFloat("max_fps", value);
break; break;
case "lock_video_orientation":
options.lockVideoOrientation = Integer.parseInt(value);
break;
case "tunnel_forward": case "tunnel_forward":
options.tunnelForward = Boolean.parseBoolean(value); options.tunnelForward = Boolean.parseBoolean(value);
break; break;
@ -448,6 +453,11 @@ public class Options {
case "new_display": case "new_display":
options.newDisplay = parseNewDisplay(value); options.newDisplay = parseNewDisplay(value);
break; break;
case "capture_orientation":
Pair<Orientation.Lock, Orientation> pair = parseCaptureOrientation(value);
options.captureOrientationLock = pair.first;
options.captureOrientation = pair.second;
break;
case "send_device_meta": case "send_device_meta":
options.sendDeviceMeta = Boolean.parseBoolean(value); options.sendDeviceMeta = Boolean.parseBoolean(value);
break; break;
@ -571,4 +581,25 @@ public class Options {
return new NewDisplay(size, dpi); return new NewDisplay(size, dpi);
} }
private static Pair<Orientation.Lock, Orientation> parseCaptureOrientation(String value) {
if (value.isEmpty()) {
throw new IllegalArgumentException("Empty capture orientation string");
}
Orientation.Lock lock;
if (value.charAt(0) == '@') {
// Consume '@'
value = value.substring(1);
if (value.isEmpty()) {
// Only '@': lock to the initial orientation (orientation is unused)
return Pair.create(Orientation.Lock.LockedInitial, Orientation.Orient0);
}
lock = Orientation.Lock.LockedValue;
} else {
lock = Orientation.Lock.Unlocked;
}
return Pair.create(lock, Orientation.getByName(value));
}
} }

View File

@ -40,9 +40,6 @@ public final class Device {
public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT; public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT;
public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH; public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH;
public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
private Device() { private Device() {
// not instantiable // not instantiable
} }

View File

@ -0,0 +1,47 @@
package com.genymobile.scrcpy.device;
public enum Orientation {
// @formatter:off
Orient0("0"),
Orient90("90"),
Orient180("180"),
Orient270("270"),
Flip0("flip0"),
Flip90("flip90"),
Flip180("flip180"),
Flip270("flip270");
public enum Lock {
Unlocked, LockedInitial, LockedValue,
}
private final String name;
Orientation(String name) {
this.name = name;
}
public static Orientation getByName(String name) {
for (Orientation orientation : values()) {
if (orientation.name.equals(name)) {
return orientation;
}
}
throw new IllegalArgumentException("Unknown orientation: " + name);
}
public static Orientation fromRotation(int rotation) {
assert rotation >= 0 && rotation < 4;
return values()[rotation];
}
public boolean isFlipped() {
return (ordinal() & 4) != 0;
}
public int getRotation() {
return this.ordinal() & 3;
}
}

View File

@ -6,6 +6,7 @@ import com.genymobile.scrcpy.control.PositionMapper;
import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Orientation;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; import com.genymobile.scrcpy.opengl.AffineOpenGLFilter;
import com.genymobile.scrcpy.opengl.OpenGLFilter; import com.genymobile.scrcpy.opengl.OpenGLFilter;
@ -30,7 +31,8 @@ public class ScreenCapture extends SurfaceCapture {
private final int displayId; private final int displayId;
private int maxSize; private int maxSize;
private final Rect crop; private final Rect crop;
private int lockVideoOrientation; private Orientation.Lock captureOrientationLock;
private Orientation captureOrientation;
private DisplayInfo displayInfo; private DisplayInfo displayInfo;
private Size videoSize; private Size videoSize;
@ -49,7 +51,10 @@ public class ScreenCapture extends SurfaceCapture {
assert displayId != Device.DISPLAY_ID_NONE; assert displayId != Device.DISPLAY_ID_NONE;
this.maxSize = options.getMaxSize(); this.maxSize = options.getMaxSize();
this.crop = options.getCrop(); this.crop = options.getCrop();
this.lockVideoOrientation = options.getLockVideoOrientation(); this.captureOrientationLock = options.getCaptureOrientationLock();
this.captureOrientation = options.getCaptureOrientation();
assert captureOrientationLock != null;
assert captureOrientation != null;
} }
@Override @Override
@ -72,9 +77,10 @@ public class ScreenCapture extends SurfaceCapture {
Size displaySize = displayInfo.getSize(); Size displaySize = displayInfo.getSize();
displaySizeMonitor.setSessionDisplaySize(displaySize); displaySizeMonitor.setSessionDisplaySize(displaySize);
if (lockVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { if (captureOrientationLock == Orientation.Lock.LockedInitial) {
// The user requested to lock the video orientation to the current orientation // The user requested to lock the video orientation to the current orientation
lockVideoOrientation = displayInfo.getRotation(); captureOrientationLock = Orientation.Lock.LockedValue;
captureOrientation = Orientation.fromRotation(displayInfo.getRotation());
} }
VideoFilter filter = new VideoFilter(displaySize); VideoFilter filter = new VideoFilter(displaySize);
@ -84,9 +90,8 @@ public class ScreenCapture extends SurfaceCapture {
filter.addCrop(crop, transposed); filter.addCrop(crop, transposed);
} }
if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_UNLOCKED) { boolean locked = captureOrientationLock != Orientation.Lock.Unlocked;
filter.addLockVideoOrientation(lockVideoOrientation, displayInfo.getRotation()); filter.addOrientation(displayInfo.getRotation(), locked, captureOrientation);
}
transform = filter.getInverseTransform(); transform = filter.getInverseTransform();
videoSize = filter.getOutputSize().limit(maxSize).round8(); videoSize = filter.getOutputSize().limit(maxSize).round8();

View File

@ -1,5 +1,6 @@
package com.genymobile.scrcpy.video; package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.device.Orientation;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.AffineMatrix;
@ -72,9 +73,20 @@ public class VideoFilter {
size = size.rotate(); size = size.rotate();
} }
} }
public void addOrientation(Orientation captureOrientation) {
public void addLockVideoOrientation(int lockVideoOrientation, int displayRotation) { if (captureOrientation.isFlipped()) {
int ccwRotation = (4 + lockVideoOrientation - displayRotation) % 4; transform = AffineMatrix.hflip().multiply(transform);
}
int ccwRotation = (4 - captureOrientation.getRotation()) % 4;
addRotation(ccwRotation); addRotation(ccwRotation);
} }
public void addOrientation(int displayRotation, boolean locked, Orientation captureOrientation) {
if (locked) {
// flip/rotate the current display from the natural device orientation (i.e. where display rotation is 0)
int reverseDisplayRotation = (4 - displayRotation) % 4;
addRotation(reverseDisplayRotation);
}
addOrientation(captureOrientation);
}
} }