Compare commits

...

18 Commits

Author SHA1 Message Date
6e1aebcc28 Reset video capture on folding event
Handle folding event the same way as rotation events.

Fixes #3960 <https://github.com/Genymobile/scrcpy/issues/3960>
PR #3979 <https://github.com/Genymobile/scrcpy/pull/3979>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-05-29 19:32:32 +02:00
e7d70f3d0c Rename rotationChanged to resetCapture
The flag is used to reset the capture (restart the encoding) on rotation
change. It will also be used for other events (on folding change), so
rename it.

PR #3979 <https://github.com/Genymobile/scrcpy/pull/3979>
2023-05-29 19:32:28 +02:00
Yan
798dfd240e Turn device screen off after set up
Sometimes it can take quite a while for everything to get set up and
the screen to appear.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-05-27 10:11:42 +02:00
c4caa6b81d Document --no-{video,audio}-playback
PR #4033 <https://github.com/Genymobile/scrcpy/pull/4033>
2023-05-27 10:08:10 +02:00
1efbfe1175 Add separate video and audio playback options
Add --no-video-playback and --no-audio-playback. The option
--no-playback is now an alias for both.

PR #4033 <https://github.com/Genymobile/scrcpy/pull/4033>
2023-05-27 10:08:10 +02:00
751c09f47a Simplify V4L2/USB ifdefs
Define local variables whose value depends on ifdefs, to avoid
cluttering all conditions with ifdefs.
2023-05-27 09:55:49 +02:00
6ad46d70b8 Define v4l2_buffer only if HAVE_V4L2
If V4L2 support is disabled, there is no v4l2 buffer option.
2023-05-27 09:55:49 +02:00
f46758d1c5 Fix V4L2 error message when disabled
For consistency, use the same error message for --v4l2-sink and
--v4l2-buffer.
2023-05-27 09:55:49 +02:00
e71f5358b3 Reorder command line options checks
Perform checks that impact the options first.
2023-05-27 09:55:49 +02:00
a2c8910006 Rename --no-mirror to --no-playback
This option impacts video and audio _playback_. For example, if we use
V4L2, the device is still "mirrored" (via V4L2), even if playback is
disabled. Therefore, "playback" is more approriate than "mirror".

The initial option --no-display option was renamed to --no-mirror by
commit 6928acdeac, but this has never been
released, so it is ok to rename it one more time.

Refs #3978 <https://github.com/Genymobile/scrcpy/pull/3978#issuecomment-1549420103>
PR #4033 <https://github.com/Genymobile/scrcpy/pull/4033>
2023-05-27 09:55:38 +02:00
cab354102d Create AudioRecord by reflection as a fallback
Some devices (Vivo phones) fail to create an AudioRecord from an
AudioRecord.Builder (which throws a NullPointerException).

In that case, create an AudioRecord instance directly by reflection.

The AOSP version of AudioRecord constructor code can be found at:
 - Android 11 (R):
   <https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:frameworks/base/media/java/android/media/AudioRecord.java;l=335;drc=64ed2ec38a511bbbd048985fe413268335e072f8>
 - Android 12 (S):
   <https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/media/java/android/media/AudioRecord.java;l=388;drc=2eebf929650e0d320a21f0d13677a27d7ab278e9>
 - Android 13 (T, functionally identical to Android 12):
   <https://cs.android.com/android/platform/superproject/+/android-13.0.0_r1:frameworks/base/media/java/android/media/AudioRecord.java;l=382;drc=ed242da52f975a1dd18671afb346b18853d729f2>
 - Android 14 (U): Not released, but expected to change

PR #3862 <https://github.com/Genymobile/scrcpy/pull/3862>
Fixes #3805 <https://github.com/Genymobile/scrcpy/issues/3805>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-05-26 18:56:43 +02:00
597d2ccc01 Rename FORMAT to ENCODING
The AudioFormat contains several properties. This specific value is
named "encoding".
2023-05-24 22:13:09 +02:00
38900d7730 Extract audio source to a static constant
For consistency with the other parameters.
2023-05-24 22:13:09 +02:00
e926bf1fe8 Delay window resize when minimized
On some window managers (e.g. on Windows), performing a resize while the
window is minimized does nothing (the restored window keeps its old
size).

Therefore, like for maximized and fullscreen states, wait for the window
to be restored to apply a resize.

Refs #3947 <https://github.com/Genymobile/scrcpy/issues/3947>
2023-05-22 18:22:45 +02:00
6298ef095f Accept texture failures
When the scrcpy window is minimized on Windows with D3D9, texture
creation and update fail.

In that case, do not terminate scrcpy. Instead, store the pending size
or frame to update, to attempt again during the next update or
rendering.

Fixes #3947 <https://github.com/Genymobile/scrcpy/issues/3947>
2023-05-22 18:21:10 +02:00
7d33798b40 Upgrade FFmpeg build to 6.0-scrcpy-4
Use FFmpeg DLLs which do not depend on zlib1.dll.
2023-05-15 21:55:22 +02:00
d500550212 Update audio recording documentation
Document how to record audio-only to .opus and .aac files.

PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-15 14:28:55 +02:00
a166eee909 Upgrade FFmpeg build to 6.0-scrcpy-3
Use a build which includes the opus muxer, to support recording to .opus
files.

Refs <https://github.com/rom1v/scrcpy-deps/commits/6.0-scrcpy-3>
PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-15 14:28:53 +02:00
26 changed files with 580 additions and 160 deletions

View File

@ -29,15 +29,17 @@ _scrcpy() {
-M --hid-mouse
-m --max-size=
--no-audio
--no-audio-playback
--no-cleanup
--no-clipboard-autosync
--no-downsize-on-error
-n --no-control
-N --no-mirror
-N --no-playback
--no-key-repeat
--no-mipmaps
--no-power-on
--no-video
--no-video-playback
--otg
-p --port=
--power-off-on-close

View File

@ -35,15 +35,17 @@ arguments=(
{-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]'
{-m,--max-size=}'[Limit both the width and height of the video to value]'
'--no-audio[Disable audio forwarding]'
'--no-audio-playback[Disable audio playback]'
'--no-cleanup[Disable device cleanup actions on exit]'
'--no-clipboard-autosync[Disable automatic clipboard synchronization]'
'--no-downsize-on-error[Disable lowering definition on MediaCodec error]'
{-n,--no-control}'[Disable device control \(mirror the device in read only\)]'
{-N,--no-mirror}'[Do not mirror device \(only when recording or V4L2 sink is enabled\)]'
{-N,--no-playback}'[Disable video and audio playback]'
'--no-key-repeat[Do not forward repeated key events when a key is held down]'
'--no-mipmaps[Disable the generation of mipmaps]'
'--no-power-on[Do not power on the device on start]'
'--no-video[Disable video forwarding]'
'--no-video-playback[Disable video playback]'
'--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]'
{-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]'
'--power-off-on-close[Turn the device screen off when closing scrcpy]'

View File

@ -6,11 +6,11 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
VERSION=6.0-scrcpy-2
VERSION=6.0-scrcpy-4
DEP_DIR="ffmpeg-$VERSION"
FILENAME="$DEP_DIR".7z
SHA256SUM=98ef97f8607c97a5c4f9c5a0a991b78f105d002a3619145011d16ffb92501b14
SHA256SUM=39274b321491ce83e76cab5d24e7cbe3f402d3ccf382f739b13be5651c146b60
if [[ -d "$DEP_DIR" ]]
then

View File

@ -187,6 +187,10 @@ Also see \fB\-\-hid\-keyboard\fR.
.B \-\-no\-audio
Disable audio forwarding.
.TP
.B \-\-no\-audio\-playback
Disable audio playback on the computer.
.TP
.B \-\-no\-cleanup
By default, scrcpy removes the server binary from the device and restores the device state (show touches, stay awake and power mode) on exit.
@ -210,8 +214,8 @@ This option disables this behavior.
Disable device control (mirror the device in read\-only).
.TP
.B \-N, \-\-no\-mirror
Do not mirror device video or audio on the computer (only when recording or V4L2 sink is enabled).
.B \-N, \-\-no\-playback
Disable video and audio playback on the computer (equivalent to --no-video-playback --no-audio-playback).
.TP
.B \-\-no\-key\-repeat
@ -229,6 +233,10 @@ Do not power on the device on start.
.B \-\-no\-video
Disable video forwarding.
.TP
.B \-\-no\-video\-playback
Disable video playback on the computer.
.TP
.B \-\-otg
Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable.

View File

@ -74,6 +74,8 @@ enum {
OPT_AUDIO_OUTPUT_BUFFER,
OPT_NO_DISPLAY,
OPT_NO_VIDEO,
OPT_NO_AUDIO_PLAYBACK,
OPT_NO_VIDEO_PLAYBACK,
};
struct sc_option {
@ -351,6 +353,11 @@ static const struct sc_option options[] = {
.longopt = "no-audio",
.text = "Disable audio forwarding.",
},
{
.longopt_id = OPT_NO_AUDIO_PLAYBACK,
.longopt = "no-audio-playback",
.text = "Disable audio playback on the computer.",
},
{
.longopt_id = OPT_NO_CLEANUP,
.longopt = "no-cleanup",
@ -382,9 +389,9 @@ static const struct sc_option options[] = {
},
{
.shortopt = 'N',
.longopt = "no-mirror",
.text = "Do not mirror device video or audio on the computer (only "
"when recording or V4L2 sink is enabled).",
.longopt = "no-playback",
.text = "Disable video and audio playback on the computer (equivalent "
"to --no-video-playback --no-audio-playback).",
},
{
// deprecated
@ -413,6 +420,11 @@ static const struct sc_option options[] = {
.longopt = "no-video",
.text = "Disable video forwarding.",
},
{
.longopt_id = OPT_NO_VIDEO_PLAYBACK,
.longopt = "no-video-playback",
.text = "Disable video playback on the computer.",
},
{
.longopt_id = OPT_OTG,
.longopt = "otg",
@ -1671,10 +1683,17 @@ 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-mirror instead.");
LOGW("--no-display is deprecated, use --no-playback instead.");
// fall through
case 'N':
opts->mirror = false;
opts->video_playback = false;
opts->audio_playback = false;
break;
case OPT_NO_VIDEO_PLAYBACK:
opts->video_playback = false;
break;
case OPT_NO_AUDIO_PLAYBACK:
opts->audio_playback = false;
break;
case 'p':
if (!parse_port_range(optarg, &opts->port_range)) {
@ -1872,7 +1891,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
break;
#else
LOGE("V4L2 (--v4l2-buffer) is only available on Linux.");
LOGE("V4L2 (--v4l2-buffer) is disabled (or unsupported on this "
"platform).");
return false;
#endif
case OPT_LIST_ENCODERS:
@ -1923,14 +1943,52 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
bool otg = false;
bool v4l2 = false;
#ifdef HAVE_USB
otg = opts->otg;
#endif
#ifdef HAVE_V4L2
if (!opts->mirror && !opts->record_filename && !opts->v4l2_device) {
LOGE("-N/--no-mirror requires either screen recording (-r/--record)"
" or sink to v4l2loopback device (--v4l2-sink)");
v4l2 = !!opts->v4l2_device;
#endif
if (!opts->video) {
opts->video_playback = false;
}
if (!opts->audio) {
opts->audio_playback = false;
}
if (!opts->video_playback && !otg) {
// If video playback is disabled and OTG are disabled, then there is
// no way to control the device.
opts->control = false;
}
if (opts->video && !opts->video_playback && !opts->record_filename
&& !v4l2) {
LOGI("No video playback, no recording, no V4L2 sink: video disabled");
opts->video = false;
}
if (opts->audio && !opts->audio_playback && !opts->record_filename) {
LOGI("No audio playback, no recording: audio disabled");
opts->audio = false;
}
if (!opts->video && !opts->audio && !otg) {
LOGE("No video, no audio, no OTG: nothing to do");
return false;
}
if (opts->v4l2_device) {
if (!opts->video && !otg) {
// If video is disabled, then scrcpy must exit on audio failure.
opts->require_audio = true;
}
#ifdef HAVE_V4L2
if (v4l2) {
if (opts->lock_video_orientation ==
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
LOGI("Video orientation is locked for v4l2 sink. "
@ -1948,18 +2006,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
LOGE("V4L2 buffer value without V4L2 sink\n");
return false;
}
#else
if (!opts->mirror && !opts->record_filename) {
LOGE("-N/--no-mirror requires screen recording (-r/--record)");
return false;
}
#endif
if (opts->audio && !opts->mirror && !opts->record_filename) {
LOGI("No mirror and no recording: audio disabled");
opts->audio = false;
}
if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) {
LOGI("Tunnel host/port is set, "
"--force-adb-forward automatically enabled.");
@ -2039,11 +2087,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
}
#ifdef HAVE_USB
# ifdef _WIN32
if (!opts->otg && (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID
|| opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID)) {
if (!otg && (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID
|| opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID)) {
LOGE("On Windows, it is not possible to open a USB device already open "
"by another process (like adb).");
LOGE("Therefore, -K/--hid-keyboard and -M/--hid-mouse may only work in "
@ -2052,7 +2098,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
# endif
if (opts->otg) {
if (otg) {
// OTG mode is compatible with only very few options.
// Only report obvious errors.
if (opts->record_filename) {
@ -2079,28 +2125,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
LOGE("OTG mode: could not select display");
return false;
}
# ifdef HAVE_V4L2
if (opts->v4l2_device) {
if (v4l2) {
LOGE("OTG mode: could not sink to V4L2 device");
return false;
}
# endif
}
#endif
#ifdef HAVE_USB
if (!(opts->mirror && opts->video) && !opts->otg) {
#else
if (!(opts->mirror && opts->video)) {
#endif
// If video mirroring is disabled and OTG are disabled, then there is
// no way to control the device.
opts->control = false;
}
if (!opts->video) {
// If video is disabled, then scrcpy must exit on audio failure.
opts->require_audio = true;
}
return true;

View File

@ -62,11 +62,17 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) {
LOGD("Trilinear filtering disabled (not an OpenGL renderer");
}
display->pending.flags = 0;
display->pending.frame = NULL;
return true;
}
void
sc_display_destroy(struct sc_display *display) {
if (display->pending.frame) {
av_frame_free(&display->pending.frame);
}
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GL_DeleteContext(display->gl_context);
#endif
@ -84,7 +90,7 @@ sc_display_create_texture(struct sc_display *display,
SDL_TEXTUREACCESS_STREAMING,
size.width, size.height);
if (!texture) {
LOGE("Could not create texture: %s", SDL_GetError());
LOGD("Could not create texture: %s", SDL_GetError());
return NULL;
}
@ -104,8 +110,66 @@ sc_display_create_texture(struct sc_display *display,
return texture;
}
bool
sc_display_set_texture_size(struct sc_display *display, struct sc_size size) {
static inline void
sc_display_set_pending_size(struct sc_display *display, struct sc_size size) {
assert(!display->texture);
display->pending.size = size;
display->pending.flags |= SC_DISPLAY_PENDING_FLAG_SIZE;
}
static bool
sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) {
if (!display->pending.frame) {
display->pending.frame = av_frame_alloc();
if (!display->pending.frame) {
LOG_OOM();
return false;
}
}
int r = av_frame_ref(display->pending.frame, frame);
if (r) {
LOGE("Could not ref frame: %d", r);
return false;
}
display->pending.flags |= SC_DISPLAY_PENDING_FLAG_FRAME;
return true;
}
static bool
sc_display_apply_pending(struct sc_display *display) {
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_SIZE) {
assert(!display->texture);
display->texture =
sc_display_create_texture(display, display->pending.size);
if (!display->texture) {
return false;
}
display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_SIZE;
}
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_FRAME) {
assert(display->pending.frame);
bool ok = sc_display_update_texture(display, display->pending.frame);
if (!ok) {
return false;
}
av_frame_unref(display->pending.frame);
display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_FRAME;
}
return true;
}
static bool
sc_display_set_texture_size_internal(struct sc_display *display,
struct sc_size size) {
assert(size.width && size.height);
if (display->texture) {
SDL_DestroyTexture(display->texture);
}
@ -119,14 +183,27 @@ sc_display_set_texture_size(struct sc_display *display, struct sc_size size) {
return true;
}
bool
sc_display_update_texture(struct sc_display *display, const AVFrame *frame) {
enum sc_display_result
sc_display_set_texture_size(struct sc_display *display, struct sc_size size) {
bool ok = sc_display_set_texture_size_internal(display, size);
if (!ok) {
sc_display_set_pending_size(display, size);
return SC_DISPLAY_RESULT_PENDING;
}
return SC_DISPLAY_RESULT_OK;
}
static bool
sc_display_update_texture_internal(struct sc_display *display,
const AVFrame *frame) {
int ret = SDL_UpdateYUVTexture(display->texture, NULL,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]);
if (ret) {
LOGE("Could not update texture: %s", SDL_GetError());
LOGD("Could not update texture: %s", SDL_GetError());
return false;
}
@ -139,11 +216,34 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame) {
return true;
}
bool
enum sc_display_result
sc_display_update_texture(struct sc_display *display, const AVFrame *frame) {
bool ok = sc_display_update_texture_internal(display, frame);
if (!ok) {
ok = sc_display_set_pending_frame(display, frame);
if (!ok) {
LOGE("Could not set pending frame");
return SC_DISPLAY_RESULT_ERROR;
}
return SC_DISPLAY_RESULT_PENDING;
}
return SC_DISPLAY_RESULT_OK;
}
enum sc_display_result
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
unsigned rotation) {
SDL_RenderClear(display->renderer);
if (display->pending.flags) {
bool ok = sc_display_apply_pending(display);
if (!ok) {
return SC_DISPLAY_RESULT_PENDING;
}
}
SDL_Renderer *renderer = display->renderer;
SDL_Texture *texture = display->texture;
@ -151,7 +251,7 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
int ret = SDL_RenderCopy(renderer, texture, NULL, geometry);
if (ret) {
LOGE("Could not render texture: %s", SDL_GetError());
return false;
return SC_DISPLAY_RESULT_ERROR;
}
} else {
// rotation in RenderCopyEx() is clockwise, while screen->rotation is
@ -176,10 +276,10 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
NULL, 0);
if (ret) {
LOGE("Could not render texture: %s", SDL_GetError());
return false;
return SC_DISPLAY_RESULT_ERROR;
}
}
SDL_RenderPresent(display->renderer);
return true;
return SC_DISPLAY_RESULT_OK;
}

View File

@ -24,6 +24,20 @@ struct sc_display {
#endif
bool mipmaps;
struct {
#define SC_DISPLAY_PENDING_FLAG_SIZE 1
#define SC_DISPLAY_PENDING_FLAG_FRAME 2
int8_t flags;
struct sc_size size;
AVFrame *frame;
} pending;
};
enum sc_display_result {
SC_DISPLAY_RESULT_OK,
SC_DISPLAY_RESULT_PENDING,
SC_DISPLAY_RESULT_ERROR,
};
bool
@ -32,13 +46,13 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps);
void
sc_display_destroy(struct sc_display *display);
bool
enum sc_display_result
sc_display_set_texture_size(struct sc_display *display, struct sc_size size);
bool
enum sc_display_result
sc_display_update_texture(struct sc_display *display, const AVFrame *frame);
bool
enum sc_display_result
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
unsigned rotation);

View File

@ -11,9 +11,6 @@ const struct scrcpy_options scrcpy_options_default = {
.audio_codec_options = NULL,
.video_encoder = NULL,
.audio_encoder = NULL,
#ifdef HAVE_V4L2
.v4l2_device = NULL,
#endif
.log_level = SC_LOG_LEVEL_INFO,
.video_codec = SC_CODEC_H264,
.audio_codec = SC_CODEC_OPUS,
@ -42,9 +39,12 @@ const struct scrcpy_options scrcpy_options_default = {
.window_height = 0,
.display_id = 0,
.display_buffer = 0,
.v4l2_buffer = 0,
.audio_buffer = SC_TICK_FROM_MS(50),
.audio_output_buffer = SC_TICK_FROM_MS(5),
#ifdef HAVE_V4L2
.v4l2_device = NULL,
.v4l2_buffer = 0,
#endif
#ifdef HAVE_USB
.otg = false,
#endif
@ -52,7 +52,8 @@ const struct scrcpy_options scrcpy_options_default = {
.fullscreen = false,
.always_on_top = false,
.control = true,
.mirror = true,
.video_playback = true,
.audio_playback = true,
.turn_screen_off = false,
.key_inject_mode = SC_KEY_INJECT_MODE_MIXED,
.window_borderless = false,

View File

@ -112,9 +112,6 @@ struct scrcpy_options {
const char *audio_codec_options;
const char *video_encoder;
const char *audio_encoder;
#ifdef HAVE_V4L2
const char *v4l2_device;
#endif
enum sc_log_level log_level;
enum sc_codec video_codec;
enum sc_codec audio_codec;
@ -137,9 +134,12 @@ struct scrcpy_options {
uint16_t window_height;
uint32_t display_id;
sc_tick display_buffer;
sc_tick v4l2_buffer;
sc_tick audio_buffer;
sc_tick audio_output_buffer;
#ifdef HAVE_V4L2
const char *v4l2_device;
sc_tick v4l2_buffer;
#endif
#ifdef HAVE_USB
bool otg;
#endif
@ -147,7 +147,8 @@ struct scrcpy_options {
bool fullscreen;
bool always_on_top;
bool control;
bool mirror;
bool video_playback;
bool audio_playback;
bool turn_screen_off;
enum sc_key_inject_mode key_inject_mode;
bool window_borderless;

View File

@ -137,7 +137,7 @@ sdl_set_hints(const char *render_driver) {
}
static void
sdl_configure(bool mirror, bool disable_screensaver) {
sdl_configure(bool video_playback, bool disable_screensaver) {
#ifdef _WIN32
// Clean up properly on Ctrl+C on Windows
bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE);
@ -146,7 +146,7 @@ sdl_configure(bool mirror, bool disable_screensaver) {
}
#endif // _WIN32
if (!mirror) {
if (!video_playback) {
return;
}
@ -386,22 +386,26 @@ scrcpy(struct scrcpy_options *options) {
goto end;
}
if (options->mirror) {
sdl_set_hints(options->render_driver);
// playback implies capture
assert(!options->video_playback || options->video);
assert(!options->audio_playback || options->audio);
// Initialize SDL video and audio in addition if mirroring is enabled
if (options->video && SDL_Init(SDL_INIT_VIDEO)) {
if (options->video_playback) {
sdl_set_hints(options->render_driver);
if (SDL_Init(SDL_INIT_VIDEO)) {
LOGE("Could not initialize SDL video: %s", SDL_GetError());
goto end;
}
}
if (options->audio && SDL_Init(SDL_INIT_AUDIO)) {
if (options->audio_playback) {
if (SDL_Init(SDL_INIT_AUDIO)) {
LOGE("Could not initialize SDL audio: %s", SDL_GetError());
goto end;
}
}
sdl_configure(options->mirror, options->disable_screensaver);
sdl_configure(options->video_playback, options->disable_screensaver);
// Await for server without blocking Ctrl+C handling
bool connected;
@ -427,7 +431,8 @@ scrcpy(struct scrcpy_options *options) {
struct sc_file_pusher *fp = NULL;
assert(!options->control || options->mirror); // control implies mirror
// control implies video playback
assert(!options->control || options->video_playback);
if (options->control) {
if (!sc_file_pusher_init(&s->file_pusher, serial,
options->push_target)) {
@ -453,8 +458,8 @@ scrcpy(struct scrcpy_options *options) {
&audio_demuxer_cbs, options);
}
bool needs_video_decoder = options->mirror && options->video;
bool needs_audio_decoder = options->mirror && options->audio;
bool needs_video_decoder = options->video_playback;
bool needs_audio_decoder = options->audio_playback;
#ifdef HAVE_V4L2
needs_video_decoder |= !!options->v4l2_device;
#endif
@ -634,23 +639,12 @@ aoa_hid_end:
}
controller_started = true;
controller = &s->controller;
if (options->turn_screen_off) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF;
if (!sc_controller_push_msg(&s->controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
}
}
}
// There is a controller if and only if control is enabled
assert(options->control == !!controller);
if (options->mirror) {
if (options->video_playback) {
const char *window_title =
options->window_title ? options->window_title : info->device_name;
@ -684,21 +678,19 @@ aoa_hid_end:
src = &s->display_buffer.frame_source;
}
if (options->video) {
if (!sc_screen_init(&s->screen, &screen_params)) {
goto end;
}
screen_initialized = true;
sc_frame_source_add_sink(src, &s->screen.frame_sink);
if (!sc_screen_init(&s->screen, &screen_params)) {
goto end;
}
screen_initialized = true;
if (options->audio) {
sc_audio_player_init(&s->audio_player, options->audio_buffer,
options->audio_output_buffer);
sc_frame_source_add_sink(&s->audio_decoder.frame_source,
&s->audio_player.frame_sink);
}
sc_frame_source_add_sink(src, &s->screen.frame_sink);
}
if (options->audio_playback) {
sc_audio_player_init(&s->audio_player, options->audio_buffer,
options->audio_output_buffer);
sc_frame_source_add_sink(&s->audio_decoder.frame_source,
&s->audio_player.frame_sink);
}
#ifdef HAVE_V4L2
@ -737,6 +729,18 @@ aoa_hid_end:
audio_demuxer_started = true;
}
// If the device screen is to be turned off, send the control message after
// everything is set up
if (options->control && options->turn_screen_off) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF;
if (!sc_controller_push_msg(&s->controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
}
}
ret = event_loop(s);
LOGD("quit...");

View File

@ -56,6 +56,7 @@ static void
set_window_size(struct sc_screen *screen, struct sc_size new_size) {
assert(!screen->fullscreen);
assert(!screen->maximized);
assert(!screen->minimized);
SDL_SetWindowSize(screen->window, new_size.width, new_size.height);
}
@ -249,9 +250,9 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
sc_screen_update_content_rect(screen);
}
bool ok = sc_display_render(&screen->display, &screen->rect,
screen->rotation);
(void) ok; // error already logged
enum sc_display_result res =
sc_display_render(&screen->display, &screen->rect, screen->rotation);
(void) res; // any error already logged
}
#if defined(__APPLE__) || defined(__WINDOWS__)
@ -359,6 +360,7 @@ sc_screen_init(struct sc_screen *screen,
screen->has_frame = false;
screen->fullscreen = false;
screen->maximized = false;
screen->minimized = false;
screen->mouse_capture_key_pressed = 0;
screen->req.x = params->window_x;
@ -531,11 +533,11 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
static void
set_content_size(struct sc_screen *screen, struct sc_size new_content_size) {
if (!screen->fullscreen && !screen->maximized) {
if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
resize_for_content(screen, screen->content_size, new_content_size);
} else if (!screen->resize_pending) {
// Store the windowed size to be able to compute the optimal size once
// fullscreen and maximized are disabled
// fullscreen/maximized/minimized are disabled
screen->windowed_content_size = screen->content_size;
screen->resize_pending = true;
}
@ -547,6 +549,7 @@ static void
apply_pending_resize(struct sc_screen *screen) {
assert(!screen->fullscreen);
assert(!screen->maximized);
assert(!screen->minimized);
if (screen->resize_pending) {
resize_for_content(screen, screen->windowed_content_size,
screen->content_size);
@ -583,15 +586,17 @@ sc_screen_init_size(struct sc_screen *screen) {
get_rotated_size(screen->frame_size, screen->rotation);
screen->content_size = content_size;
return sc_display_set_texture_size(&screen->display, screen->frame_size);
enum sc_display_result res =
sc_display_set_texture_size(&screen->display, screen->frame_size);
return res != SC_DISPLAY_RESULT_ERROR;
}
// recreate the texture and resize the window if the frame size has changed
static bool
static enum sc_display_result
prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
if (screen->frame_size.width == new_frame_size.width
&& screen->frame_size.height == new_frame_size.height) {
return true;
return SC_DISPLAY_RESULT_OK;
}
// frame dimension changed
@ -615,13 +620,23 @@ sc_screen_update_frame(struct sc_screen *screen) {
sc_fps_counter_add_rendered_frame(&screen->fps_counter);
struct sc_size new_frame_size = {frame->width, frame->height};
if (!prepare_for_frame(screen, new_frame_size)) {
enum sc_display_result res = prepare_for_frame(screen, new_frame_size);
if (res == SC_DISPLAY_RESULT_ERROR) {
return false;
}
if (res == SC_DISPLAY_RESULT_PENDING) {
// Not an error, but do not continue
return true;
}
if (!sc_display_update_texture(&screen->display, frame)) {
res = sc_display_update_texture(&screen->display, frame);
if (res == SC_DISPLAY_RESULT_ERROR) {
return false;
}
if (res == SC_DISPLAY_RESULT_PENDING) {
// Not an error, but do not continue
return true;
}
if (!screen->has_frame) {
screen->has_frame = true;
@ -647,7 +662,7 @@ sc_screen_switch_fullscreen(struct sc_screen *screen) {
}
screen->fullscreen = !screen->fullscreen;
if (!screen->fullscreen && !screen->maximized) {
if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
apply_pending_resize(screen);
}
@ -657,7 +672,7 @@ sc_screen_switch_fullscreen(struct sc_screen *screen) {
void
sc_screen_resize_to_fit(struct sc_screen *screen) {
if (screen->fullscreen || screen->maximized) {
if (screen->fullscreen || screen->maximized || screen->minimized) {
return;
}
@ -681,7 +696,7 @@ sc_screen_resize_to_fit(struct sc_screen *screen) {
void
sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
if (screen->fullscreen) {
if (screen->fullscreen || screen->minimized) {
return;
}
@ -738,6 +753,9 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
case SDL_WINDOWEVENT_MAXIMIZED:
screen->maximized = true;
break;
case SDL_WINDOWEVENT_MINIMIZED:
screen->minimized = true;
break;
case SDL_WINDOWEVENT_RESTORED:
if (screen->fullscreen) {
// On Windows, in maximized+fullscreen, disabling
@ -748,6 +766,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
break;
}
screen->maximized = false;
screen->minimized = false;
apply_pending_resize(screen);
sc_screen_render(screen, true);
break;

View File

@ -56,6 +56,7 @@ struct sc_screen {
bool has_frame;
bool fullscreen;
bool maximized;
bool minimized;
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
// RGUI) must be pressed. This variable tracks the pressed capture key.

View File

@ -53,7 +53,7 @@ static void test_options(void) {
"--max-size", "1024",
"--lock-video-orientation=2", // optional arguments require '='
// "--no-control" is not compatible with "--turn-screen-off"
// "--no-mirror" is not compatible with "--fulscreen"
// "--no-playback" is not compatible with "--fulscreen"
"--port", "1234:1236",
"--push-target", "/sdcard/Movies",
"--record", "file",
@ -108,8 +108,8 @@ static void test_options2(void) {
char *argv[] = {
"scrcpy",
"--no-control",
"--no-mirror",
"--record", "file.mp4", // cannot enable --no-mirror without recording
"--no-playback",
"--record", "file.mp4", // cannot enable --no-playback without recording
};
bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv);
@ -117,7 +117,8 @@ static void test_options2(void) {
const struct scrcpy_options *opts = &args.opts;
assert(!opts->control);
assert(!opts->mirror);
assert(!opts->video_playback);
assert(!opts->audio_playback);
assert(!strcmp(opts->record_filename, "file.mp4"));
assert(opts->record_format == SC_RECORD_FORMAT_MP4);
}

View File

@ -16,6 +16,6 @@ cpu = 'i686'
endian = 'little'
[properties]
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win32'
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win32'
prebuilt_sdl2 = 'SDL2-2.26.4/i686-w64-mingw32'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'

View File

@ -16,6 +16,6 @@ cpu = 'x86_64'
endian = 'little'
[properties]
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win64'
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win64'
prebuilt_sdl2 = 'SDL2-2.26.4/x86_64-w64-mingw32'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'

View File

@ -24,6 +24,8 @@ To disable audio:
scrcpy --no-audio
```
To disable only the audio playback, see [no playback](video.md#no-playback).
## Audio only
To play audio only, disable the video:

View File

@ -16,13 +16,15 @@ scrcpy --no-audio --record=file.mp4
To record only the audio:
```bash
scrcpy --no-video --record=file.mp4
scrcpy --no-video --record=file.opus
scrcpy --no-video --audio-codec=aac --record-file=file.aac
# .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac
```
To disable mirroring while recording:
To disable playback while recording:
```bash
scrcpy --no-mirror --record=file.mp4
scrcpy --no-playback --record=file.mp4
scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C
```

View File

@ -35,7 +35,7 @@ To start `scrcpy` using a v4l2 sink:
```bash
scrcpy --v4l2-sink=/dev/videoN
scrcpy --v4l2-sink=/dev/videoN --no-mirror # disable mirroring window
scrcpy --v4l2-sink=/dev/videoN --no-video-playback # disable playback window
```
(replace `N` with the device ID, check with `ls /dev/video*`)

View File

@ -159,15 +159,25 @@ scrcpy --display-buffer=50 --v4l2-buffer=300
```
## No mirror
## No playback
It is possible to capture an Android device without displaying a mirroring
window. This option is available if either [recording](recording.md) or
It is possible to capture an Android device without playing video or audio on
the computer. This option is useful when [recording](recording.md) or when
[v4l2](#video4linux) is enabled:
```bash
scrcpy --v4l2-sink=/dev/video2 --no-mirror
scrcpy --record=file.mkv --no-mirror
scrcpy --v4l2-sink=/dev/video2 --no-playback
scrcpy --record=file.mkv --no-playback
```
It is also possible to disable video and audio playback separately:
```bash
# Send video to V4L2 sink without playing it, but keep audio playback
scrcpy --v4l2-sink=/dev/video2 --no-video-playback
# Record both video and audio, but only play video
scrcpy --record=file.mkv --no-audio-playback
```

View File

@ -94,11 +94,10 @@ dist-win32: build-server build-win32
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)"
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
@ -113,11 +112,10 @@ dist-win64: build-server build-win64
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"

View File

@ -0,0 +1,26 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view;
/**
* {@hide}
*/
oneway interface IDisplayFoldListener
{
/** Called when the foldedness of a display changes */
void onDisplayFoldChanged(int displayId, boolean folded);
}

View File

@ -18,10 +18,12 @@ import java.nio.ByteBuffer;
public final class AudioCapture {
public static final int SOURCE = MediaRecorder.AudioSource.REMOTE_SUBMIX;
public static final int SAMPLE_RATE = 48000;
public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
public static final int CHANNELS = 2;
public static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT;
public static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_LEFT | AudioFormat.CHANNEL_IN_RIGHT;
public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
public static final int BYTES_PER_SAMPLE = 2;
private AudioRecord recorder;
@ -36,7 +38,7 @@ public final class AudioCapture {
private static AudioFormat createAudioFormat() {
AudioFormat.Builder builder = new AudioFormat.Builder();
builder.setEncoding(FORMAT);
builder.setEncoding(ENCODING);
builder.setSampleRate(SAMPLE_RATE);
builder.setChannelMask(CHANNEL_CONFIG);
return builder.build();
@ -50,9 +52,9 @@ public final class AudioCapture {
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get());
}
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
builder.setAudioSource(SOURCE);
builder.setAudioFormat(createAudioFormat());
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT);
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING);
// This buffer size does not impact latency
builder.setBufferSizeInBytes(8 * minBufferSize);
return builder.build();
@ -97,7 +99,14 @@ public final class AudioCapture {
}
private void startRecording() {
recorder = createAudioRecord();
try {
recorder = createAudioRecord();
} catch (NullPointerException e) {
// Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones:
// - <https://github.com/Genymobile/scrcpy/issues/3805>
// - <https://github.com/Genymobile/scrcpy/pull/3862>
recorder = Workarounds.createAudioRecord(SOURCE, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING);
}
recorder.startRecording();
}

View File

@ -12,6 +12,7 @@ import android.os.Build;
import android.os.IBinder;
import android.os.SystemClock;
import android.view.IRotationWatcher;
import android.view.IDisplayFoldListener;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyCharacterMap;
@ -35,6 +36,10 @@ public final class Device {
void onRotationChanged(int rotation);
}
public interface FoldListener {
void onFoldChanged(int displayId, boolean folded);
}
public interface ClipboardListener {
void onClipboardTextChanged(String text);
}
@ -46,6 +51,7 @@ public final class Device {
private ScreenInfo screenInfo;
private RotationListener rotationListener;
private FoldListener foldListener;
private ClipboardListener clipboardListener;
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
@ -93,6 +99,26 @@ public final class Device {
}
}, displayId);
ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() {
@Override
public void onDisplayFoldChanged(int displayId, boolean folded) {
synchronized (Device.this) {
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
return;
}
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(),
options.getMaxSize(), options.getLockVideoOrientation());
// notify
if (foldListener != null) {
foldListener.onFoldChanged(displayId, folded);
}
}
}
});
if (options.getControl() && options.getClipboardAutosync()) {
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
@ -224,6 +250,10 @@ public final class Device {
this.rotationListener = rotationListener;
}
public synchronized void setFoldListener(FoldListener foldlistener) {
this.foldListener = foldlistener;
}
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
this.clipboardListener = clipboardListener;
}

View File

@ -16,7 +16,7 @@ import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
public class ScreenEncoder implements Device.RotationListener, Device.FoldListener, AsyncProcessor {
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
@ -26,7 +26,7 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
private static final int MAX_CONSECUTIVE_ERRORS = 3;
private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final AtomicBoolean resetCapture = new AtomicBoolean();
private final Device device;
private final Streamer streamer;
@ -54,12 +54,17 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
}
@Override
public void onRotationChanged(int rotation) {
rotationChanged.set(true);
public void onFoldChanged(int displayId, boolean folded) {
resetCapture.set(true);
}
private boolean consumeRotationChange() {
return rotationChanged.getAndSet(false);
@Override
public void onRotationChanged(int rotation) {
resetCapture.set(true);
}
private boolean consumeResetCapture() {
return resetCapture.getAndSet(false);
}
private void streamScreen() throws IOException, ConfigurationException {
@ -68,6 +73,7 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions);
IBinder display = createDisplay();
device.setRotationListener(this);
device.setFoldListener(this);
streamer.writeVideoHeader(device.getScreenInfo().getVideoSize());
@ -115,6 +121,7 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
} finally {
mediaCodec.release();
device.setRotationListener(null);
device.setFoldListener(null);
SurfaceControl.destroyDisplay(display);
}
}
@ -169,14 +176,14 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
boolean alive = true;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!consumeRotationChange() && !eof) {
while (!consumeResetCapture() && !eof) {
if (stopped.get()) {
alive = false;
break;
}
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
try {
if (consumeRotationChange()) {
if (consumeResetCapture()) {
// must restart encoding with new size
break;
}

View File

@ -1,13 +1,22 @@
package com.genymobile.scrcpy;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Application;
import android.content.AttributionSource;
import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.os.Build;
import android.os.Looper;
import android.os.Parcel;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public final class Workarounds {
@ -95,4 +104,140 @@ public final class Workarounds {
Ln.d("Could not fill app context: " + throwable.getMessage());
}
}
@TargetApi(Build.VERSION_CODES.R)
@SuppressLint({"WrongConstant", "MissingPermission", "BlockedPrivateApi", "SoonBlockedPrivateApi"})
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) {
// Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment.
//
// This method invokes the `AudioRecord(long nativeRecordInJavaObj)` constructor to create an empty `AudioRecord` instance, then uses
// reflections to initialize it like the normal constructor do (or the `AudioRecord.Builder.build()` method do).
// As a result, the modified code was not executed.
try {
// AudioRecord audioRecord = new AudioRecord(0L);
Constructor<AudioRecord> audioRecordConstructor = AudioRecord.class.getDeclaredConstructor(long.class);
audioRecordConstructor.setAccessible(true);
AudioRecord audioRecord = audioRecordConstructor.newInstance(0L);
// audioRecord.mRecordingState = RECORDSTATE_STOPPED;
Field mRecordingStateField = AudioRecord.class.getDeclaredField("mRecordingState");
mRecordingStateField.setAccessible(true);
mRecordingStateField.set(audioRecord, AudioRecord.RECORDSTATE_STOPPED);
Looper looper = Looper.myLooper();
if (looper == null) {
looper = Looper.getMainLooper();
}
// audioRecord.mInitializationLooper = looper;
Field mInitializationLooperField = AudioRecord.class.getDeclaredField("mInitializationLooper");
mInitializationLooperField.setAccessible(true);
mInitializationLooperField.set(audioRecord, looper);
// Create `AudioAttributes` with fixed capture preset
int capturePreset = source;
AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder();
Method setInternalCapturePresetMethod = AudioAttributes.Builder.class.getMethod("setInternalCapturePreset", int.class);
setInternalCapturePresetMethod.invoke(audioAttributesBuilder, capturePreset);
AudioAttributes attributes = audioAttributesBuilder.build();
// audioRecord.mAudioAttributes = attributes;
Field mAudioAttributesField = AudioRecord.class.getDeclaredField("mAudioAttributes");
mAudioAttributesField.setAccessible(true);
mAudioAttributesField.set(audioRecord, attributes);
// audioRecord.audioParamCheck(capturePreset, sampleRate, encoding);
Method audioParamCheckMethod = AudioRecord.class.getDeclaredMethod("audioParamCheck", int.class, int.class, int.class);
audioParamCheckMethod.setAccessible(true);
audioParamCheckMethod.invoke(audioRecord, capturePreset, sampleRate, encoding);
// audioRecord.mChannelCount = channels
Field mChannelCountField = AudioRecord.class.getDeclaredField("mChannelCount");
mChannelCountField.setAccessible(true);
mChannelCountField.set(audioRecord, channels);
// audioRecord.mChannelMask = channelMask
Field mChannelMaskField = AudioRecord.class.getDeclaredField("mChannelMask");
mChannelMaskField.setAccessible(true);
mChannelMaskField.set(audioRecord, channelMask);
int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, encoding);
int bufferSizeInBytes = minBufferSize * 8;
// audioRecord.audioBuffSizeCheck(bufferSizeInBytes)
Method audioBuffSizeCheckMethod = AudioRecord.class.getDeclaredMethod("audioBuffSizeCheck", int.class);
audioBuffSizeCheckMethod.setAccessible(true);
audioBuffSizeCheckMethod.invoke(audioRecord, bufferSizeInBytes);
final int channelIndexMask = 0;
int[] sampleRateArray = new int[]{sampleRate};
int[] session = new int[]{AudioManager.AUDIO_SESSION_ID_GENERATE};
int initResult;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
// private native final int native_setup(Object audiorecord_this,
// Object /*AudioAttributes*/ attributes,
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
// int buffSizeInBytes, int[] sessionId, String opPackageName,
// long nativeRecordInJavaObj);
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class,
int.class, int.class, int.class, int[].class, String.class, long.class);
nativeSetupMethod.setAccessible(true);
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes, sampleRateArray,
channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, FakeContext.get().getOpPackageName(),
0L);
} else {
// Assume `context` is never `null`
AttributionSource attributionSource = FakeContext.get().getAttributionSource();
// Assume `attributionSource.getPackageName()` is never null
// ScopedParcelState attributionSourceState = attributionSource.asScopedParcelState()
Method asScopedParcelStateMethod = AttributionSource.class.getDeclaredMethod("asScopedParcelState");
asScopedParcelStateMethod.setAccessible(true);
try (AutoCloseable attributionSourceState = (AutoCloseable) asScopedParcelStateMethod.invoke(attributionSource)) {
Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel");
Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState);
// private native int native_setup(Object audiorecordThis,
// Object /*AudioAttributes*/ attributes,
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
// int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource,
// long nativeRecordInJavaObj, int maxSharedAudioHistoryMs);
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class,
int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class);
nativeSetupMethod.setAccessible(true);
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes, sampleRateArray,
channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, attributionSourceParcel, 0L, 0);
}
}
if (initResult != AudioRecord.SUCCESS) {
Ln.e("Error code " + initResult + " when initializing native AudioRecord object.");
throw new RuntimeException("Cannot create AudioRecord");
}
// mSampleRate = sampleRate[0]
Field mSampleRateField = AudioRecord.class.getDeclaredField("mSampleRate");
mSampleRateField.setAccessible(true);
mSampleRateField.set(audioRecord, sampleRateArray[0]);
// audioRecord.mSessionId = session[0]
Field mSessionIdField = AudioRecord.class.getDeclaredField("mSessionId");
mSessionIdField.setAccessible(true);
mSessionIdField.set(audioRecord, session[0]);
// audioRecord.mState = AudioRecord.STATE_INITIALIZED
Field mStateField = AudioRecord.class.getDeclaredField("mState");
mStateField.setAccessible(true);
mStateField.set(audioRecord, AudioRecord.STATE_INITIALIZED);
return audioRecord;
} catch (Exception e) {
Ln.e("Failed to invoke AudioRecord.<init>.", e);
throw new RuntimeException("Cannot create AudioRecord");
}
}
}

View File

@ -4,6 +4,7 @@ import com.genymobile.scrcpy.Ln;
import android.os.IInterface;
import android.view.IRotationWatcher;
import android.view.IDisplayFoldListener;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@ -108,4 +109,13 @@ public final class WindowManager {
throw new AssertionError(e);
}
}
public void registerDisplayFoldListener(IDisplayFoldListener foldListener) {
try {
Class<?> cls = manager.getClass();
cls.getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener);
} catch (Exception e) {
throw new AssertionError(e);
}
}
}