mirror of
https://github.com/Genymobile/scrcpy.git
synced 2026-02-23 23:04:43 +01:00
Compare commits
11 Commits
glfilter.5
...
glfilter.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c09b8a767d | ||
|
|
52568dcb97 | ||
|
|
4ab704df6a | ||
|
|
2fa35849d8 | ||
|
|
d255bb9332 | ||
|
|
5f215cf1a4 | ||
|
|
010753ca3b | ||
|
|
e76c70729b | ||
|
|
52097e25b4 | ||
|
|
ab7a2e42d8 | ||
|
|
8c22df301e |
@@ -2,6 +2,7 @@ _scrcpy() {
|
||||
local cur prev words cword
|
||||
local opts="
|
||||
--always-on-top
|
||||
--angle
|
||||
--audio-bit-rate=
|
||||
--audio-buffer=
|
||||
--audio-codec=
|
||||
@@ -17,6 +18,7 @@ _scrcpy() {
|
||||
--camera-fps=
|
||||
--camera-high-speed
|
||||
--camera-size=
|
||||
--capture-orientation=
|
||||
--crop=
|
||||
-d --select-usb
|
||||
--disable-screensaver
|
||||
@@ -37,8 +39,6 @@ _scrcpy() {
|
||||
--list-cameras
|
||||
--list-displays
|
||||
--list-encoders
|
||||
--lock-video-orientation
|
||||
--lock-video-orientation=
|
||||
-m --max-size=
|
||||
-M
|
||||
--max-fps=
|
||||
@@ -57,6 +57,7 @@ _scrcpy() {
|
||||
--no-mipmaps
|
||||
--no-mouse-hover
|
||||
--no-power-on
|
||||
--no-vd-system-decorations
|
||||
--no-video
|
||||
--no-video-playback
|
||||
--orientation=
|
||||
|
||||
@@ -9,6 +9,7 @@ local arguments
|
||||
|
||||
arguments=(
|
||||
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
|
||||
'--angle=[Rotate the video content by a custom angle, in degrees]'
|
||||
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
|
||||
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
|
||||
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)'
|
||||
@@ -24,6 +25,7 @@ arguments=(
|
||||
'--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)'
|
||||
'--camera-fps=[Specify the camera capture frame rate]'
|
||||
'--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]'
|
||||
{-d,--select-usb}'[Use USB device]'
|
||||
'--disable-screensaver[Disable screensaver while scrcpy is running]'
|
||||
@@ -44,7 +46,6 @@ 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 90 180 270)'
|
||||
{-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)]'
|
||||
'--max-fps=[Limit the frame rate of screen capture]'
|
||||
@@ -62,6 +63,7 @@ arguments=(
|
||||
'--no-mipmaps[Disable the generation of mipmaps]'
|
||||
'--no-mouse-hover[Do not forward mouse hover events]'
|
||||
'--no-power-on[Do not power on the device on start]'
|
||||
'--no-vd-system-decorations[Disable virtual display system decorations flag]'
|
||||
'--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)'
|
||||
|
||||
30
app/scrcpy.1
30
app/scrcpy.1
@@ -19,6 +19,10 @@ provides display and control of Android devices connected on USB (or over TCP/IP
|
||||
.B \-\-always\-on\-top
|
||||
Make scrcpy window always on top (above other windows).
|
||||
|
||||
.TP
|
||||
.BI "\-\-angle " degrees
|
||||
Rotate the video content by a custom angle, in degrees (clockwise).
|
||||
|
||||
.TP
|
||||
.BI "\-\-audio\-bit\-rate " value
|
||||
Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
||||
@@ -121,6 +125,18 @@ If not specified, Android's default frame rate (30 fps) is used.
|
||||
.BI "\-\-camera\-size " width\fRx\fIheight
|
||||
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
|
||||
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
||||
Crop the device screen on the server.
|
||||
@@ -241,16 +257,6 @@ List video and audio encoders available on the device.
|
||||
.B \-\-list\-displays
|
||||
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
|
||||
.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.
|
||||
@@ -366,6 +372,10 @@ Do not forward mouse hover (mouse motion without any clicks) events.
|
||||
.B \-\-no\-power\-on
|
||||
Do not power on the device on start.
|
||||
|
||||
.TP
|
||||
.B \-\-no\-vd\-system\-decorations
|
||||
Disable virtual display system decorations flag.
|
||||
|
||||
.TP
|
||||
.B \-\-no\-video
|
||||
Disable video forwarding.
|
||||
|
||||
222
app/src/cli.c
222
app/src/cli.c
@@ -107,6 +107,9 @@ enum {
|
||||
OPT_LIST_APPS,
|
||||
OPT_START_APP,
|
||||
OPT_SCREEN_OFF_TIMEOUT,
|
||||
OPT_CAPTURE_ORIENTATION,
|
||||
OPT_ANGLE,
|
||||
OPT_NO_VD_SYSTEM_DECORATIONS,
|
||||
};
|
||||
|
||||
struct sc_option {
|
||||
@@ -148,6 +151,13 @@ static const struct sc_option options[] = {
|
||||
.longopt = "always-on-top",
|
||||
.text = "Make scrcpy window always on top (above other windows).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_ANGLE,
|
||||
.longopt = "angle",
|
||||
.argdesc = "degrees",
|
||||
.text = "Rotate the video content by a custom angle, in degrees "
|
||||
"(clockwise).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_AUDIO_BIT_RATE,
|
||||
.longopt = "audio-bit-rate",
|
||||
@@ -471,18 +481,27 @@ static const struct sc_option options[] = {
|
||||
.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 = "lock-video-orientation",
|
||||
.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',
|
||||
@@ -642,6 +661,11 @@ static const struct sc_option options[] = {
|
||||
.longopt = "no-power-on",
|
||||
.text = "Do not power on the device on start.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_NO_VD_SYSTEM_DECORATIONS,
|
||||
.longopt = "no-vd-system-decorations",
|
||||
.text = "Disable virtual display system decorations flag.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_NO_VIDEO,
|
||||
.longopt = "no-video",
|
||||
@@ -1582,78 +1606,6 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) {
|
||||
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
|
||||
parse_rotation(const char *s, uint8_t *rotation) {
|
||||
long value;
|
||||
bool ok = parse_integer_arg(s, &value, false, 0, 3, "rotation");
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*rotation = (uint8_t) value;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_orientation(const char *s, enum sc_orientation *orientation) {
|
||||
if (!strcmp(s, "0")) {
|
||||
@@ -1693,6 +1645,32 @@ parse_orientation(const char *s, enum sc_orientation *orientation) {
|
||||
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
|
||||
parse_window_position(const char *s, int16_t *position) {
|
||||
// special value for "auto"
|
||||
@@ -2302,8 +2280,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->crop = optarg;
|
||||
break;
|
||||
case OPT_DISPLAY:
|
||||
LOGW("--display is deprecated, use --display-id instead.");
|
||||
// fall through
|
||||
LOGE("--display has been removed, use --display-id instead.");
|
||||
return false;
|
||||
case OPT_DISPLAY_ID:
|
||||
if (!parse_display_id(optarg, &opts->display_id)) {
|
||||
return false;
|
||||
@@ -2367,8 +2345,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
"--mouse=uhid instead.");
|
||||
return false;
|
||||
case OPT_LOCK_VIDEO_ORIENTATION:
|
||||
if (!parse_lock_video_orientation(optarg,
|
||||
&opts->lock_video_orientation)) {
|
||||
LOGE("--lock-video-orientation has been removed, use "
|
||||
"--capture-orientation instead.");
|
||||
return false;
|
||||
case OPT_CAPTURE_ORIENTATION:
|
||||
if (!parse_capture_orientation(optarg,
|
||||
&opts->capture_orientation,
|
||||
&opts->capture_orientation_lock)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
@@ -2386,8 +2369,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->control = false;
|
||||
break;
|
||||
case OPT_NO_DISPLAY:
|
||||
LOGW("--no-display is deprecated, use --no-playback instead.");
|
||||
// fall through
|
||||
LOGE("--no-display has been removed, use --no-playback "
|
||||
"instead.");
|
||||
return false;
|
||||
case 'N':
|
||||
opts->video_playback = false;
|
||||
opts->audio_playback = false;
|
||||
@@ -2473,32 +2457,9 @@ 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:
|
||||
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 3 was 270° counterclockwise, but orientation
|
||||
// is expressed clockwise
|
||||
opts->display_orientation = SC_ORIENTATION_90;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
LOGE("--rotation has been removed, use --orientation or "
|
||||
"--capture-orientation instead.");
|
||||
return false;
|
||||
case OPT_DISPLAY_ORIENTATION:
|
||||
if (!parse_orientation(optarg, &opts->display_orientation)) {
|
||||
return false;
|
||||
@@ -2559,23 +2520,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
break;
|
||||
case OPT_FORWARD_ALL_CLICKS:
|
||||
LOGW("--forward-all-clicks is deprecated, "
|
||||
LOGE("--forward-all-clicks has been removed, "
|
||||
"use --mouse-bind=++++ instead.");
|
||||
opts->mouse_bindings = (struct sc_mouse_bindings) {
|
||||
.pri = {
|
||||
.right_click = SC_MOUSE_BINDING_CLICK,
|
||||
.middle_click = SC_MOUSE_BINDING_CLICK,
|
||||
.click4 = SC_MOUSE_BINDING_CLICK,
|
||||
.click5 = SC_MOUSE_BINDING_CLICK,
|
||||
},
|
||||
.sec = {
|
||||
.right_click = SC_MOUSE_BINDING_CLICK,
|
||||
.middle_click = SC_MOUSE_BINDING_CLICK,
|
||||
.click4 = SC_MOUSE_BINDING_CLICK,
|
||||
.click5 = SC_MOUSE_BINDING_CLICK,
|
||||
},
|
||||
};
|
||||
break;
|
||||
return false;
|
||||
case OPT_LEGACY_PASTE:
|
||||
opts->legacy_paste = true;
|
||||
break;
|
||||
@@ -2583,9 +2530,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->power_off_on_close = true;
|
||||
break;
|
||||
case OPT_DISPLAY_BUFFER:
|
||||
LOGW("--display-buffer is deprecated, use --video-buffer "
|
||||
LOGE("--display-buffer has been removed, use --video-buffer "
|
||||
"instead.");
|
||||
// fall through
|
||||
return false;
|
||||
case OPT_VIDEO_BUFFER:
|
||||
if (!parse_buffering_time(optarg, &opts->video_buffer)) {
|
||||
return false;
|
||||
@@ -2758,6 +2705,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_ANGLE:
|
||||
opts->angle = optarg;
|
||||
break;
|
||||
case OPT_NO_VD_SYSTEM_DECORATIONS:
|
||||
opts->vd_system_decorations = optarg;
|
||||
break;
|
||||
default:
|
||||
// getopt prints the error message on stderr
|
||||
return false;
|
||||
@@ -2852,13 +2805,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
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.
|
||||
// Do not log because downsizing on error is the default behavior,
|
||||
// not an explicit request from the user.
|
||||
|
||||
@@ -50,7 +50,8 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.video_bit_rate = 0,
|
||||
.audio_bit_rate = 0,
|
||||
.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,
|
||||
.record_orientation = SC_ORIENTATION_0,
|
||||
.window_x = SC_WINDOW_POSITION_UNDEFINED,
|
||||
@@ -106,6 +107,8 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.audio_dup = false,
|
||||
.new_display = NULL,
|
||||
.start_app = NULL,
|
||||
.angle = NULL,
|
||||
.vd_system_decorations = true,
|
||||
};
|
||||
|
||||
enum sc_orientation
|
||||
|
||||
@@ -84,6 +84,12 @@ enum sc_orientation { // v v v
|
||||
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
|
||||
sc_orientation_is_mirror(enum sc_orientation orientation) {
|
||||
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 {
|
||||
SC_KEYBOARD_INPUT_MODE_AUTO,
|
||||
SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode
|
||||
@@ -251,7 +247,9 @@ struct scrcpy_options {
|
||||
uint32_t video_bit_rate;
|
||||
uint32_t audio_bit_rate;
|
||||
const char *max_fps; // float to be parsed by the server
|
||||
enum sc_lock_video_orientation lock_video_orientation;
|
||||
const char *angle; // float to be parsed by the server
|
||||
enum sc_orientation capture_orientation;
|
||||
enum sc_orientation_lock capture_orientation_lock;
|
||||
enum sc_orientation display_orientation;
|
||||
enum sc_orientation record_orientation;
|
||||
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
|
||||
@@ -312,6 +310,7 @@ struct scrcpy_options {
|
||||
bool audio_dup;
|
||||
const char *new_display; // [<width>x<height>][/<dpi>] parsed by the server
|
||||
const char *start_app;
|
||||
bool vd_system_decorations;
|
||||
};
|
||||
|
||||
extern const struct scrcpy_options scrcpy_options_default;
|
||||
|
||||
@@ -428,8 +428,10 @@ scrcpy(struct scrcpy_options *options) {
|
||||
.video_bit_rate = options->video_bit_rate,
|
||||
.audio_bit_rate = options->audio_bit_rate,
|
||||
.max_fps = options->max_fps,
|
||||
.angle = options->angle,
|
||||
.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,
|
||||
.display_id = options->display_id,
|
||||
.new_display = options->new_display,
|
||||
@@ -456,6 +458,7 @@ scrcpy(struct scrcpy_options *options) {
|
||||
.power_on = options->power_on,
|
||||
.kill_adb_on_close = options->kill_adb_on_close,
|
||||
.camera_high_speed = options->camera_high_speed,
|
||||
.vd_system_decorations = options->vd_system_decorations,
|
||||
.list = options->list,
|
||||
};
|
||||
|
||||
|
||||
@@ -274,9 +274,21 @@ execute_server(struct sc_server *server,
|
||||
VALIDATE_STRING(params->max_fps);
|
||||
ADD_PARAM("max_fps=%s", params->max_fps);
|
||||
}
|
||||
if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
|
||||
ADD_PARAM("lock_video_orientation=%" PRIi8,
|
||||
params->lock_video_orientation);
|
||||
if (params->angle) {
|
||||
VALIDATE_STRING(params->angle);
|
||||
ADD_PARAM("angle=%s", params->angle);
|
||||
}
|
||||
if (params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED
|
||||
|| params->capture_orientation != SC_ORIENTATION_0) {
|
||||
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) {
|
||||
ADD_PARAM("tunnel_forward=true");
|
||||
@@ -364,6 +376,9 @@ execute_server(struct sc_server *server,
|
||||
VALIDATE_STRING(params->new_display);
|
||||
ADD_PARAM("new_display=%s", params->new_display);
|
||||
}
|
||||
if (!params->vd_system_decorations) {
|
||||
ADD_PARAM("vd_system_decorations=false");
|
||||
}
|
||||
if (params->list & SC_OPTION_LIST_ENCODERS) {
|
||||
ADD_PARAM("list_encoders=true");
|
||||
}
|
||||
|
||||
@@ -45,8 +45,10 @@ struct sc_server_params {
|
||||
uint32_t video_bit_rate;
|
||||
uint32_t audio_bit_rate;
|
||||
const char *max_fps; // float to be parsed by the server
|
||||
const char *angle; // float to be parsed by the server
|
||||
sc_tick screen_off_timeout;
|
||||
int8_t lock_video_orientation;
|
||||
enum sc_orientation capture_orientation;
|
||||
enum sc_orientation_lock capture_orientation_lock;
|
||||
bool control;
|
||||
uint32_t display_id;
|
||||
const char *new_display;
|
||||
@@ -67,6 +69,7 @@ struct sc_server_params {
|
||||
bool power_on;
|
||||
bool kill_adb_on_close;
|
||||
bool camera_high_speed;
|
||||
bool vd_system_decorations;
|
||||
uint8_t list;
|
||||
};
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ static void test_options(void) {
|
||||
"--fullscreen",
|
||||
"--max-fps", "30",
|
||||
"--max-size", "1024",
|
||||
"--lock-video-orientation=2", // optional arguments require '='
|
||||
// "--no-control" is not compatible with "--turn-screen-off"
|
||||
// "--no-playback" is not compatible with "--fulscreen"
|
||||
"--port", "1234:1236",
|
||||
@@ -80,7 +79,6 @@ static void test_options(void) {
|
||||
assert(opts->fullscreen);
|
||||
assert(!strcmp(opts->max_fps, "30"));
|
||||
assert(opts->max_size == 1024);
|
||||
assert(opts->lock_video_orientation == 2);
|
||||
assert(opts->port_range.first == 1234);
|
||||
assert(opts->port_range.last == 1236);
|
||||
assert(!strcmp(opts->push_target, "/sdcard/Movies"));
|
||||
|
||||
43
doc/video.md
43
doc/video.md
@@ -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
|
||||
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
|
||||
- `--capture-orientation` changes the mirroring orientation (the orientation
|
||||
of the video sent from the device to the computer). This affects the
|
||||
recording.
|
||||
- `--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 (on the capture side):
|
||||
To capture the video with a specific orientation:
|
||||
|
||||
```bash
|
||||
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
|
||||
scrcpy --capture-orientation=0
|
||||
scrcpy --capture-orientation=90 # 90° clockwise
|
||||
scrcpy --capture-orientation=180 # 180°
|
||||
scrcpy --capture-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):
|
||||
@@ -141,6 +159,17 @@ to the MP4 or MKV target file. Flipping is not supported, so only the 4 first
|
||||
values are allowed when recording.
|
||||
|
||||
|
||||
## Angle
|
||||
|
||||
To rotate the video content by a custom angle (in degrees, clockwise):
|
||||
|
||||
```
|
||||
scrcpy --angle=23
|
||||
```
|
||||
|
||||
The center of rotation is the center of the visible area (after cropping).
|
||||
|
||||
|
||||
## Crop
|
||||
|
||||
The device screen may be cropped to mirror only part of the screen.
|
||||
|
||||
@@ -24,3 +24,13 @@ For example:
|
||||
```bash
|
||||
scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc
|
||||
```
|
||||
|
||||
## System decorations
|
||||
|
||||
By default, virtual display system decorations are enabled. But some devices
|
||||
might display a broken UI;
|
||||
|
||||
Use `--no-vd-system-decorations` to disable it.
|
||||
|
||||
Note that if no app is started, no content will be rendered, so no video frame
|
||||
will be produced at all.
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.genymobile.scrcpy.audio.AudioCodec;
|
||||
import com.genymobile.scrcpy.audio.AudioSource;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.NewDisplay;
|
||||
import com.genymobile.scrcpy.device.Orientation;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.CodecOption;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
@@ -13,6 +14,7 @@ import com.genymobile.scrcpy.video.VideoCodec;
|
||||
import com.genymobile.scrcpy.video.VideoSource;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -32,7 +34,7 @@ public class Options {
|
||||
private int videoBitRate = 8000000;
|
||||
private int audioBitRate = 128000;
|
||||
private float maxFps;
|
||||
private int lockVideoOrientation = -1;
|
||||
private float angle;
|
||||
private boolean tunnelForward;
|
||||
private Rect crop;
|
||||
private boolean control = true;
|
||||
@@ -58,6 +60,10 @@ public class Options {
|
||||
private boolean powerOn = true;
|
||||
|
||||
private NewDisplay newDisplay;
|
||||
private boolean vdSystemDecorations = true;
|
||||
|
||||
private Orientation.Lock captureOrientationLock = Orientation.Lock.Unlocked;
|
||||
private Orientation captureOrientation = Orientation.Orient0;
|
||||
|
||||
private boolean listEncoders;
|
||||
private boolean listDisplays;
|
||||
@@ -123,8 +129,8 @@ public class Options {
|
||||
return maxFps;
|
||||
}
|
||||
|
||||
public int getLockVideoOrientation() {
|
||||
return lockVideoOrientation;
|
||||
public float getAngle() {
|
||||
return angle;
|
||||
}
|
||||
|
||||
public boolean isTunnelForward() {
|
||||
@@ -219,6 +225,18 @@ public class Options {
|
||||
return newDisplay;
|
||||
}
|
||||
|
||||
public Orientation getCaptureOrientation() {
|
||||
return captureOrientation;
|
||||
}
|
||||
|
||||
public Orientation.Lock getCaptureOrientationLock() {
|
||||
return captureOrientationLock;
|
||||
}
|
||||
|
||||
public boolean getVDSystemDecorations() {
|
||||
return vdSystemDecorations;
|
||||
}
|
||||
|
||||
public boolean getList() {
|
||||
return listEncoders || listDisplays || listCameras || listCameraSizes || listApps;
|
||||
}
|
||||
@@ -341,8 +359,8 @@ public class Options {
|
||||
case "max_fps":
|
||||
options.maxFps = parseFloat("max_fps", value);
|
||||
break;
|
||||
case "lock_video_orientation":
|
||||
options.lockVideoOrientation = Integer.parseInt(value);
|
||||
case "angle":
|
||||
options.angle = parseFloat("angle", value);
|
||||
break;
|
||||
case "tunnel_forward":
|
||||
options.tunnelForward = Boolean.parseBoolean(value);
|
||||
@@ -448,6 +466,14 @@ public class Options {
|
||||
case "new_display":
|
||||
options.newDisplay = parseNewDisplay(value);
|
||||
break;
|
||||
case "vd_system_decorations":
|
||||
options.vdSystemDecorations = Boolean.parseBoolean(value);
|
||||
break;
|
||||
case "capture_orientation":
|
||||
Pair<Orientation.Lock, Orientation> pair = parseCaptureOrientation(value);
|
||||
options.captureOrientationLock = pair.first;
|
||||
options.captureOrientation = pair.second;
|
||||
break;
|
||||
case "send_device_meta":
|
||||
options.sendDeviceMeta = Boolean.parseBoolean(value);
|
||||
break;
|
||||
@@ -571,4 +597,25 @@ public class Options {
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.DeviceApp;
|
||||
import com.genymobile.scrcpy.device.Point;
|
||||
import com.genymobile.scrcpy.device.Position;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
import com.genymobile.scrcpy.video.SurfaceCapture;
|
||||
@@ -359,7 +360,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
Point point = displayData.positionMapper.map(position);
|
||||
if (point == null) {
|
||||
Ln.w("Ignore touch event, it was generated for a different device size");
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Size eventSize = position.getScreenSize();
|
||||
Size currentSize = displayData.positionMapper.getVideoSize();
|
||||
Ln.v("Ignore touch event generated for size " + eventSize + " (current size is " + currentSize + ")");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -473,7 +478,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
Point point = displayData.positionMapper.map(position);
|
||||
if (point == null) {
|
||||
Ln.w("Ignore scroll event, it was generated for a different device size");
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Size eventSize = position.getScreenSize();
|
||||
Size currentSize = displayData.positionMapper.getVideoSize();
|
||||
Ln.v("Ignore scroll event generated for size " + eventSize + " (current size is " + currentSize + ")");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ public final class PositionMapper {
|
||||
return new PositionMapper(videoSize, transform);
|
||||
}
|
||||
|
||||
public Size getVideoSize() {
|
||||
return videoSize;
|
||||
}
|
||||
|
||||
public Point map(Position position) {
|
||||
Size clientVideoSize = position.getScreenSize();
|
||||
if (!videoSize.equals(clientVideoSize)) {
|
||||
|
||||
@@ -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_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() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,6 @@ public final class Size {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Size{" + width + 'x' + height + '}';
|
||||
return width + "x" + height;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public final class OpenGLRunner {
|
||||
private EGLSurface eglSurface;
|
||||
|
||||
private final OpenGLFilter filter;
|
||||
private final float[] overrideTransformMatrix;
|
||||
|
||||
private SurfaceTexture surfaceTexture;
|
||||
private Surface inputSurface;
|
||||
@@ -35,8 +36,13 @@ public final class OpenGLRunner {
|
||||
|
||||
private boolean stopped;
|
||||
|
||||
public OpenGLRunner(OpenGLFilter filter) {
|
||||
public OpenGLRunner(OpenGLFilter filter, float[] overrideTransformMatrix) {
|
||||
this.filter = filter;
|
||||
this.overrideTransformMatrix = overrideTransformMatrix;
|
||||
}
|
||||
|
||||
public OpenGLRunner(OpenGLFilter filter) {
|
||||
this(filter, null);
|
||||
}
|
||||
|
||||
public static synchronized void initOnce() {
|
||||
@@ -194,8 +200,14 @@ public final class OpenGLRunner {
|
||||
GLUtils.checkGlError();
|
||||
|
||||
surfaceTexture.updateTexImage();
|
||||
float[] matrix = new float[16];
|
||||
surfaceTexture.getTransformMatrix(matrix);
|
||||
|
||||
float[] matrix;
|
||||
if (overrideTransformMatrix != null) {
|
||||
matrix = overrideTransformMatrix;
|
||||
} else {
|
||||
matrix = new float[16];
|
||||
surfaceTexture.getTransformMatrix(matrix);
|
||||
}
|
||||
|
||||
filter.draw(textureId, matrix);
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@ package com.genymobile.scrcpy.video;
|
||||
import com.genymobile.scrcpy.AndroidVersions;
|
||||
import com.genymobile.scrcpy.Options;
|
||||
import com.genymobile.scrcpy.device.ConfigurationException;
|
||||
import com.genymobile.scrcpy.device.Orientation;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.opengl.AffineOpenGLFilter;
|
||||
import com.genymobile.scrcpy.opengl.OpenGLFilter;
|
||||
import com.genymobile.scrcpy.opengl.OpenGLRunner;
|
||||
import com.genymobile.scrcpy.util.AffineMatrix;
|
||||
import com.genymobile.scrcpy.util.HandlerExecutor;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
@@ -41,6 +46,13 @@ import java.util.stream.Stream;
|
||||
|
||||
public class CameraCapture extends SurfaceCapture {
|
||||
|
||||
public static final float[] VFLIP_MATRIX = {
|
||||
1, 0, 0, 0, // column 1
|
||||
0, -1, 0, 0, // column 2
|
||||
0, 0, 1, 0, // column 3
|
||||
0, 1, 0, 1, // column 4
|
||||
};
|
||||
|
||||
private final String explicitCameraId;
|
||||
private final CameraFacing cameraFacing;
|
||||
private final Size explicitSize;
|
||||
@@ -48,9 +60,16 @@ public class CameraCapture extends SurfaceCapture {
|
||||
private final CameraAspectRatio aspectRatio;
|
||||
private final int fps;
|
||||
private final boolean highSpeed;
|
||||
private final Rect crop;
|
||||
private final Orientation captureOrientation;
|
||||
private final float angle;
|
||||
|
||||
private String cameraId;
|
||||
private Size size;
|
||||
private Size captureSize;
|
||||
private Size videoSize; // after OpenGL transforms
|
||||
|
||||
private AffineMatrix transform;
|
||||
private OpenGLRunner glRunner;
|
||||
|
||||
private HandlerThread cameraThread;
|
||||
private Handler cameraHandler;
|
||||
@@ -67,6 +86,10 @@ public class CameraCapture extends SurfaceCapture {
|
||||
this.aspectRatio = options.getCameraAspectRatio();
|
||||
this.fps = options.getCameraFps();
|
||||
this.highSpeed = options.getCameraHighSpeed();
|
||||
this.crop = options.getCrop();
|
||||
this.captureOrientation = options.getCaptureOrientation();
|
||||
assert captureOrientation != null;
|
||||
this.angle = options.getAngle();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -92,13 +115,28 @@ public class CameraCapture extends SurfaceCapture {
|
||||
@Override
|
||||
public void prepare() throws IOException {
|
||||
try {
|
||||
size = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed);
|
||||
if (size == null) {
|
||||
captureSize = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed);
|
||||
if (captureSize == null) {
|
||||
throw new IOException("Could not select camera size");
|
||||
}
|
||||
} catch (CameraAccessException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
VideoFilter filter = new VideoFilter(captureSize);
|
||||
|
||||
if (crop != null) {
|
||||
filter.addCrop(crop, false);
|
||||
}
|
||||
|
||||
if (captureOrientation != Orientation.Orient0) {
|
||||
filter.addOrientation(captureOrientation);
|
||||
}
|
||||
|
||||
filter.addAngle(angle);
|
||||
|
||||
transform = filter.getInverseTransform();
|
||||
videoSize = filter.getOutputSize().limit(maxSize).round8();
|
||||
}
|
||||
|
||||
private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException, ConfigurationException {
|
||||
@@ -214,6 +252,15 @@ public class CameraCapture extends SurfaceCapture {
|
||||
|
||||
@Override
|
||||
public void start(Surface surface) throws IOException {
|
||||
if (transform != null) {
|
||||
assert glRunner == null;
|
||||
OpenGLFilter glFilter = new AffineOpenGLFilter(transform);
|
||||
// The transform matrix returned by SurfaceTexture is incorrect for camera capture (it often contains an additional unexpected 90°
|
||||
// rotation). Use a vertical flip transform matrix instead.
|
||||
glRunner = new OpenGLRunner(glFilter, VFLIP_MATRIX);
|
||||
surface = glRunner.start(captureSize, videoSize, surface);
|
||||
}
|
||||
|
||||
try {
|
||||
CameraCaptureSession session = createCaptureSession(cameraDevice, surface);
|
||||
CaptureRequest request = createCaptureRequest(surface);
|
||||
@@ -235,7 +282,7 @@ public class CameraCapture extends SurfaceCapture {
|
||||
|
||||
@Override
|
||||
public Size getSize() {
|
||||
return size;
|
||||
return videoSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
package com.genymobile.scrcpy.video;
|
||||
|
||||
import com.genymobile.scrcpy.AndroidVersions;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.DisplayInfo;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.wrappers.DisplayManager;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.SystemClock;
|
||||
import android.view.IDisplayFoldListener;
|
||||
import android.view.IRotationWatcher;
|
||||
|
||||
public class DisplaySizeMonitor {
|
||||
|
||||
public interface Listener {
|
||||
void onDisplaySizeChanged();
|
||||
}
|
||||
|
||||
private DisplayManager.DisplayListenerHandle displayListenerHandle;
|
||||
private HandlerThread handlerThread;
|
||||
|
||||
// On Android 14, the DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really
|
||||
// detect it directly, so register a RotationWatcher and a DisplayFoldListener as a fallback, until we receive the first event from
|
||||
// DisplayListener (which proves that it works).
|
||||
private boolean displayListenerWorks; // only accessed from the display listener thread
|
||||
private boolean fallbacksUnregistered; // a register call after an unregister must be ignored (protected by this)
|
||||
private IRotationWatcher rotationWatcher;
|
||||
private IDisplayFoldListener displayFoldListener;
|
||||
|
||||
private int displayId = Device.DISPLAY_ID_NONE;
|
||||
|
||||
private Size sessionDisplaySize;
|
||||
|
||||
private Listener listener;
|
||||
|
||||
public void start(int displayId, Listener listener) {
|
||||
start(displayId, listener, true, true, false);
|
||||
}
|
||||
|
||||
public void start(int displayId, Listener listener, boolean useRotationWatcherFallback, boolean useFoldListenerFallback,
|
||||
boolean delayRotationWatcherFallback) {
|
||||
// Once started, the listener and the displayId must never change
|
||||
assert listener != null;
|
||||
this.listener = listener;
|
||||
|
||||
assert this.displayId == Device.DISPLAY_ID_NONE;
|
||||
this.displayId = displayId;
|
||||
|
||||
handlerThread = new HandlerThread("DisplayListener");
|
||||
handlerThread.start();
|
||||
Handler handler = new Handler(handlerThread.getLooper());
|
||||
|
||||
displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(this::handleDisplayChanged, handler);
|
||||
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
|
||||
registerDisplayListenerFallbacks(useRotationWatcherFallback, useFoldListenerFallback, delayRotationWatcherFallback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop and release the monitor.
|
||||
* <p/>
|
||||
* It must not be used anymore.
|
||||
* It is ok to call this method even if {@link #start(int, Listener)} was not called.
|
||||
*/
|
||||
public void stopAndRelease() {
|
||||
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
|
||||
unregisterDisplayListenerFallbacks();
|
||||
}
|
||||
|
||||
// displayListenerHandle may be null if registration failed
|
||||
if (displayListenerHandle != null) {
|
||||
ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle);
|
||||
displayListenerHandle = null;
|
||||
}
|
||||
|
||||
if (handlerThread != null) {
|
||||
handlerThread.quitSafely();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized Size getSessionDisplaySize() {
|
||||
return sessionDisplaySize;
|
||||
}
|
||||
|
||||
public synchronized void setSessionDisplaySize(Size sessionDisplaySize) {
|
||||
this.sessionDisplaySize = sessionDisplaySize;
|
||||
}
|
||||
|
||||
private void handleDisplayChanged(int displayId) {
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("DisplaySizeMonitor: onDisplayChanged(" + displayId + ")");
|
||||
}
|
||||
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
|
||||
if (!displayListenerWorks) {
|
||||
// On the first display listener event, we know it works, we can unregister the fallbacks
|
||||
displayListenerWorks = true;
|
||||
unregisterDisplayListenerFallbacks();
|
||||
}
|
||||
}
|
||||
if (this.displayId == displayId) {
|
||||
DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
|
||||
if (di == null) {
|
||||
Ln.w("DisplayInfo for " + displayId + " cannot be retrieved");
|
||||
// We can't compare with the current size, so reset unconditionally
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("DisplaySizeMonitor: requestReset(): " + getSessionDisplaySize() + " -> (unknown)");
|
||||
}
|
||||
setSessionDisplaySize(null);
|
||||
listener.onDisplaySizeChanged();
|
||||
} else {
|
||||
Size size = di.getSize();
|
||||
|
||||
// The field is hidden on purpose, to read it with synchronization
|
||||
@SuppressWarnings("checkstyle:HiddenField")
|
||||
Size sessionDisplaySize = getSessionDisplaySize(); // synchronized
|
||||
|
||||
// .equals() also works if sessionDisplaySize == null
|
||||
if (!size.equals(sessionDisplaySize)) {
|
||||
// Reset only if the size is different
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("DisplaySizeMonitor: requestReset(): " + sessionDisplaySize + " -> " + size);
|
||||
}
|
||||
// Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare()
|
||||
// considers that the current size is the requested size (to avoid a duplicate requestReset())
|
||||
setSessionDisplaySize(size);
|
||||
listener.onDisplaySizeChanged();
|
||||
} else if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("DisplaySizeMonitor: Size not changed (" + size + "): do not requestReset()");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void registerDisplayListenerFallbacks(boolean useRotationWatcherFallback, boolean useFoldListenerFallback,
|
||||
boolean delayRotationWatcherFallback) {
|
||||
if (fallbacksUnregistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (useRotationWatcherFallback) {
|
||||
if (delayRotationWatcherFallback) {
|
||||
// Hack: If the virtual display was just created, registering a rotation watcher immediately fails with an error because the
|
||||
// display id does not exist yet.
|
||||
SystemClock.sleep(50);
|
||||
}
|
||||
rotationWatcher = new IRotationWatcher.Stub() {
|
||||
@Override
|
||||
public void onRotationChanged(int rotation) {
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("DisplaySizeMonitor: onRotationChanged(" + rotation + ")");
|
||||
}
|
||||
|
||||
handleDisplayChanged(displayId);
|
||||
}
|
||||
};
|
||||
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
|
||||
}
|
||||
|
||||
if (useFoldListenerFallback) {
|
||||
// Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14)
|
||||
displayFoldListener = new IDisplayFoldListener.Stub() {
|
||||
|
||||
private boolean first = true;
|
||||
|
||||
@Override
|
||||
public void onDisplayFoldChanged(int displayId, boolean folded) {
|
||||
if (first) {
|
||||
// An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding.
|
||||
first = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("DisplaySizeMonitor: onDisplayFoldChanged(" + displayId + ", " + folded + ")");
|
||||
}
|
||||
|
||||
if (DisplaySizeMonitor.this.displayId != displayId) {
|
||||
// Ignore events related to other display ids
|
||||
return;
|
||||
}
|
||||
|
||||
handleDisplayChanged(displayId);
|
||||
}
|
||||
};
|
||||
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void unregisterDisplayListenerFallbacks() {
|
||||
fallbacksUnregistered = true;
|
||||
|
||||
if (rotationWatcher != null) {
|
||||
ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher);
|
||||
rotationWatcher = null;
|
||||
}
|
||||
if (displayFoldListener != null) {
|
||||
// Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14)
|
||||
ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener);
|
||||
displayFoldListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,16 @@ import com.genymobile.scrcpy.Options;
|
||||
import com.genymobile.scrcpy.control.PositionMapper;
|
||||
import com.genymobile.scrcpy.device.DisplayInfo;
|
||||
import com.genymobile.scrcpy.device.NewDisplay;
|
||||
import com.genymobile.scrcpy.device.Orientation;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.opengl.AffineOpenGLFilter;
|
||||
import com.genymobile.scrcpy.opengl.OpenGLFilter;
|
||||
import com.genymobile.scrcpy.opengl.OpenGLRunner;
|
||||
import com.genymobile.scrcpy.util.AffineMatrix;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.os.Build;
|
||||
import android.view.Surface;
|
||||
@@ -19,8 +24,8 @@ import java.io.IOException;
|
||||
public class NewDisplayCapture extends SurfaceCapture {
|
||||
|
||||
// Internal fields copied from android.hardware.display.DisplayManager
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8;
|
||||
@@ -35,12 +40,26 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
private final VirtualDisplayListener vdListener;
|
||||
private final NewDisplay newDisplay;
|
||||
|
||||
private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor();
|
||||
|
||||
private AffineMatrix displayTransform;
|
||||
private AffineMatrix eventTransform;
|
||||
private OpenGLRunner glRunner;
|
||||
|
||||
private Size mainDisplaySize;
|
||||
private int mainDisplayDpi;
|
||||
private int maxSize; // only used if newDisplay.getSize() != null
|
||||
private final Rect crop;
|
||||
private final boolean captureOrientationLocked;
|
||||
private final Orientation captureOrientation;
|
||||
private final float angle;
|
||||
private final boolean vdSystemDecorations;
|
||||
|
||||
private VirtualDisplay virtualDisplay;
|
||||
private Size size;
|
||||
private Size videoSize;
|
||||
private Size displaySize; // the logical size of the display (including rotation)
|
||||
private Size physicalSize; // the physical size of the display (without rotation)
|
||||
|
||||
private int dpi;
|
||||
|
||||
public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) {
|
||||
@@ -48,16 +67,26 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
this.newDisplay = options.getNewDisplay();
|
||||
assert newDisplay != null;
|
||||
this.maxSize = options.getMaxSize();
|
||||
this.crop = options.getCrop();
|
||||
assert options.getCaptureOrientationLock() != null;
|
||||
this.captureOrientationLocked = options.getCaptureOrientationLock() != Orientation.Lock.Unlocked;
|
||||
this.captureOrientation = options.getCaptureOrientation();
|
||||
assert captureOrientation != null;
|
||||
this.angle = options.getAngle();
|
||||
this.vdSystemDecorations = options.getVDSystemDecorations();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
size = newDisplay.getSize();
|
||||
displaySize = newDisplay.getSize();
|
||||
dpi = newDisplay.getDpi();
|
||||
if (size == null || dpi == 0) {
|
||||
if (displaySize == null || dpi == 0) {
|
||||
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(0);
|
||||
if (displayInfo != null) {
|
||||
mainDisplaySize = displayInfo.getSize();
|
||||
if ((displayInfo.getRotation() % 2) != 0) {
|
||||
mainDisplaySize = mainDisplaySize.rotate(); // Use the natural device orientation (at rotation 0), not the current one
|
||||
}
|
||||
mainDisplayDpi = displayInfo.getDpi();
|
||||
} else {
|
||||
Ln.w("Main display not found, fallback to 1920x1080 240dpi");
|
||||
@@ -69,12 +98,59 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
|
||||
@Override
|
||||
public void prepare() {
|
||||
if (!newDisplay.hasExplicitSize()) {
|
||||
size = mainDisplaySize.limit(maxSize).round8();
|
||||
int displayRotation;
|
||||
if (virtualDisplay == null) {
|
||||
if (!newDisplay.hasExplicitSize()) {
|
||||
displaySize = mainDisplaySize.limit(maxSize).round8();
|
||||
}
|
||||
if (!newDisplay.hasExplicitDpi()) {
|
||||
dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, displaySize);
|
||||
}
|
||||
|
||||
videoSize = displaySize;
|
||||
displayRotation = 0;
|
||||
// Set the current display size to avoid an unnecessary call to invalidate()
|
||||
displaySizeMonitor.setSessionDisplaySize(displaySize);
|
||||
} else {
|
||||
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(virtualDisplay.getDisplay().getDisplayId());
|
||||
displaySize = displayInfo.getSize();
|
||||
dpi = displayInfo.getDpi();
|
||||
displayRotation = displayInfo.getRotation();
|
||||
}
|
||||
if (!newDisplay.hasExplicitDpi()) {
|
||||
dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, size);
|
||||
|
||||
VideoFilter filter = new VideoFilter(displaySize);
|
||||
|
||||
if (crop != null) {
|
||||
boolean transposed = (displayRotation % 2) != 0;
|
||||
filter.addCrop(crop, transposed);
|
||||
}
|
||||
|
||||
filter.addOrientation(displayRotation, captureOrientationLocked, captureOrientation);
|
||||
filter.addAngle(angle);
|
||||
|
||||
eventTransform = filter.getInverseTransform();
|
||||
|
||||
// The display info gives the oriented size (so videoSize includes the display rotation)
|
||||
videoSize = filter.getOutputSize().limit(maxSize).round8();
|
||||
|
||||
// But the virtual display video always remains in the origin orientation (the video itself is not rotated, so it must rotated manually).
|
||||
// This additional display rotation must not be included in the input events transform (the expected coordinates are already in the
|
||||
// physical display size)
|
||||
if ((displayRotation % 2) == 0) {
|
||||
physicalSize = displaySize;
|
||||
} else {
|
||||
physicalSize = displaySize.rotate();
|
||||
}
|
||||
VideoFilter displayFilter = new VideoFilter(physicalSize);
|
||||
displayFilter.addRotation(displayRotation);
|
||||
// The display info gives the oriented size, but the virtual display video always remains in the origin orientation
|
||||
AffineMatrix displayRotationMatrix = displayFilter.getInverseTransform();
|
||||
|
||||
// The display rotation should be the first filter:
|
||||
// DISPLAY_TRANSFORM = (FILTER_MATRIX * DISPLAY_FILTER_MATRIX)⁻¹
|
||||
// = DISPLAY_FILTER_MATRIX⁻¹ * FILTER_MATRIX⁻¹
|
||||
// = displayRotationMatrix * eventTransform
|
||||
displayTransform = AffineMatrix.multiplyAll(displayRotationMatrix, eventTransform);
|
||||
}
|
||||
|
||||
public void startNew(Surface surface) {
|
||||
@@ -84,8 +160,10 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
| VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
|
||||
| VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH
|
||||
| VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT
|
||||
| VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL
|
||||
| VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
|
||||
| VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL;
|
||||
if (vdSystemDecorations) {
|
||||
flags |= VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_33_ANDROID_13) {
|
||||
flags |= VIRTUAL_DISPLAY_FLAG_TRUSTED
|
||||
| VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP
|
||||
@@ -97,31 +175,51 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
}
|
||||
}
|
||||
virtualDisplay = ServiceManager.getDisplayManager()
|
||||
.createNewVirtualDisplay("scrcpy", size.getWidth(), size.getHeight(), dpi, surface, flags);
|
||||
.createNewVirtualDisplay("scrcpy", displaySize.getWidth(), displaySize.getHeight(), dpi, surface, flags);
|
||||
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
|
||||
Ln.i("New display: " + size.getWidth() + "x" + size.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")");
|
||||
Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")");
|
||||
|
||||
// Disable DisplayFoldListener fallback, and delay registering the RotationWatcher as a workaround
|
||||
displaySizeMonitor.start(virtualDisplayId, this::invalidate, true, false, true);
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not create display", e);
|
||||
throw new AssertionError("Could not create display");
|
||||
}
|
||||
|
||||
if (vdListener != null) {
|
||||
PositionMapper positionMapper = new PositionMapper(size, null);
|
||||
vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Surface surface) throws IOException {
|
||||
if (displayTransform != null) {
|
||||
assert glRunner == null;
|
||||
OpenGLFilter glFilter = new AffineOpenGLFilter(displayTransform);
|
||||
glRunner = new OpenGLRunner(glFilter);
|
||||
surface = glRunner.start(physicalSize, videoSize, surface);
|
||||
}
|
||||
|
||||
if (virtualDisplay == null) {
|
||||
startNew(surface);
|
||||
} else {
|
||||
virtualDisplay.setSurface(surface);
|
||||
}
|
||||
|
||||
if (vdListener != null) {
|
||||
PositionMapper positionMapper = PositionMapper.create(videoSize, eventTransform, displaySize);
|
||||
vdListener.onNewVirtualDisplay(virtualDisplay.getDisplay().getDisplayId(), positionMapper);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (glRunner != null) {
|
||||
glRunner.stopAndRelease();
|
||||
glRunner = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
displaySizeMonitor.stopAndRelease();
|
||||
|
||||
if (virtualDisplay != null) {
|
||||
virtualDisplay.release();
|
||||
virtualDisplay = null;
|
||||
@@ -130,7 +228,7 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
|
||||
@Override
|
||||
public synchronized Size getSize() {
|
||||
return size;
|
||||
return videoSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.genymobile.scrcpy.control.PositionMapper;
|
||||
import com.genymobile.scrcpy.device.ConfigurationException;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.DisplayInfo;
|
||||
import com.genymobile.scrcpy.device.Orientation;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.opengl.AffineOpenGLFilter;
|
||||
import com.genymobile.scrcpy.opengl.OpenGLFilter;
|
||||
@@ -13,18 +14,13 @@ import com.genymobile.scrcpy.opengl.OpenGLRunner;
|
||||
import com.genymobile.scrcpy.util.AffineMatrix;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
import com.genymobile.scrcpy.wrappers.DisplayManager;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.view.IDisplayFoldListener;
|
||||
import android.view.IRotationWatcher;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -35,13 +31,14 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
private final int displayId;
|
||||
private int maxSize;
|
||||
private final Rect crop;
|
||||
private int lockVideoOrientation;
|
||||
private Orientation.Lock captureOrientationLock;
|
||||
private Orientation captureOrientation;
|
||||
private final float angle;
|
||||
|
||||
private DisplayInfo displayInfo;
|
||||
private Size videoSize;
|
||||
|
||||
// Source display size (before resizing/crop) for the current session
|
||||
private Size sessionDisplaySize;
|
||||
private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor();
|
||||
|
||||
private IBinder display;
|
||||
private VirtualDisplay virtualDisplay;
|
||||
@@ -49,78 +46,22 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
private AffineMatrix transform;
|
||||
private OpenGLRunner glRunner;
|
||||
|
||||
private DisplayManager.DisplayListenerHandle displayListenerHandle;
|
||||
private HandlerThread handlerThread;
|
||||
|
||||
// On Android 14, the DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really
|
||||
// detect it directly, so register a RotationWatcher and a DisplayFoldListener as a fallback, until we receive the first event from
|
||||
// DisplayListener (which proves that it works).
|
||||
private boolean displayListenerWorks; // only accessed from the display listener thread
|
||||
private IRotationWatcher rotationWatcher;
|
||||
private IDisplayFoldListener displayFoldListener;
|
||||
|
||||
public ScreenCapture(VirtualDisplayListener vdListener, Options options) {
|
||||
this.vdListener = vdListener;
|
||||
this.displayId = options.getDisplayId();
|
||||
assert displayId != Device.DISPLAY_ID_NONE;
|
||||
this.maxSize = options.getMaxSize();
|
||||
this.crop = options.getCrop();
|
||||
this.lockVideoOrientation = options.getLockVideoOrientation();
|
||||
this.captureOrientationLock = options.getCaptureOrientationLock();
|
||||
this.captureOrientation = options.getCaptureOrientation();
|
||||
assert captureOrientationLock != null;
|
||||
assert captureOrientation != null;
|
||||
this.angle = options.getAngle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
|
||||
registerDisplayListenerFallbacks();
|
||||
}
|
||||
|
||||
handlerThread = new HandlerThread("DisplayListener");
|
||||
handlerThread.start();
|
||||
Handler handler = new Handler(handlerThread.getLooper());
|
||||
displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(displayId -> {
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("ScreenCapture: onDisplayChanged(" + displayId + ")");
|
||||
}
|
||||
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
|
||||
if (!displayListenerWorks) {
|
||||
// On the first display listener event, we know it works, we can unregister the fallbacks
|
||||
displayListenerWorks = true;
|
||||
unregisterDisplayListenerFallbacks();
|
||||
}
|
||||
}
|
||||
if (this.displayId == displayId) {
|
||||
DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
|
||||
if (di == null) {
|
||||
Ln.w("DisplayInfo for " + displayId + " cannot be retrieved");
|
||||
// We can't compare with the current size, so reset unconditionally
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("ScreenCapture: requestReset(): " + getSessionDisplaySize() + " -> (unknown)");
|
||||
}
|
||||
setSessionDisplaySize(null);
|
||||
invalidate();
|
||||
} else {
|
||||
Size size = di.getSize();
|
||||
|
||||
// The field is hidden on purpose, to read it with synchronization
|
||||
@SuppressWarnings("checkstyle:HiddenField")
|
||||
Size sessionDisplaySize = getSessionDisplaySize(); // synchronized
|
||||
|
||||
// .equals() also works if sessionDisplaySize == null
|
||||
if (!size.equals(sessionDisplaySize)) {
|
||||
// Reset only if the size is different
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("ScreenCapture: requestReset(): " + sessionDisplaySize + " -> " + size);
|
||||
}
|
||||
// Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare()
|
||||
// considers that the current size is the requested size (to avoid a duplicate requestReset())
|
||||
setSessionDisplaySize(size);
|
||||
invalidate();
|
||||
} else if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("ScreenCapture: Size not changed (" + size + "): do not requestReset()");
|
||||
}
|
||||
}
|
||||
}
|
||||
}, handler);
|
||||
displaySizeMonitor.start(displayId, this::invalidate);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -136,11 +77,12 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
}
|
||||
|
||||
Size displaySize = displayInfo.getSize();
|
||||
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
|
||||
lockVideoOrientation = displayInfo.getRotation();
|
||||
captureOrientationLock = Orientation.Lock.LockedValue;
|
||||
captureOrientation = Orientation.fromRotation(displayInfo.getRotation());
|
||||
}
|
||||
|
||||
VideoFilter filter = new VideoFilter(displaySize);
|
||||
@@ -150,9 +92,9 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
filter.addCrop(crop, transposed);
|
||||
}
|
||||
|
||||
if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_UNLOCKED) {
|
||||
filter.addLockVideoOrientation(lockVideoOrientation, displayInfo.getRotation());
|
||||
}
|
||||
boolean locked = captureOrientationLock != Orientation.Lock.Unlocked;
|
||||
filter.addOrientation(displayInfo.getRotation(), locked, captureOrientation);
|
||||
filter.addAngle(angle);
|
||||
|
||||
transform = filter.getInverseTransform();
|
||||
videoSize = filter.getOutputSize().limit(maxSize).round8();
|
||||
@@ -226,18 +168,7 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
|
||||
unregisterDisplayListenerFallbacks();
|
||||
}
|
||||
|
||||
handlerThread.quitSafely();
|
||||
handlerThread = null;
|
||||
|
||||
// displayListenerHandle may be null if registration failed
|
||||
if (displayListenerHandle != null) {
|
||||
ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle);
|
||||
displayListenerHandle = null;
|
||||
}
|
||||
displaySizeMonitor.stopAndRelease();
|
||||
|
||||
if (display != null) {
|
||||
SurfaceControl.destroyDisplay(display);
|
||||
@@ -279,67 +210,6 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized Size getSessionDisplaySize() {
|
||||
return sessionDisplaySize;
|
||||
}
|
||||
|
||||
private synchronized void setSessionDisplaySize(Size sessionDisplaySize) {
|
||||
this.sessionDisplaySize = sessionDisplaySize;
|
||||
}
|
||||
|
||||
private void registerDisplayListenerFallbacks() {
|
||||
rotationWatcher = new IRotationWatcher.Stub() {
|
||||
@Override
|
||||
public void onRotationChanged(int rotation) {
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")");
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
};
|
||||
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
|
||||
|
||||
// Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14)
|
||||
displayFoldListener = new IDisplayFoldListener.Stub() {
|
||||
|
||||
private boolean first = true;
|
||||
|
||||
@Override
|
||||
public void onDisplayFoldChanged(int displayId, boolean folded) {
|
||||
if (first) {
|
||||
// An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding.
|
||||
first = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||
Ln.v("ScreenCapture: onDisplayFoldChanged(" + displayId + ", " + folded + ")");
|
||||
}
|
||||
|
||||
if (ScreenCapture.this.displayId != displayId) {
|
||||
// Ignore events related to other display ids
|
||||
return;
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
};
|
||||
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
|
||||
}
|
||||
|
||||
private void unregisterDisplayListenerFallbacks() {
|
||||
synchronized (this) {
|
||||
if (rotationWatcher != null) {
|
||||
ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher);
|
||||
rotationWatcher = null;
|
||||
}
|
||||
if (displayFoldListener != null) {
|
||||
// Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14)
|
||||
ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener);
|
||||
displayFoldListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestInvalidate() {
|
||||
invalidate();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.genymobile.scrcpy.video;
|
||||
|
||||
import com.genymobile.scrcpy.device.Orientation;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.AffineMatrix;
|
||||
|
||||
@@ -72,9 +73,28 @@ public class VideoFilter {
|
||||
size = size.rotate();
|
||||
}
|
||||
}
|
||||
|
||||
public void addLockVideoOrientation(int lockVideoOrientation, int displayRotation) {
|
||||
int ccwRotation = (4 + lockVideoOrientation - displayRotation) % 4;
|
||||
public void addOrientation(Orientation captureOrientation) {
|
||||
if (captureOrientation.isFlipped()) {
|
||||
transform = AffineMatrix.hflip().multiply(transform);
|
||||
}
|
||||
int ccwRotation = (4 - captureOrientation.getRotation()) % 4;
|
||||
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);
|
||||
}
|
||||
|
||||
public void addAngle(double cwAngle) {
|
||||
if (cwAngle == 0) {
|
||||
return;
|
||||
}
|
||||
double ccwAngle = -cwAngle;
|
||||
transform = AffineMatrix.rotate(ccwAngle).withAspectRatio(size).fromCenter().multiply(transform);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user