Compare commits

..

23 Commits

Author SHA1 Message Date
Romain Vimont
3c7a6d363e Disable default stdout/stderr for Xiaomi devices
Xiaomi device ROMs print internal errors using e.printStackTRace(),
flooding the console with irrelevant errors.

To get rid of them, on Xiaomi devices, trash the messages printed to the
console by direct calls to System.out and System.err.

Refs #994 <https://github.com/Genymobile/scrcpy/issues/994>
Refs #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-11-01 00:35:42 +01:00
Romain Vimont
c64d150202 Improve manpage formatting 2023-10-31 19:20:59 +01:00
Romain Vimont
8350a61926 Simplify URLs in manpage
The .UR-formatted URLs are not always rendered correctly. Use simple
brackets instead.
2023-10-31 19:19:34 +01:00
Romain Vimont
5580803406 Always print device model and version
Print the log before checking for --list-* options so that it is
printed in all cases.
2023-10-31 19:19:33 +01:00
Romain Vimont
9bfc749803 Add camera documentation
PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 15:57:06 +01:00
Andrew Gunnerson
6af4bd601f Add support for high frame rate camera capture
Add --camera-high-speed to enable high frame rate camera capture. If
the option is enabled, then --camera-fps is mandatory.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-31 15:57:06 +01:00
Andrew Gunnerson
4722bff423 Add --camera-fps
Add a new option for specifying the camera frame rate.

By default, Android's default frame rate (30 fps) is used.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-31 15:57:06 +01:00
Romain Vimont
928f8b8eb3 Do not arbitrary limit --max-fps to 1000
Limit to the variable type size, for consistency.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 15:57:06 +01:00
Simon Chan
9fc5835485 Fail-fast camera mirroring on Android 11 and older
PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-31 15:57:06 +01:00
Romain Vimont
dd36d6135f Support camera size selection using -m/--camera-ar
In addition to --camera-size to specify an explicit size, make it
possible to select the camera size automatically, respecting the maximum
size (already used for display mirroring) and an aspect ratio.

For example, "scrcpy --video-source=camera" followed by:
 - (no additional arguments)
    : mirrors at the maximum size, any a-r
 - -m1920
    : only consider valid sizes having both dimensions not above 1920
 - --camera-ar=4:3
    : only consider valid sizes having an aspect ratio of 4:3 (+/- 10%)
 - -m2048 --camera-ar=1.6
    : only consider valid sizes having both dimensions not above 2048
      and an aspect ratio of 1.6 (+/- 10%)

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2023-10-31 15:57:06 +01:00
Simon Chan
faebb7d70a Add --camera-facing
Add an option to select the camera by its lens facing (front, back or
external).

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-31 15:57:06 +01:00
Romain Vimont
7f8d079c8c Make camera id optional
If no camera id is provided, use the first camera available.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 15:57:06 +01:00
Romain Vimont
64930e71b9 Handle camera disconnection
Stop mirroring on camera disconnection.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 15:57:06 +01:00
Romain Vimont
d544e577c0 Automatically select audio source
If --audio-source is not specified, select the default value
according to the video source:
 - for display mirroring, use device audio by default;
 - for camera mirroring, use microphone by default.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 15:57:06 +01:00
Simon Chan
bfeecc0131 Add camera mirroring
Add --video-source=camera, and related options:
 - --camera-id=<id>: select the camera by its id (see --list-cameras);
 - --camera-size=<width>x<height>: select the capture size.

Fixed #241 <https://github.com/Genymobile/scrcpy/issues/241>
PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-31 15:57:06 +01:00
Romain Vimont
f032262cd7 Add --list-camera-sizes
Add an option to list the device camera declared sizes.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 15:57:06 +01:00
Simon Chan
cd63896d63 Add --list-cameras
Add an option to list the device cameras.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-31 15:56:25 +01:00
Romain Vimont
f085765e04 Factorize --list- options handling
This will limit code duplication as more list options will be added.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 12:45:40 +01:00
Simon Chan
a2fb1b40f6 Extract SurfaceCapture from ScreenEncoder
Extract an interface SurfaceCapture from ScreenEncoder, representing a
video source which can be rendered to a Surface for encoding.

Split ScreenEncoder into:
 - ScreenCapture, implementing SurfaceCapture to capture the device
   screen,
 - SurfaceEncoder, to encode any SurfaceCapture.

This separation prepares the introduction of another SurfaceCapture
implementation to capture the camera instead of the device screen.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-31 12:45:40 +01:00
Romain Vimont
41ccb5883e Force server exit at the end of main()
By default, the Java process exits when all non-daemon threads are
terminated.

The Android SDK might start some non-daemon threads internally,
preventing the scrcpy server to exit in some cases.

So force the process to exit explicitly.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 12:45:40 +01:00
Romain Vimont
23e116064d Rename --display to --display-id
The option is named "display id" everywhere.

This will be consistent with --camera-id (there will be many camera
options, so an option --camera would be confusing).

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 12:45:40 +01:00
Romain Vimont
3432029a3d Make separator configurable for parsing integers
The separator was hardcoded to ':'. This will allow to reuse the
function to parse sizes as WIDTHxHEIGHT.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 12:45:31 +01:00
Romain Vimont
7a2b756f1e Fix incorrect comment about AV1 constant
MediaFormat.MIMETYPE_VIDEO_AV1 has been added in API 29, not 21.
2023-10-31 12:35:04 +01:00
22 changed files with 488 additions and 85 deletions

View File

@@ -25,12 +25,13 @@ It focuses on:
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
Its features include:
- [audio forwarding](doc/audio.md) (Android >= 11)
- [audio forwarding](doc/audio.md) (Android 11+)
- [recording](doc/recording.md)
- mirroring with [Android device screen off](doc/device.md#turn-screen-off)
- [copy-paste](doc/control.md#copy-paste) in both directions
- [configurable quality](doc/video.md)
- Android device screen [as a webcam (V4L2)](doc/v4l2.md) (Linux-only)
- [camera mirroring](doc/camera.md) (Android 12+)
- [mirroring as a webcam (V4L2)](doc/v4l2.md) (Linux-only)
- [physical keyboard/mouse simulation (HID)](doc/hid-otg.md)
- [OTG mode](doc/hid-otg.md#otg)
- and more…
@@ -77,6 +78,7 @@ documented in the following pages:
- [Recording](doc/recording.md)
- [Tunnels](doc/tunnels.md)
- [HID/OTG](doc/hid-otg.md)
- [Camera](doc/camera.md)
- [Video4Linux](doc/v4l2.md)
- [Shortcuts](doc/shortcuts.md)

View File

@@ -13,6 +13,8 @@ _scrcpy() {
--camera-ar=
--camera-id=
--camera-facing=
--camera-fps=
--camera-high-speed
--camera-size=
--crop=
-d --select-usb
@@ -156,6 +158,7 @@ _scrcpy() {
|--audio-output-buffer \
|--camera-ar \
|--camera-id \
|--camera-fps \
|--camera-size \
|--crop \
|--display-id \

View File

@@ -18,8 +18,10 @@ arguments=(
'--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]'
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
'--camera-ar=[Select the camera size by its aspect ratio]'
'--camera-high-speed=[Enable high-speed camera capture mode]'
'--camera-id=[Specify the camera id to mirror]'
'--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]'
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
{-d,--select-usb}'[Use USB device]'

View File

@@ -45,15 +45,15 @@ Set a list of comma-separated key:type=value options for the device audio encode
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
The list of possible codec options is available in the Android documentation
.UR https://d.android.com/reference/android/media/MediaFormat
.UE .
The list of possible codec options is available in the Android documentation:
<https://d.android.com/reference/android/media/MediaFormat>
.TP
.BI "\-\-audio\-encoder " name
Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR).
The available encoders can be listed by \-\-list\-encoders.
The available encoders can be listed by \fB\-\-list\-encoders\fR.
.TP
.BI "\-\-audio\-source " source
@@ -79,13 +79,19 @@ Default is 8M (8000000).
.BI "\-\-camera\-ar " ar
Select the camera size by its aspect ratio (+/- 10%).
Possible values are "sensor" (use the camera sensor aspect ratio), "<num>:<den>" (e.g. "4:3") and "<value>" (e.g. "1.6").
Possible values are "sensor" (use the camera sensor aspect ratio), "\fInum\fR:\fIden\fR" (e.g. "4:3") and "\fIvalue\fR" (e.g. "1.6").
.TP
.B \-\-camera\-high\-speed
Enable high-speed camera capture mode.
This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR.
.TP
.BI "\-\-camera\-id " id
Specify the device camera id to mirror.
The available camera ids can be listed by \-\-list\-cameras.
The available camera ids can be listed by \fB\-\-list\-cameras\fR.
.TP
.BI "\-\-camera\-facing " facing
@@ -93,6 +99,12 @@ Select the device camera by its facing direction.
Possible values are "front", "back" and "external".
.TP
.BI "\-\-camera\-fps " fps
Specify the camera capture frame rate.
If not specified, Android's default frame rate (30 fps) is used.
.TP
.BI "\-\-camera\-size " width\fRx\fIheight
Specify an explicit camera capture size.
@@ -119,7 +131,7 @@ Disable screensaver while scrcpy is running.
.BI "\-\-display\-id " id
Specify the device display id to mirror.
The available display ids can be listed by \-\-list\-displays.
The available display ids can be listed by \fB\-\-list\-displays\fR.
Default is 0.
@@ -177,9 +189,11 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
.TP
.B \-\-list\-camera\-sizes
List the valid camera capture sizes.
.TP
.B \-\-list\-cameras
List cameras available on the device.
@@ -227,7 +241,7 @@ Disable device control (mirror the device in read\-only).
.TP
.B \-N, \-\-no\-playback
Disable video and audio playback on the computer (equivalent to --no-video-playback --no-audio-playback).
Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR).
.TP
.B \-\-no\-audio
@@ -349,8 +363,7 @@ Request SDL to use the given render driver (this is just a hint).
Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software".
.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER
.UE
<https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER>
.TP
.B \-\-require\-audio
@@ -398,13 +411,13 @@ Set the maximum mirroring time, in seconds.
.TP
.BI "\-\-tunnel\-host " ip
Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward.
Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR.
Default is localhost.
.TP
.BI "\-\-tunnel\-port " port
Set the TCP port of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward.
Set the TCP port of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR.
Default is 0 (not forced): the local port used for establishing the tunnel will be used.
@@ -444,15 +457,15 @@ Set a list of comma-separated key:type=value options for the device video encode
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
The list of possible codec options is available in the Android documentation
.UR https://d.android.com/reference/android/media/MediaFormat
.UE .
The list of possible codec options is available in the Android documentation:
<https://d.android.com/reference/android/media/MediaFormat>
.TP
.BI "\-\-video\-encoder " name
Use a specific MediaCodec video encoder (depending on the codec provided by \fB\-\-video\-codec\fR).
The available encoders can be listed by \-\-list\-encoders.
The available encoders can be listed by \fB\-\-list\-encoders\fR.
.TP
.BI "\-\-video\-source " source
@@ -622,7 +635,7 @@ Path to adb.
.TP
.B ANDROID_SERIAL
Device serial to use if no selector (-s, -d, -e or --tcpip=<addr>) is specified.
Device serial to use if no selector (\fB-s\fR, \fB-d\fR, \fB-e\fR or \fB\-\-tcpip=\fIaddr\fR) is specified.
.TP
.B SCRCPY_ICON_PATH
@@ -645,23 +658,14 @@ for the Debian Project (and may be used by others).
.SH "REPORTING BUGS"
Report bugs to
.UR https://github.com/Genymobile/scrcpy/issues
.UE .
Report bugs to <https://github.com/Genymobile/scrcpy/issues>.
.SH COPYRIGHT
Copyright \(co 2018 Genymobile
.UR https://www.genymobile.com
Genymobile
.UE
Copyright \(co 2018 Genymobile <https://www.genymobile.com>
Copyright \(co 2018\-2023
.MT rom@rom1v.com
Romain Vimont
.ME
Copyright \(co 2018\-2023 Romain Vimont <rom@rom1v.com>
Licensed under the Apache License, Version 2.0.
.SH WWW
.UR https://github.com/Genymobile/scrcpy
.UE
<https://github.com/Genymobile/scrcpy>

View File

@@ -88,6 +88,8 @@ enum {
OPT_CAMERA_SIZE,
OPT_CAMERA_FACING,
OPT_CAMERA_AR,
OPT_CAMERA_FPS,
OPT_CAMERA_HIGH_SPEED,
};
struct sc_option {
@@ -228,12 +230,27 @@ static const struct sc_option options[] = {
.text = "Select the device camera by its facing direction.\n"
"Possible values are \"front\", \"back\" and \"external\".",
},
{
.longopt_id = OPT_CAMERA_HIGH_SPEED,
.longopt = "camera-high-speed",
.text = "Enable high-speed camera capture mode.\n"
"This mode is restricted to specific resolutions and frame "
"rates, listed by --list-camera-sizes.",
},
{
.longopt_id = OPT_CAMERA_SIZE,
.longopt = "camera-size",
.argdesc = "<width>x<height>",
.text = "Specify an explicit camera capture size.",
},
{
.longopt_id = OPT_CAMERA_FPS,
.longopt = "camera-fps",
.argdesc = "value",
.text = "Specify the camera capture frame rate.\n"
"If not specified, Android's default frame rate (30 fps) is "
"used.",
},
{
// Not really deprecated (--codec has never been released), but without
// declaring an explicit --codec option, getopt_long() partial matching
@@ -1315,7 +1332,7 @@ parse_max_size(const char *s, uint16_t *max_size) {
static bool
parse_max_fps(const char *s, uint16_t *max_fps) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 1000, "max fps");
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max fps");
if (!ok) {
return false;
}
@@ -1746,6 +1763,18 @@ parse_camera_facing(const char *optarg, enum sc_camera_facing *facing) {
return false;
}
static bool
parse_camera_fps(const char *s, uint16_t *camera_fps) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "camera fps");
if (!ok) {
return false;
}
*camera_fps = (uint16_t) value;
return true;
}
static bool
parse_time_limit(const char *s, sc_tick *tick) {
long value;
@@ -2154,6 +2183,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
break;
case OPT_CAMERA_FPS:
if (!parse_camera_fps(optarg, &opts->camera_fps)) {
return false;
}
break;
case OPT_CAMERA_HIGH_SPEED:
opts->camera_high_speed = true;
break;
default:
// getopt prints the error message on stderr
return false;
@@ -2253,19 +2290,13 @@ 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) {
LOGE("--lock-video-orientation is not supported for camera");
return false;
}
if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) {
LOGE("Could not specify both --camera-id and --camera-facing");
return false;
}
if (opts->camera_size) {
if (opts->camera_ar) {
if (opts->max_size) {
LOGE("Could not specify both --camera-size and -m/--max-size");
return false;
}
@@ -2276,6 +2307,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
}
if (opts->camera_high_speed && !opts->camera_fps) {
LOGE("--camera-high-speed requires an explicit --camera-fps value");
return false;
}
if (opts->control) {
LOGI("Camera video source: control disabled");
opts->control = false;
@@ -2283,6 +2319,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
} else if (opts->camera_id
|| opts->camera_ar
|| opts->camera_facing != SC_CAMERA_FACING_ANY
|| opts->camera_fps
|| opts->camera_high_speed
|| opts->camera_size) {
LOGE("Camera options are only available with --video-source=camera");
return false;

View File

@@ -14,6 +14,7 @@ const struct scrcpy_options scrcpy_options_default = {
.camera_id = NULL,
.camera_size = NULL,
.camera_ar = NULL,
.camera_fps = 0,
.log_level = SC_LOG_LEVEL_INFO,
.video_codec = SC_CODEC_H264,
.audio_codec = SC_CODEC_OPUS,
@@ -85,5 +86,6 @@ const struct scrcpy_options scrcpy_options_default = {
.audio = true,
.require_audio = false,
.kill_adb_on_close = false,
.camera_high_speed = false,
.list = 0,
};

View File

@@ -133,6 +133,7 @@ struct scrcpy_options {
const char *camera_id;
const char *camera_size;
const char *camera_ar;
uint16_t camera_fps;
enum sc_log_level log_level;
enum sc_codec video_codec;
enum sc_codec audio_codec;
@@ -198,6 +199,7 @@ struct scrcpy_options {
bool audio;
bool require_audio;
bool kill_adb_on_close;
bool camera_high_speed;
#define SC_OPTION_LIST_ENCODERS 0x1
#define SC_OPTION_LIST_DISPLAYS 0x2
#define SC_OPTION_LIST_CAMERAS 0x4

View File

@@ -376,6 +376,7 @@ scrcpy(struct scrcpy_options *options) {
.camera_id = options->camera_id,
.camera_size = options->camera_size,
.camera_ar = options->camera_ar,
.camera_fps = options->camera_fps,
.force_adb_forward = options->force_adb_forward,
.power_off_on_close = options->power_off_on_close,
.clipboard_autosync = options->clipboard_autosync,
@@ -385,6 +386,7 @@ scrcpy(struct scrcpy_options *options) {
.cleanup = options->cleanup,
.power_on = options->power_on,
.kill_adb_on_close = options->kill_adb_on_close,
.camera_high_speed = options->camera_high_speed,
.list = options->list,
};

View File

@@ -308,6 +308,12 @@ execute_server(struct sc_server *server,
if (params->camera_ar) {
ADD_PARAM("camera_ar=%s", params->camera_ar);
}
if (params->camera_fps) {
ADD_PARAM("camera_fps=%" PRIu16, params->camera_fps);
}
if (params->camera_high_speed) {
ADD_PARAM("camera_high_speed=true");
}
if (params->show_touches) {
ADD_PARAM("show_touches=true");
}

View File

@@ -37,6 +37,7 @@ struct sc_server_params {
const char *camera_id;
const char *camera_size;
const char *camera_ar;
uint16_t camera_fps;
struct sc_port_range port_range;
uint32_t tunnel_host;
uint16_t tunnel_port;
@@ -62,6 +63,7 @@ struct sc_server_params {
bool cleanup;
bool power_on;
bool kill_adb_on_close;
bool camera_high_speed;
uint8_t list;
};

150
doc/camera.md Normal file
View File

@@ -0,0 +1,150 @@
# Camera
Camera mirroring is supported for devices with Android 12 or higher.
To capture the camera instead of the device screen:
```
scrcpy --video-source=camera
```
By default, it automatically switches [audio source](audio.md#source) to
microphone (as if `--audio-source=mic` were also passed).
```bash
scrcpy --video-source=display # default is --audio-source=output
scrcpy --video-source=camera # default is --audio-source=mic
scrcpy --video-source=display --audio-source=mic # force display AND microphone
scrcpy --video-source=camera --audio-source=output # force camera AND device audio output
```
## List
To list the cameras available (with their declared valid sizes and frame rates):
```
scrcpy --list-cameras
scrcpy --list-camera-sizes
```
_Note that the sizes and frame rates are declarative. They are not accurate on
all devices: some of them are declared but not supported, while some others are
not declared but supported._
## Selection
It is possible to pass an explicit camera id (as listed by `--list-cameras`):
```
scrcpy --video-source=camera --camera-id=0
```
Alternatively, the camera may be selected automatically:
```bash
scrcpy --video-source=camera # use the first camera
scrcpy --video-source=camera --camera-facing=front # use the first front camera
scrcpy --video-source=camera --camera-facing=back # use the first back camera
scrcpy --video-source=camera --camera-facing=external # use the first external camera
```
If `--camera-id` is specified, then `--camera-facing` is forbidden (the id
already determines the camera):
```bash
scrcpy --video-source=camera --camera-id=0 --camera-facing=front # error
```
### Size selection
It is possible to pass an explicit camera size:
```
scrcpy --video-source=camera --camera-size=1920x1080
```
The given size may be listed among the declared valid sizes
(`--list-camera-sizes`), but may also be anything else (some devices support
arbitrary sizes):
```
scrcpy --video-source=camera --camera-size=1840x444
```
Alternatively, a declared valid size (among the ones listed by
`list-camera-sizes`) may be selected automatically.
Two constraints are supported:
- `-m`/`--max-size` (already used for display mirroring), for example `-m1920`;
- `--camera-ar` to specify an aspect ratio (`<num>:<den>`, `<value>` or
`sensor`).
Some examples:
```bash
scrcpy --video-source=camera # use the greatest width and the greatest associated height
scrcpy --video-source=camera -m1920 # use the greatest width not above 1920 and the greatest associated height
scrcpy --video-source=camera --camera-ar=4:3 # use the greatest size with an aspect ratio of 4:3 (+/- 10%)
scrcpy --video-source=camera --camera-ar=1.6 # use the greatest size with an aspect ratio of 1.6 (+/- 10%)
scrcpy --video-source=camera --camera-ar=sensor # use the greatest size with the aspect ratio of the camera sensor (+/- 10%)
scrcpy --video-source=camera -m1920 --camera-ar=16:9 # use the greatest width not above 1920 and the closest to 16:9 aspect ratio
```
If `--camera-size` is specified, then `-m`/`--max-size` and `--camera-ar` are
forbidden (the size is determined by the value given explicitly):
```bash
scrcpy --video-source=camera --camera-size=1920x1080 -m3000 # error
```
## Frame rate
By default, camera is captured at Android's default frame rate (30 fps).
To configure a different frame rate:
```
scrcpy --video-source=camera --camera-fps=60
```
## High speed capture
The Android camera API also supports a [high speed capture mode][high speed].
This mode is restricted to specific resolutions and frame rates, listed by
`--list-camera-sizes`.
```
scrcpy --video-source=camera --camera-size=1920x1080 --camera-fps=240
```
[high speed]: https://developer.android.com/reference/android/hardware/camera2/CameraConstrainedHighSpeedCaptureSession
## Brace expansion tip
All camera options start with `--camera-`, so if your shell supports it, you can
benefit from [brace expansion] (for example, it is supported _bash_ and _zsh_):
```bash
scrcpy --video-source=camera --camera-{facing=back,ar=16:9,high-speed,fps=120}
```
This will be expanded as:
```bash
scrcpy --video-source=camera --camera-facing=back --camera-ar=16:9 --camera-high-speed --camera-fps=120
```
[brace expansion]: https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html
## Webcam
Combined with the [V4L2](v4l2.md) feature on Linux, the Android device camera
may be used as a webcam on the computer.

View File

@@ -1,5 +1,14 @@
# Video
## Source
By default, scrcpy mirrors the device screen.
It is possible to capture the device camera instead.
See the dedicated [camera](camera.md) page.
## Size
By default, scrcpy attempts to mirror at the Android device resolution.

View File

@@ -8,6 +8,7 @@ import android.graphics.Rect;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureFailure;
@@ -19,11 +20,11 @@ import android.media.MediaCodec;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Range;
import android.view.Surface;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@@ -39,6 +40,8 @@ public class CameraCapture extends SurfaceCapture {
private final Size explicitSize;
private int maxSize;
private final CameraAspectRatio aspectRatio;
private final int fps;
private final boolean highSpeed;
private String cameraId;
private Size size;
@@ -50,12 +53,15 @@ public class CameraCapture extends SurfaceCapture {
private final AtomicBoolean disconnected = new AtomicBoolean();
public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio) {
public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, int fps,
boolean highSpeed) {
this.explicitCameraId = explicitCameraId;
this.cameraFacing = cameraFacing;
this.explicitSize = explicitSize;
this.maxSize = maxSize;
this.aspectRatio = aspectRatio;
this.fps = fps;
this.highSpeed = highSpeed;
}
@Override
@@ -71,7 +77,7 @@ public class CameraCapture extends SurfaceCapture {
throw new IOException("No matching camera found");
}
size = selectSize(cameraId, explicitSize, maxSize, aspectRatio);
size = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed);
if (size == null) {
throw new IOException("Could not select camera size");
}
@@ -110,7 +116,8 @@ public class CameraCapture extends SurfaceCapture {
}
@TargetApi(Build.VERSION_CODES.N)
private static Size selectSize(String cameraId, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio) throws CameraAccessException {
private static Size selectSize(String cameraId, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, boolean highSpeed)
throws CameraAccessException {
if (explicitSize != null) {
return explicitSize;
}
@@ -119,7 +126,7 @@ public class CameraCapture extends SurfaceCapture {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class);
android.util.Size[] sizes = highSpeed ? configs.getHighSpeedVideoSizes() : configs.getOutputSizes(MediaCodec.class);
Stream<android.util.Size> stream = Arrays.stream(sizes);
if (maxSize > 0) {
stream = stream.filter(it -> it.getWidth() <= maxSize && it.getHeight() <= maxSize);
@@ -135,8 +142,34 @@ public class CameraCapture extends SurfaceCapture {
});
}
Optional<android.util.Size> selected = stream.min(
Comparator.comparing(android.util.Size::getWidth).thenComparing(android.util.Size::getHeight).reversed());
Optional<android.util.Size> selected = stream.max((s1, s2) -> {
// Greater width is better
int cmp = Integer.compare(s1.getWidth(), s2.getWidth());
if (cmp != 0) {
return cmp;
}
if (targetAspectRatio != null) {
// Closer to the target aspect ratio is better
float ar1 = ((float) s1.getWidth() / s1.getHeight());
float arRatio1 = ar1 / targetAspectRatio;
float distance1 = Math.abs(1 - arRatio1);
float ar2 = ((float) s2.getWidth() / s2.getHeight());
float arRatio2 = ar2 / targetAspectRatio;
float distance2 = Math.abs(1 - arRatio2);
// Reverse the order because lower distance is better
cmp = Float.compare(distance2, distance1);
if (cmp != 0) {
return cmp;
}
}
// Greater height is better
return Integer.compare(s1.getHeight(), s2.getHeight());
});
if (selected.isPresent()) {
android.util.Size size = selected.get();
return new Size(size.getWidth(), size.getHeight());
@@ -163,9 +196,7 @@ public class CameraCapture extends SurfaceCapture {
public void start(Surface surface) throws IOException {
try {
CameraCaptureSession session = createCaptureSession(cameraDevice, surface);
CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
requestBuilder.addTarget(surface);
CaptureRequest request = requestBuilder.build();
CaptureRequest request = createCaptureRequest(surface);
setRepeatingRequest(session, request);
} catch (CameraAccessException | InterruptedException e) {
throw new IOException(e);
@@ -195,8 +226,8 @@ public class CameraCapture extends SurfaceCapture {
this.maxSize = maxSize;
try {
size = selectSize(cameraId, null, maxSize, aspectRatio);
return true;
size = selectSize(cameraId, null, maxSize, aspectRatio, highSpeed);
return size != null;
} catch (CameraAccessException e) {
Ln.w("Could not select camera size", e);
return false;
@@ -256,7 +287,9 @@ public class CameraCapture extends SurfaceCapture {
CompletableFuture<CameraCaptureSession> future = new CompletableFuture<>();
OutputConfiguration outputConfig = new OutputConfiguration(surface);
List<OutputConfiguration> outputs = Arrays.asList(outputConfig);
SessionConfiguration sessionConfig = new SessionConfiguration(SessionConfiguration.SESSION_REGULAR, outputs, cameraExecutor,
int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR;
SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor,
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
@@ -278,25 +311,37 @@ public class CameraCapture extends SurfaceCapture {
}
}
private CaptureRequest createCaptureRequest(Surface surface) throws CameraAccessException {
CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
requestBuilder.addTarget(surface);
if (fps > 0) {
requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<>(fps, fps));
}
return requestBuilder.build();
}
@TargetApi(Build.VERSION_CODES.S)
private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException, InterruptedException {
CompletableFuture<Void> future = new CompletableFuture<>();
session.setRepeatingRequest(request, new CameraCaptureSession.CaptureCallback() {
CameraCaptureSession.CaptureCallback callback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber) {
future.complete(null);
// Called for each frame captured, do nothing
}
@Override
public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) {
future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR));
Ln.w("Camera capture failed: frame " + failure.getFrameNumber());
}
}, cameraHandler);
};
try {
future.get();
} catch (ExecutionException e) {
throw (CameraAccessException) e.getCause();
if (highSpeed) {
CameraConstrainedHighSpeedCaptureSession highSpeedSession = (CameraConstrainedHighSpeedCaptureSession) session;
List<CaptureRequest> requests = highSpeedSession.createHighSpeedRequestList(request);
highSpeedSession.setRepeatingBurst(requests, callback, cameraHandler);
} else {
session.setRepeatingRequest(request, callback, cameraHandler);
}
}

View File

@@ -1,10 +1,12 @@
package com.genymobile.scrcpy;
import android.annotation.SuppressLint;
import android.hardware.camera2.CameraCharacteristics;
public enum CameraFacing {
FRONT("front", CameraCharacteristics.LENS_FACING_FRONT),
BACK("back", CameraCharacteristics.LENS_FACING_BACK),
@SuppressLint("InlinedApi") // introduced in API 23
EXTERNAL("external", CameraCharacteristics.LENS_FACING_EXTERNAL);
private final String name;

View File

@@ -8,6 +8,11 @@ import android.util.Log;
*/
public final class Ln {
public interface ConsolePrinter {
void printOut(String message);
void printErr(String message, Throwable throwable);
}
private static final String TAG = "scrcpy";
private static final String PREFIX = "[server] ";
@@ -15,12 +20,17 @@ public final class Ln {
VERBOSE, DEBUG, INFO, WARN, ERROR
}
private static ConsolePrinter consolePrinter = new DefaultConsolePrinter();
private static Level threshold = Level.INFO;
private Ln() {
// not instantiable
}
public static void setConsolePrinter(ConsolePrinter consolePrinter) {
Ln.consolePrinter = consolePrinter;
}
/**
* Initialize the log level.
* <p>
@@ -39,31 +49,28 @@ public final class Ln {
public static void v(String message) {
if (isEnabled(Level.VERBOSE)) {
Log.v(TAG, message);
System.out.print(PREFIX + "VERBOSE: " + message + '\n');
consolePrinter.printOut(PREFIX + "VERBOSE: " + message + '\n');
}
}
public static void d(String message) {
if (isEnabled(Level.DEBUG)) {
Log.d(TAG, message);
System.out.print(PREFIX + "DEBUG: " + message + '\n');
consolePrinter.printOut(PREFIX + "DEBUG: " + message + '\n');
}
}
public static void i(String message) {
if (isEnabled(Level.INFO)) {
Log.i(TAG, message);
System.out.print(PREFIX + "INFO: " + message + '\n');
consolePrinter.printOut(PREFIX + "INFO: " + message + '\n');
}
}
public static void w(String message, Throwable throwable) {
if (isEnabled(Level.WARN)) {
Log.w(TAG, message, throwable);
System.err.print(PREFIX + "WARN: " + message + '\n');
if (throwable != null) {
throwable.printStackTrace();
}
consolePrinter.printErr(PREFIX + "WARN: " + message + '\n', throwable);
}
}
@@ -74,14 +81,26 @@ public final class Ln {
public static void e(String message, Throwable throwable) {
if (isEnabled(Level.ERROR)) {
Log.e(TAG, message, throwable);
System.err.print(PREFIX + "ERROR: " + message + "\n");
if (throwable != null) {
throwable.printStackTrace();
}
consolePrinter.printErr(PREFIX + "ERROR: " + message + '\n', throwable);
}
}
public static void e(String message) {
e(message, null);
}
public static class DefaultConsolePrinter implements ConsolePrinter {
@Override
public void printOut(String message) {
System.out.print(message);
}
@Override
public void printErr(String message, Throwable throwable) {
System.err.print(message);
if (throwable != null) {
throwable.printStackTrace();
}
}
}
}

View File

@@ -9,8 +9,11 @@ import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.MediaCodec;
import android.util.Range;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
public final class LogUtils {
@@ -92,19 +95,36 @@ public final class LogUtils {
for (String id : cameraIds) {
builder.append("\n --video-source=camera --camera-id=").append(id);
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
builder.append(" (").append(getCameraFacingName(facing)).append(", ");
Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
builder.append(activeSize.width()).append("x").append(activeSize.height());
builder.append(')');
builder.append(activeSize.width()).append("x").append(activeSize.height()).append(", ");
// Capture frame rates for low-FPS mode are the same for every resolution
Range<Integer>[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges);
builder.append("fps=").append(uniqueLowFps).append(')');
if (includeSizes) {
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class);
for (android.util.Size size : sizes) {
builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight());
}
android.util.Size[] highSpeedSizes = configs.getHighSpeedVideoSizes();
if (highSpeedSizes.length > 0) {
builder.append("\n High speed capture (--camera-high-speed):");
for (android.util.Size size : highSpeedSizes) {
Range<Integer>[] highFpsRanges = configs.getHighSpeedVideoFpsRanges();
SortedSet<Integer> uniqueHighFps = getUniqueSet(highFpsRanges);
builder.append("\n - ").append(size.getWidth()).append("x").append(size.getHeight());
builder.append(" (fps=").append(uniqueHighFps).append(')');
}
}
}
}
}
@@ -113,4 +133,12 @@ public final class LogUtils {
}
return builder.toString();
}
private static SortedSet<Integer> getUniqueSet(Range<Integer>[] ranges) {
SortedSet<Integer> set = new TreeSet<>();
for (Range<Integer> range : ranges) {
set.add(range.getUpper());
}
return set;
}
}

View File

@@ -28,6 +28,8 @@ public class Options {
private Size cameraSize;
private CameraFacing cameraFacing;
private CameraAspectRatio cameraAspectRatio;
private int cameraFps;
private boolean cameraHighSpeed;
private boolean showTouches;
private boolean stayAwake;
private List<CodecOption> videoCodecOptions;
@@ -136,6 +138,14 @@ public class Options {
return cameraAspectRatio;
}
public int getCameraFps() {
return cameraFps;
}
public boolean getCameraHighSpeed() {
return cameraHighSpeed;
}
public boolean getShowTouches() {
return showTouches;
}
@@ -384,6 +394,12 @@ public class Options {
options.cameraAspectRatio = parseCameraAspectRatio(value);
}
break;
case "camera_fps":
options.cameraFps = Integer.parseInt(value);
break;
case "camera_high_speed":
options.cameraHighSpeed = Boolean.parseBoolean(value);
break;
case "send_device_meta":
options.sendDeviceMeta = Boolean.parseBoolean(value);
break;

View File

@@ -48,8 +48,8 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList
}
@Override
public boolean setMaxSize(int size) {
device.setMaxSize(size);
public boolean setMaxSize(int maxSize) {
device.setMaxSize(maxSize);
return true;
}

View File

@@ -87,8 +87,6 @@ public final class Server {
}
private static void scrcpy(Options options) throws IOException, ConfigurationException {
Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && options.getVideoSource() == VideoSource.CAMERA) {
Ln.e("Camera mirroring is not supported before Android 12");
throw new ConfigurationException("Camera mirroring is not supported");
@@ -104,8 +102,8 @@ public final class Server {
boolean video = options.getVideo();
boolean audio = options.getAudio();
boolean sendDummyByte = options.getSendDummyByte();
boolean camera = options.getVideoSource() == VideoSource.CAMERA;
boolean camera = true;
Workarounds.apply(audio, camera);
List<AsyncProcessor> asyncProcessors = new ArrayList<>();
@@ -144,7 +142,7 @@ public final class Server {
surfaceCapture = new ScreenCapture(device);
} else {
surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(),
options.getMaxSize(), options.getCameraAspectRatio());
options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed());
}
SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
@@ -184,7 +182,22 @@ public final class Server {
return thread;
}
public static void main(String... args) throws Exception {
public static void main(String... args) {
int status = 0;
try {
internalMain(args);
} catch (Throwable t) {
t.printStackTrace();
status = 1;
} finally {
// By default, the Java process exits when all non-daemon threads are terminated.
// The Android SDK might start some non-daemon threads internally, preventing the scrcpy server to exit.
// So force the process to exit explicitly.
System.exit(status);
}
}
private static void internalMain(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
Ln.e("Exception on thread " + t, e);
});
@@ -193,6 +206,8 @@ public final class Server {
Ln.initLogLevel(options.getLogLevel());
Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
if (options.getList()) {
if (options.getCleanup()) {
CleanUp.unlinkSelf();

View File

@@ -56,9 +56,9 @@ public abstract class SurfaceCapture {
/**
* Set the maximum capture size (set by the encoder if it does not support the current size).
*
* @param size Maximum size
* @param maxSize Maximum size
*/
public abstract boolean setMaxSize(int size);
public abstract boolean setMaxSize(int maxSize);
/**
* Indicate if the capture has been closed internally.

View File

@@ -6,7 +6,7 @@ import android.media.MediaFormat;
public enum VideoCodec implements Codec {
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC),
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC),
@SuppressLint("InlinedApi") // introduced in API 21
@SuppressLint("InlinedApi") // introduced in API 29
AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1);
private final int id; // 4-byte ASCII representation of the name

View File

@@ -14,6 +14,12 @@ import android.os.Build;
import android.os.Looper;
import android.os.Parcel;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
@@ -55,6 +61,12 @@ public final class Workarounds {
mustFillAppInfo = true;
mustFillBaseContext = true;
mustFillAppContext = true;
} else if (Build.MANUFACTURER.equalsIgnoreCase("xiaomi")) {
// Trash the messages printed to the console by direct calls to System.out and System.err.
// Xiaomi device ROMs may print internal errors using e.printStackTrace(), flooding the console with irrelevant errors.
ExclusiveConsolePrinter exclusiveConsolePrinter = new ExclusiveConsolePrinter();
exclusiveConsolePrinter.installNullSystemStreams();
Ln.setConsolePrinter(new ExclusiveConsolePrinter());
}
if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
@@ -306,4 +318,48 @@ public final class Workarounds {
throw new RuntimeException("Cannot create AudioRecord");
}
}
static class ExclusiveConsolePrinter implements Ln.ConsolePrinter {
static class NullOutputStream extends OutputStream {
@Override
public void write(byte[] b) {
// ignore
}
@Override
public void write(byte[] b, int off, int len) {
// ignore
}
@Override
public void write(int b) {
// ignore
}
}
private final PrintStream realOut = new PrintStream(new FileOutputStream(FileDescriptor.out));
private final PrintStream realErr = new PrintStream(new FileOutputStream(FileDescriptor.err));
void installNullSystemStreams() {
PrintStream nullStream = new PrintStream(new NullOutputStream());
System.setOut(nullStream);
System.setErr(nullStream);
}
@Override
public void printOut(String message) {
realOut.print(message);
}
@Override
public void printErr(String message, Throwable throwable) {
realErr.print(message);
if (throwable != null) {
StringWriter errors = new StringWriter();
throwable.printStackTrace(new PrintWriter(errors));
realErr.print(errors);
}
}
}
}