Merge branch 'Genymobile:master' into master

This commit is contained in:
HackSys Team 2024-08-04 19:32:49 +05:30 committed by GitHub
commit 3ef6b50f10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
106 changed files with 1114 additions and 490 deletions

3
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,3 @@
github: [rom1v]
liberapay: rom1v
custom: ["https://paypal.me/rom2v"]

8
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View file

@ -0,0 +1,8 @@
---
name: Question
about: Ask a question about scrcpy
title: ''
labels: ''
assignees: ''
---

View file

@ -2,7 +2,7 @@
source for the project. Do not download releases from random websites, even if
their name contains `scrcpy`.**
# scrcpy (v2.5)
# scrcpy (v2.6.1)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@ -53,10 +53,16 @@ Make sure you [enabled USB debugging][enable-adb] on your device(s).
[enable-adb]: https://developer.android.com/studio/debug/dev-options#enable
On some devices, you also need to enable [an additional option][control] `USB
debugging (Security Settings)` (this is an item different from `USB debugging`)
to control it using a keyboard and mouse. Rebooting the device is necessary once
this option is set.
On some devices (especially Xiaomi), you might get the following error:
```
java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission.
```
In that case, you need to enable [an additional option][control] `USB debugging
(Security Settings)` (this is an item different from `USB debugging`) to control
it using a keyboard and mouse. Rebooting the device is necessary once this
option is set.
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
@ -148,11 +154,14 @@ documented in the following pages:
## Contact
If you encounter a bug, please read the [FAQ](FAQ.md) first, then open an [issue].
You can open an [issue] for bug reports, feature requests or general questions.
For bug reports, please read the [FAQ](FAQ.md) first, you might find a solution
to your problem immediately.
[issue]: https://github.com/Genymobile/scrcpy/issues
For general questions or discussions, you can also use:
You can also use:
- Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy)
- Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app)

View file

@ -6,6 +6,7 @@ _scrcpy() {
--audio-buffer=
--audio-codec=
--audio-codec-options=
--audio-dup
--audio-encoder=
--audio-source=
--audio-output-buffer=
@ -111,7 +112,7 @@ _scrcpy() {
return
;;
--audio-source)
COMPREPLY=($(compgen -W 'output mic' -- "$cur"))
COMPREPLY=($(compgen -W 'output mic playback' -- "$cur"))
return
;;
--camera-facing)

View file

@ -13,8 +13,9 @@ arguments=(
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)'
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
'--audio-dup=[Duplicate audio]'
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
'--audio-source=[Select the audio source]:source:(output mic)'
'--audio-source=[Select the audio source]:source:(output mic playback)'
'--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]'

View file

@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=2.30.4
VERSION=2.30.5
FILENAME=SDL-$VERSION.tar.gz
PROJECT_DIR=SDL-release-$VERSION
SHA256SUM=dcc2c8c9c3e9e1a7c8d61d9522f1cba4e9b740feb560dcb15234030984610ee2
SHA256SUM=be3ca88f8c362704627a0bc5406edb2cd6cc6ba463596d81ebb7c2f18763d3bf
cd "$SOURCES_DIR"

View file

@ -13,7 +13,7 @@ BEGIN
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
VALUE "OriginalFilename", "scrcpy.exe"
VALUE "ProductName", "scrcpy"
VALUE "ProductVersion", "2.5"
VALUE "ProductVersion", "2.6.1"
END
END
BLOCK "VarFileInfo"

View file

@ -49,6 +49,12 @@ The list of possible codec options is available in the Android documentation:
<https://d.android.com/reference/android/media/MediaFormat>
.TP
.B \-\-audio\-dup
Duplicate audio (capture and keep playing on the device).
This feature is only available with --audio-source=playback.
.TP
.BI "\-\-audio\-encoder " name
Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR).
@ -57,7 +63,13 @@ The available encoders can be listed by \fB\-\-list\-encoders\fR.
.TP
.BI "\-\-audio\-source " source
Select the audio source (output or mic).
Select the audio source (output, mic or playback).
The "output" source forwards the whole audio output, and disables playback on the device.
The "playback" source captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured).
The "mic" source captures the microphone.
Default is output.
@ -258,10 +270,14 @@ LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse bac
Also see \fB\-\-keyboard\fR.
.TP
.BI "\-\-mouse\-bind " xxxx
.BI "\-\-mouse\-bind " xxxx[:xxxx]
Configure bindings of secondary clicks.
The argument must be exactly 4 characters, one for each secondary click (in order: right click, middle click, 4th click, 5th click).
The argument must be one or two sequences (separated by ':') of exactly 4 characters, one for each secondary click (in order: right click, middle click, 4th click, 5th click).
The first sequence defines the primary bindings, used when a mouse button is pressed alone. The second sequence defines the secondary bindings, used when a mouse button is pressed while the Shift key is held.
If the second sequence of bindings is omitted, then it is the same as the first one.
Each character must be one of the following:
@ -272,7 +288,7 @@ Each character must be one of the following:
- 's': trigger shortcut APP_SWITCH
- 'n': trigger shortcut "expand notification panel"
Default is 'bhsn' for SDK mouse, and '++++' for AOA and UHID.
Default is 'bhsn:++++' for SDK mouse, and '++++:bhsn' for AOA and UHID.
.TP

View file

@ -100,6 +100,7 @@ enum {
OPT_NO_WINDOW,
OPT_MOUSE_BIND,
OPT_NO_MOUSE_HOVER,
OPT_AUDIO_DUP,
};
struct sc_option {
@ -177,6 +178,13 @@ static const struct sc_option options[] = {
"Android documentation: "
"<https://d.android.com/reference/android/media/MediaFormat>",
},
{
.longopt_id = OPT_AUDIO_DUP,
.longopt = "audio-dup",
.text = "Duplicate audio (capture and keep playing on the device).\n"
"This feature is only available with --audio-source=playback."
},
{
.longopt_id = OPT_AUDIO_ENCODER,
.longopt = "audio-encoder",
@ -189,7 +197,13 @@ static const struct sc_option options[] = {
.longopt_id = OPT_AUDIO_SOURCE,
.longopt = "audio-source",
.argdesc = "source",
.text = "Select the audio source (output or mic).\n"
.text = "Select the audio source (output, mic or playback).\n"
"The \"output\" source forwards the whole audio output, and "
"disables playback on the device.\n"
"The \"playback\" source captures the audio playback (Android "
"apps can opt-out, so the whole output is not necessarily "
"captured).\n"
"The \"mic\" source captures the microphone.\n"
"Default is output.",
},
{
@ -493,11 +507,17 @@ static const struct sc_option options[] = {
{
.longopt_id = OPT_MOUSE_BIND,
.longopt = "mouse-bind",
.argdesc = "xxxx",
.argdesc = "xxxx[:xxxx]",
.text = "Configure bindings of secondary clicks.\n"
"The argument must be exactly 4 characters, one for each "
"secondary click (in order: right click, middle click, 4th "
"click, 5th click).\n"
"The argument must be one or two sequences (separated by ':') "
"of exactly 4 characters, one for each secondary click (in "
"order: right click, middle click, 4th click, 5th click).\n"
"The first sequence defines the primary bindings, used when a "
"mouse button is pressed alone. The second sequence defines "
"the secondary bindings, used when a mouse button is pressed "
"while the Shift key is held.\n"
"If the second sequence of bindings is omitted, then it is the "
"same as the first one.\n"
"Each character must be one of the following:\n"
" '+': forward the click to the device\n"
" '-': ignore the click\n"
@ -505,7 +525,8 @@ static const struct sc_option options[] = {
" 'h': trigger shortcut HOME\n"
" 's': trigger shortcut APP_SWITCH\n"
" 'n': trigger shortcut \"expand notification panel\"\n"
"Default is 'bhsn' for SDK mouse, and '++++' for AOA and UHID.",
"Default is 'bhsn:++++' for SDK mouse, and '++++:bhsn' for AOA "
"and UHID.",
},
{
.shortopt = 'n',
@ -1924,7 +1945,13 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) {
return true;
}
LOGE("Unsupported audio source: %s (expected output or mic)", optarg);
if (!strcmp(optarg, "playback")) {
*source = SC_AUDIO_SOURCE_PLAYBACK;
return true;
}
LOGE("Unsupported audio source: %s (expected output, mic or playback)",
optarg);
return false;
}
@ -2095,24 +2122,46 @@ parse_mouse_binding(char c, enum sc_mouse_binding *b) {
}
static bool
parse_mouse_bindings(const char *s, struct sc_mouse_bindings *mb) {
if (strlen(s) != 4) {
LOGE("Invalid mouse bindings: '%s' (expected exactly 4 characters from "
"{'+', '-', 'b', 'h', 's', 'n'})", s);
parse_mouse_binding_set(const char *s, struct sc_mouse_binding_set *mbs) {
assert(strlen(s) >= 4);
if (!parse_mouse_binding(s[0], &mbs->right_click)) {
return false;
}
if (!parse_mouse_binding(s[1], &mbs->middle_click)) {
return false;
}
if (!parse_mouse_binding(s[2], &mbs->click4)) {
return false;
}
if (!parse_mouse_binding(s[3], &mbs->click5)) {
return false;
}
if (!parse_mouse_binding(s[0], &mb->right_click)) {
return true;
}
static bool
parse_mouse_bindings(const char *s, struct sc_mouse_bindings *mb) {
size_t len = strlen(s);
// either "xxxx" or "xxxx:xxxx"
if (len != 4 && (len != 9 || s[4] != ':')) {
LOGE("Invalid mouse bindings: '%s' (expected 'xxxx' or 'xxxx:xxxx', "
"with each 'x' being in {'+', '-', 'b', 'h', 's', 'n'})", s);
return false;
}
if (!parse_mouse_binding(s[1], &mb->middle_click)) {
if (!parse_mouse_binding_set(s, &mb->pri)) {
return false;
}
if (!parse_mouse_binding(s[2], &mb->click4)) {
return false;
}
if (!parse_mouse_binding(s[3], &mb->click5)) {
return false;
if (len == 9) {
if (!parse_mouse_binding_set(s + 5, &mb->sec)) {
return false;
}
} else {
// use the same bindings for Shift+click
mb->sec = mb->pri;
}
return true;
@ -2408,10 +2457,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
LOGW("--forward-all-clicks is deprecated, "
"use --mouse-bind=++++ instead.");
opts->mouse_bindings = (struct sc_mouse_bindings) {
.right_click = SC_MOUSE_BINDING_CLICK,
.middle_click = SC_MOUSE_BINDING_CLICK,
.click4 = SC_MOUSE_BINDING_CLICK,
.click5 = SC_MOUSE_BINDING_CLICK,
.pri = {
.right_click = SC_MOUSE_BINDING_CLICK,
.middle_click = SC_MOUSE_BINDING_CLICK,
.click4 = SC_MOUSE_BINDING_CLICK,
.click5 = SC_MOUSE_BINDING_CLICK,
},
.sec = {
.right_click = SC_MOUSE_BINDING_CLICK,
.middle_click = SC_MOUSE_BINDING_CLICK,
.click4 = SC_MOUSE_BINDING_CLICK,
.click5 = SC_MOUSE_BINDING_CLICK,
},
};
break;
case OPT_LEGACY_PASTE:
@ -2566,6 +2623,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_NO_WINDOW:
opts->window = false;
break;
case OPT_AUDIO_DUP:
opts->audio_dup = true;
break;
default:
// getopt prints the error message on stderr
return false;
@ -2701,26 +2761,36 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
// If mouse bindings are not explictly set, configure default bindings
if (opts->mouse_bindings.right_click == SC_MOUSE_BINDING_AUTO) {
assert(opts->mouse_bindings.middle_click == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.click4 == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.click5 == SC_MOUSE_BINDING_AUTO);
if (opts->mouse_bindings.pri.right_click == SC_MOUSE_BINDING_AUTO) {
assert(opts->mouse_bindings.pri.middle_click == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.pri.click4 == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.pri.click5 == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.sec.right_click == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.sec.middle_click == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.sec.click4 == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.sec.click5 == SC_MOUSE_BINDING_AUTO);
static struct sc_mouse_binding_set default_shortcuts = {
.right_click = SC_MOUSE_BINDING_BACK,
.middle_click = SC_MOUSE_BINDING_HOME,
.click4 = SC_MOUSE_BINDING_APP_SWITCH,
.click5 = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL,
};
static struct sc_mouse_binding_set forward = {
.right_click = SC_MOUSE_BINDING_CLICK,
.middle_click = SC_MOUSE_BINDING_CLICK,
.click4 = SC_MOUSE_BINDING_CLICK,
.click5 = SC_MOUSE_BINDING_CLICK,
};
// By default, forward all clicks only for UHID and AOA
if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) {
opts->mouse_bindings = (struct sc_mouse_bindings) {
.right_click = SC_MOUSE_BINDING_BACK,
.middle_click = SC_MOUSE_BINDING_HOME,
.click4 = SC_MOUSE_BINDING_APP_SWITCH,
.click5 = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL,
};
opts->mouse_bindings.pri = default_shortcuts;
opts->mouse_bindings.sec = forward;
} else {
opts->mouse_bindings = (struct sc_mouse_bindings) {
.right_click = SC_MOUSE_BINDING_CLICK,
.middle_click = SC_MOUSE_BINDING_CLICK,
.click4 = SC_MOUSE_BINDING_CLICK,
.click5 = SC_MOUSE_BINDING_CLICK,
};
opts->mouse_bindings.pri = forward;
opts->mouse_bindings.sec = default_shortcuts;
}
}
@ -2825,13 +2895,31 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) {
// Select the audio source according to the video source
if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) {
opts->audio_source = SC_AUDIO_SOURCE_OUTPUT;
if (opts->audio_dup) {
LOGI("Audio duplication enabled: audio source switched to "
"\"playback\"");
opts->audio_source = SC_AUDIO_SOURCE_PLAYBACK;
} else {
opts->audio_source = SC_AUDIO_SOURCE_OUTPUT;
}
} else {
opts->audio_source = SC_AUDIO_SOURCE_MIC;
LOGI("Camera video source: microphone audio source selected");
}
}
if (opts->audio_dup) {
if (!opts->audio) {
LOGE("--audio-dup not supported if audio is disabled");
return false;
}
if (opts->audio_source != SC_AUDIO_SOURCE_PLAYBACK) {
LOGE("--audio-dup is specific to --audio-source=playback");
return false;
}
}
if (opts->record_format && !opts->record_filename) {
LOGE("Record format specified without recording");
return false;

View file

@ -64,13 +64,11 @@ static const char *const copy_key_labels[] = {
static inline const char *
get_well_known_pointer_id_name(uint64_t pointer_id) {
switch (pointer_id) {
case POINTER_ID_MOUSE:
case SC_POINTER_ID_MOUSE:
return "mouse";
case POINTER_ID_GENERIC_FINGER:
case SC_POINTER_ID_GENERIC_FINGER:
return "finger";
case POINTER_ID_VIRTUAL_MOUSE:
return "vmouse";
case POINTER_ID_VIRTUAL_FINGER:
case SC_POINTER_ID_VIRTUAL_FINGER:
return "vfinger";
default:
return NULL;

View file

@ -18,12 +18,11 @@
// type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes
#define SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (SC_CONTROL_MSG_MAX_SIZE - 14)
#define POINTER_ID_MOUSE UINT64_C(-1)
#define POINTER_ID_GENERIC_FINGER UINT64_C(-2)
#define SC_POINTER_ID_MOUSE UINT64_C(-1)
#define SC_POINTER_ID_GENERIC_FINGER UINT64_C(-2)
// Used for injecting an additional virtual pointer for pinch-to-zoom
#define POINTER_ID_VIRTUAL_MOUSE UINT64_C(-3)
#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-4)
#define SC_POINTER_ID_VIRTUAL_FINGER UINT64_C(-3)
enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_INJECT_KEYCODE,

View file

@ -7,12 +7,13 @@
#define SC_CONTROL_MSG_QUEUE_MAX 64
static void
sc_controller_receiver_on_error(struct sc_receiver *receiver, void *userdata) {
sc_controller_receiver_on_ended(struct sc_receiver *receiver, bool error,
void *userdata) {
(void) receiver;
struct sc_controller *controller = userdata;
// Forward the event to the controller listener
controller->cbs->on_error(controller, controller->cbs_userdata);
controller->cbs->on_ended(controller, error, controller->cbs_userdata);
}
bool
@ -27,7 +28,7 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
}
static const struct sc_receiver_callbacks receiver_cbs = {
.on_error = sc_controller_receiver_on_error,
.on_ended = sc_controller_receiver_on_ended,
};
ok = sc_receiver_init(&controller->receiver, control_socket, &receiver_cbs,
@ -55,7 +56,7 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
controller->control_socket = control_socket;
controller->stopped = false;
assert(cbs && cbs->on_error);
assert(cbs && cbs->on_ended);
controller->cbs = cbs;
controller->cbs_userdata = cbs_userdata;
@ -110,21 +111,30 @@ sc_controller_push_msg(struct sc_controller *controller,
static bool
process_msg(struct sc_controller *controller,
const struct sc_control_msg *msg) {
const struct sc_control_msg *msg, bool *eos) {
static uint8_t serialized_msg[SC_CONTROL_MSG_MAX_SIZE];
size_t length = sc_control_msg_serialize(msg, serialized_msg);
if (!length) {
*eos = false;
return false;
}
ssize_t w =
net_send_all(controller->control_socket, serialized_msg, length);
return (size_t) w == length;
if ((size_t) w != length) {
*eos = true;
return false;
}
return true;
}
static int
run_controller(void *data) {
struct sc_controller *controller = data;
bool error = false;
for (;;) {
sc_mutex_lock(&controller->mutex);
while (!controller->stopped
@ -134,6 +144,7 @@ run_controller(void *data) {
if (controller->stopped) {
// stop immediately, do not process further msgs
sc_mutex_unlock(&controller->mutex);
LOGD("Controller stopped");
break;
}
@ -141,20 +152,21 @@ run_controller(void *data) {
struct sc_control_msg msg = sc_vecdeque_pop(&controller->queue);
sc_mutex_unlock(&controller->mutex);
bool ok = process_msg(controller, &msg);
bool eos;
bool ok = process_msg(controller, &msg, &eos);
sc_control_msg_destroy(&msg);
if (!ok) {
LOGD("Could not write msg to socket");
goto error;
if (eos) {
LOGD("Controller stopped (socket closed)");
} // else error already logged
error = !eos;
break;
}
}
controller->cbs->on_ended(controller, error, controller->cbs_userdata);
return 0;
error:
controller->cbs->on_error(controller, controller->cbs_userdata);
return 1; // ignored
}
bool

View file

@ -28,7 +28,8 @@ struct sc_controller {
};
struct sc_controller_callbacks {
void (*on_error)(struct sc_controller *controller, void *userdata);
void (*on_ended)(struct sc_controller *controller, bool error,
void *userdata);
};
bool

View file

@ -43,6 +43,10 @@ sc_display_init(struct sc_display *display, SDL_Window *window,
display->mipmaps = false;
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
display->gl_context = NULL;
#endif
// starts with "opengl"
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) {

View file

@ -437,25 +437,11 @@ sc_mouse_button_from_sdl(uint8_t button) {
}
static inline uint8_t
sc_mouse_buttons_state_from_sdl(uint32_t buttons_state,
const struct sc_mouse_bindings *mb) {
sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) {
assert(buttons_state < 0x100); // fits in uint8_t
uint8_t mask = SC_MOUSE_BUTTON_LEFT;
if (!mb || mb->right_click == SC_MOUSE_BINDING_CLICK) {
mask |= SC_MOUSE_BUTTON_RIGHT;
}
if (!mb || mb->middle_click == SC_MOUSE_BINDING_CLICK) {
mask |= SC_MOUSE_BUTTON_MIDDLE;
}
if (!mb || mb->click4 == SC_MOUSE_BINDING_CLICK) {
mask |= SC_MOUSE_BUTTON_X1;
}
if (!mb || mb->click5 == SC_MOUSE_BINDING_CLICK) {
mask |= SC_MOUSE_BUTTON_X2;
}
return buttons_state & mask;
// SC_MOUSE_BUTTON_* constants are initialized from SDL_BUTTON(index)
return buttons_state;
}
#endif

View file

@ -52,14 +52,6 @@ is_shortcut_key(struct sc_input_manager *im, SDL_Keycode keycode) {
|| (im->sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI);
}
static inline bool
mouse_bindings_has_secondary_click(const struct sc_mouse_bindings *mb) {
return mb->right_click == SC_MOUSE_BINDING_CLICK
|| mb->middle_click == SC_MOUSE_BINDING_CLICK
|| mb->click4 == SC_MOUSE_BINDING_CLICK
|| mb->click5 == SC_MOUSE_BINDING_CLICK;
}
void
sc_input_manager_init(struct sc_input_manager *im,
const struct sc_input_manager_params *params) {
@ -76,8 +68,6 @@ sc_input_manager_init(struct sc_input_manager *im,
im->mp = params->mp;
im->mouse_bindings = params->mouse_bindings;
im->has_secondary_click =
mouse_bindings_has_secondary_click(&im->mouse_bindings);
im->legacy_paste = params->legacy_paste;
im->clipboard_autosync = params->clipboard_autosync;
@ -87,6 +77,8 @@ sc_input_manager_init(struct sc_input_manager *im,
im->vfinger_invert_x = false;
im->vfinger_invert_y = false;
im->mouse_buttons_state = 0;
im->last_keycode = SDLK_UNKNOWN;
im->last_mod = 0;
im->key_repeat = 0;
@ -375,9 +367,7 @@ simulate_virtual_finger(struct sc_input_manager *im,
msg.inject_touch_event.action = action;
msg.inject_touch_event.position.screen_size = im->screen->frame_size;
msg.inject_touch_event.position.point = point;
msg.inject_touch_event.pointer_id =
im->has_secondary_click ? POINTER_ID_VIRTUAL_MOUSE
: POINTER_ID_VIRTUAL_FINGER;
msg.inject_touch_event.pointer_id = SC_POINTER_ID_VIRTUAL_FINGER;
msg.inject_touch_event.pressure = up ? 0.0f : 1.0f;
msg.inject_touch_event.action_button = 0;
msg.inject_touch_event.buttons = 0;
@ -662,12 +652,11 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
struct sc_mouse_motion_event evt = {
.position = sc_input_manager_get_position(im, event->x, event->y),
.pointer_id = im->has_secondary_click ? POINTER_ID_MOUSE
: POINTER_ID_GENERIC_FINGER,
.pointer_id = im->vfinger_down ? SC_POINTER_ID_GENERIC_FINGER
: SC_POINTER_ID_MOUSE,
.xrel = event->xrel,
.yrel = event->yrel,
.buttons_state =
sc_mouse_buttons_state_from_sdl(event->state, &im->mouse_bindings),
.buttons_state = im->mouse_buttons_state,
};
assert(im->mp->ops->process_mouse_motion);
@ -719,7 +708,7 @@ sc_input_manager_process_touch(struct sc_input_manager *im,
}
static enum sc_mouse_binding
sc_input_manager_get_binding(const struct sc_mouse_bindings *bindings,
sc_input_manager_get_binding(const struct sc_mouse_binding_set *bindings,
uint8_t sdl_button) {
switch (sdl_button) {
case SDL_BUTTON_LEFT:
@ -748,11 +737,25 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
bool control = im->controller;
bool paused = im->screen->paused;
bool down = event->type == SDL_MOUSEBUTTONDOWN;
enum sc_mouse_button button = sc_mouse_button_from_sdl(event->button);
if (!down) {
// Mark the button as released
im->mouse_buttons_state &= ~button;
}
SDL_Keymod keymod = SDL_GetModState();
bool ctrl_pressed = keymod & KMOD_CTRL;
bool shift_pressed = keymod & KMOD_SHIFT;
if (control && !paused) {
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
struct sc_mouse_binding_set *bindings = !shift_pressed
? &im->mouse_bindings.pri
: &im->mouse_bindings.sec;
enum sc_mouse_binding binding =
sc_input_manager_get_binding(&im->mouse_bindings, event->button);
sc_input_manager_get_binding(bindings, event->button);
assert(binding != SC_MOUSE_BINDING_AUTO);
switch (binding) {
case SC_MOUSE_BINDING_DISABLED:
@ -811,16 +814,23 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
return;
}
uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL);
if (down) {
// Mark the button as pressed
im->mouse_buttons_state |= button;
}
bool change_vfinger = event->button == SDL_BUTTON_LEFT &&
((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) ||
(!down && im->vfinger_down));
bool use_finger = im->vfinger_down || change_vfinger;
struct sc_mouse_click_event evt = {
.position = sc_input_manager_get_position(im, event->x, event->y),
.action = sc_action_from_sdl_mousebutton_type(event->type),
.button = sc_mouse_button_from_sdl(event->button),
.pointer_id = im->has_secondary_click ? POINTER_ID_MOUSE
: POINTER_ID_GENERIC_FINGER,
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state,
&im->mouse_bindings),
.pointer_id = use_finger ? SC_POINTER_ID_GENERIC_FINGER
: SC_POINTER_ID_MOUSE,
.buttons_state = im->mouse_buttons_state,
};
assert(im->mp->ops->process_mouse_click);
@ -846,14 +856,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
// can be used instead of Ctrl. The "virtual finger" has a position
// inverted with respect to the vertical axis of symmetry in the middle of
// the screen.
const SDL_Keymod keymod = SDL_GetModState();
const bool ctrl_pressed = keymod & KMOD_CTRL;
const bool shift_pressed = keymod & KMOD_SHIFT;
if (event->button == SDL_BUTTON_LEFT &&
((down && !im->vfinger_down &&
((ctrl_pressed && !shift_pressed) ||
(!ctrl_pressed && shift_pressed))) ||
(!down && im->vfinger_down))) {
if (change_vfinger) {
struct sc_point mouse =
sc_screen_convert_window_to_frame_coords(im->screen, event->x,
event->y);
@ -886,6 +889,7 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
int mouse_x;
int mouse_y;
uint32_t buttons = SDL_GetMouseState(&mouse_x, &mouse_y);
(void) buttons; // Actual buttons are tracked manually to ignore shortcuts
struct sc_mouse_scroll_event evt = {
.position = sc_input_manager_get_position(im, mouse_x, mouse_y),
@ -896,8 +900,7 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
.hscroll = CLAMP(event->x, -1, 1),
.vscroll = CLAMP(event->y, -1, 1),
#endif
.buttons_state = sc_mouse_buttons_state_from_sdl(buttons,
&im->mouse_bindings),
.buttons_state = im->mouse_buttons_state,
};
im->mp->ops->process_mouse_scroll(im->mp, &evt);

View file

@ -23,7 +23,6 @@ struct sc_input_manager {
struct sc_mouse_processor *mp;
struct sc_mouse_bindings mouse_bindings;
bool has_secondary_click;
bool legacy_paste;
bool clipboard_autosync;
@ -33,6 +32,8 @@ struct sc_input_manager {
bool vfinger_invert_x;
bool vfinger_invert_y;
uint8_t mouse_buttons_state; // OR of enum sc_mouse_button values
// Tracks the number of identical consecutive shortcut key down events.
// Not to be confused with event->repeat, which counts the number of
// system-generated repeated key presses.

View file

@ -24,10 +24,18 @@ const struct scrcpy_options scrcpy_options_default = {
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AUTO,
.mouse_input_mode = SC_MOUSE_INPUT_MODE_AUTO,
.mouse_bindings = {
.right_click = SC_MOUSE_BINDING_AUTO,
.middle_click = SC_MOUSE_BINDING_AUTO,
.click4 = SC_MOUSE_BINDING_AUTO,
.click5 = SC_MOUSE_BINDING_AUTO,
.pri = {
.right_click = SC_MOUSE_BINDING_AUTO,
.middle_click = SC_MOUSE_BINDING_AUTO,
.click4 = SC_MOUSE_BINDING_AUTO,
.click5 = SC_MOUSE_BINDING_AUTO,
},
.sec = {
.right_click = SC_MOUSE_BINDING_AUTO,
.middle_click = SC_MOUSE_BINDING_AUTO,
.click4 = SC_MOUSE_BINDING_AUTO,
.click5 = SC_MOUSE_BINDING_AUTO,
},
},
.camera_facing = SC_CAMERA_FACING_ANY,
.port_range = {
@ -93,6 +101,7 @@ const struct scrcpy_options scrcpy_options_default = {
.list = 0,
.window = true,
.mouse_hover = true,
.audio_dup = false,
};
enum sc_orientation

View file

@ -59,6 +59,7 @@ enum sc_audio_source {
SC_AUDIO_SOURCE_AUTO, // OUTPUT for video DISPLAY, MIC for video CAMERA
SC_AUDIO_SOURCE_OUTPUT,
SC_AUDIO_SOURCE_MIC,
SC_AUDIO_SOURCE_PLAYBACK,
};
enum sc_camera_facing {
@ -165,13 +166,18 @@ enum sc_mouse_binding {
SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL,
};
struct sc_mouse_bindings {
struct sc_mouse_binding_set {
enum sc_mouse_binding right_click;
enum sc_mouse_binding middle_click;
enum sc_mouse_binding click4;
enum sc_mouse_binding click5;
};
struct sc_mouse_bindings {
struct sc_mouse_binding_set pri;
struct sc_mouse_binding_set sec; // When Shift is pressed
};
enum sc_key_inject_mode {
// Inject special keys, letters and space as key events.
// Inject numbers and punctuation as text events.
@ -291,6 +297,7 @@ struct scrcpy_options {
uint8_t list;
bool window;
bool mouse_hover;
bool audio_dup;
};
extern const struct scrcpy_options scrcpy_options_default;

View file

@ -21,7 +21,7 @@ sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
receiver->acksync = NULL;
receiver->uhid_devices = NULL;
assert(cbs && cbs->on_error);
assert(cbs && cbs->on_ended);
receiver->cbs = cbs;
receiver->cbs_userdata = cbs_userdata;
@ -134,12 +134,15 @@ run_receiver(void *data) {
static uint8_t buf[DEVICE_MSG_MAX_SIZE];
size_t head = 0;
bool error = false;
for (;;) {
assert(head < DEVICE_MSG_MAX_SIZE);
ssize_t r = net_recv(receiver->control_socket, buf + head,
DEVICE_MSG_MAX_SIZE - head);
if (r <= 0) {
LOGD("Receiver stopped");
// device disconnected: keep error=false
break;
}
@ -147,6 +150,7 @@ run_receiver(void *data) {
ssize_t consumed = process_msgs(receiver, buf, head);
if (consumed == -1) {
// an error occurred
error = true;
break;
}
@ -157,7 +161,7 @@ run_receiver(void *data) {
}
}
receiver->cbs->on_error(receiver, receiver->cbs_userdata);
receiver->cbs->on_ended(receiver, error, receiver->cbs_userdata);
return 0;
}

View file

@ -25,7 +25,7 @@ struct sc_receiver {
};
struct sc_receiver_callbacks {
void (*on_error)(struct sc_receiver *receiver, void *userdata);
void (*on_ended)(struct sc_receiver *receiver, bool error, void *userdata);
};
bool

View file

@ -269,13 +269,18 @@ sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer,
}
static void
sc_controller_on_error(struct sc_controller *controller, void *userdata) {
sc_controller_on_ended(struct sc_controller *controller, bool error,
void *userdata) {
// Note: this function may be called twice, once from the controller thread
// and once from the receiver thread
(void) controller;
(void) userdata;
PUSH_EVENT(SC_EVENT_CONTROLLER_ERROR);
if (error) {
PUSH_EVENT(SC_EVENT_CONTROLLER_ERROR);
} else {
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
}
}
static void
@ -389,6 +394,7 @@ scrcpy(struct scrcpy_options *options) {
.display_id = options->display_id,
.video = options->video,
.audio = options->audio,
.audio_dup = options->audio_dup,
.show_touches = options->show_touches,
.stay_awake = options->stay_awake,
.video_codec_options = options->video_codec_options,
@ -567,7 +573,7 @@ scrcpy(struct scrcpy_options *options) {
if (options->control) {
static const struct sc_controller_callbacks controller_cbs = {
.on_error = sc_controller_on_error,
.on_ended = sc_controller_on_ended,
};
if (!sc_controller_init(&s->controller, s->server.control_socket,
@ -730,23 +736,20 @@ scrcpy(struct scrcpy_options *options) {
.start_fps_counter = options->start_fps_counter,
};
struct sc_frame_source *src;
if (options->video_playback) {
src = &s->video_decoder.frame_source;
if (options->display_buffer) {
sc_delay_buffer_init(&s->display_buffer,
options->display_buffer, true);
sc_frame_source_add_sink(src, &s->display_buffer.frame_sink);
src = &s->display_buffer.frame_source;
}
}
if (!sc_screen_init(&s->screen, &screen_params)) {
goto end;
}
screen_initialized = true;
if (options->video_playback) {
struct sc_frame_source *src = &s->video_decoder.frame_source;
if (options->display_buffer) {
sc_delay_buffer_init(&s->display_buffer,
options->display_buffer, true);
sc_frame_source_add_sink(src, &s->display_buffer.frame_sink);
src = &s->display_buffer.frame_source;
}
sc_frame_source_add_sink(src, &s->screen.frame_sink);
}
}

View file

@ -147,7 +147,7 @@ log_level_to_server_string(enum sc_log_level level) {
return "error";
default:
assert(!"unexpected log level");
return "(unknown)";
return NULL;
}
}
@ -183,6 +183,7 @@ sc_server_get_codec_name(enum sc_codec codec) {
case SC_CODEC_RAW:
return "raw";
default:
assert(!"unexpected codec");
return NULL;
}
}
@ -197,6 +198,22 @@ sc_server_get_camera_facing_name(enum sc_camera_facing camera_facing) {
case SC_CAMERA_FACING_EXTERNAL:
return "external";
default:
assert(!"unexpected camera facing");
return NULL;
}
}
static const char *
sc_server_get_audio_source_name(enum sc_audio_source audio_source) {
switch (audio_source) {
case SC_AUDIO_SOURCE_OUTPUT:
return "output";
case SC_AUDIO_SOURCE_MIC:
return "mic";
case SC_AUDIO_SOURCE_PLAYBACK:
return "playback";
default:
assert(!"unexpected audio source");
return NULL;
}
}
@ -271,8 +288,14 @@ execute_server(struct sc_server *server,
assert(params->video_source == SC_VIDEO_SOURCE_CAMERA);
ADD_PARAM("video_source=camera");
}
if (params->audio_source == SC_AUDIO_SOURCE_MIC) {
ADD_PARAM("audio_source=mic");
// If audio is enabled, an "auto" audio source must have been resolved
assert(params->audio_source != SC_AUDIO_SOURCE_AUTO || !params->audio);
if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT && params->audio) {
ADD_PARAM("audio_source=%s",
sc_server_get_audio_source_name(params->audio_source));
}
if (params->audio_dup) {
ADD_PARAM("audio_dup=true");
}
if (params->max_size) {
ADD_PARAM("max_size=%" PRIu16, params->max_size);

View file

@ -50,6 +50,7 @@ struct sc_server_params {
uint32_t display_id;
bool video;
bool audio;
bool audio_dup;
bool show_touches;
bool stay_awake;
bool force_adb_forward;

View file

@ -169,7 +169,7 @@ sc_screen_otg_process_mouse_motion(struct sc_screen_otg *screen,
// .position not used for HID events
.xrel = event->xrel,
.yrel = event->yrel,
.buttons_state = sc_mouse_buttons_state_from_sdl(event->state, NULL),
.buttons_state = sc_mouse_buttons_state_from_sdl(event->state),
};
assert(mp->ops->process_mouse_motion);
@ -188,8 +188,7 @@ sc_screen_otg_process_mouse_button(struct sc_screen_otg *screen,
// .position not used for HID events
.action = sc_action_from_sdl_mousebutton_type(event->type),
.button = sc_mouse_button_from_sdl(event->button),
.buttons_state =
sc_mouse_buttons_state_from_sdl(sdl_buttons_state, NULL),
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state),
};
assert(mp->ops->process_mouse_click);
@ -208,8 +207,7 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen,
// .position not used for HID events
.hscroll = event->x,
.vscroll = event->y,
.buttons_state =
sc_mouse_buttons_state_from_sdl(sdl_buttons_state, NULL),
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state),
};
assert(mp->ops->process_mouse_scroll);

View file

@ -66,6 +66,30 @@ the computer:
scrcpy --audio-source=mic --no-video --no-playback --record=file.opus
```
### Duplication
An alternative device audio capture method is also available (only for Android
13 and above):
```
scrcpy --audio-source=playback
```
This audio source supports keeping the audio playing on the device while
mirroring, with `--audio-dup`:
```bash
scrcpy --audio-source=playback --audio-dup
# or simply:
scrcpy --audio-dup # --audio-source=playback is implied
```
However, it requires Android 13, and Android apps can opt-out (so they are not
captured).
See [#4380](https://github.com/Genymobile/scrcpy/issues/4380).
## Codec

View file

@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server
- [`scrcpy-server-v2.5`][direct-scrcpy-server]
<sub>SHA-256: `1488b1105d6aff534873a26bf610cd2aea06ee867dd7a4d9c6bb2c091396eb15`</sub>
- [`scrcpy-server-v2.6.1`][direct-scrcpy-server]
<sub>SHA-256: `ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b`</sub>
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-server-v2.5
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-server-v2.6.1
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:

View file

@ -80,21 +80,37 @@ process like the _adb daemon_).
## Mouse bindings
By default, with SDK mouse, right-click triggers BACK (or POWER on) and
middle-click triggers HOME. In addition, the 4th click triggers APP_SWITCH and
the 5th click expands the notification panel.
By default, with SDK mouse:
- right-click triggers BACK (or POWER on)
- middle-click triggers HOME
- the 4th click triggers APP_SWITCH
- the 5th click expands the notification panel
In AOA and UHID mouse modes, all clicks are forwarded by default.
The secondary clicks may be forwarded to the device instead by pressing the
<kbd>Shift</kbd> key (e.g. <kbd>Shift</kbd>+right-click injects a right click to
the device).
The shortcuts can be configured using `--mouse-bind=xxxx` for any mouse mode.
The argument must be exactly 4 characters, one for each secondary click:
In AOA and UHID mouse modes, the default bindings are reversed: all clicks are
forwarded by default, and pressing <kbd>Shift</kbd> gives access to the
shortcuts (since the cursor is handled on the device side, it makes more sense
to forward all mouse buttons by default in these modes).
The shortcuts can be configured using `--mouse-bind=xxxx:xxxx` for any mouse
mode. The argument must be one or two sequences (separated by `:`) of exactly 4
characters, one for each secondary click:
```
--mouse-bind=xxxx
.---- Shift + right click
SECONDARY |.--- Shift + middle click
BINDINGS ||.-- Shift + 4th click
|||.- Shift + 5th click
||||
vvvv
--mouse-bind=xxxx:xxxx
^^^^
||||
||| `- 5th click
|| `-- 4th click
PRIMARY ||| `- 5th click
BINDINGS || `-- 4th click
| `--- middle click
`---- right click
```
@ -111,8 +127,18 @@ Each character must be one of the following:
For example:
```bash
scrcpy --mouse-bind=bhsn # the default mode with SDK mouse
scrcpy --mouse-bind=++++ # forward all clicks (default for AOA/UHID)
scrcpy --mouse-bind=++bh # forward right and middle clicks,
# use 4th and 5th for BACK and HOME
scrcpy --mouse-bind=bhsn:++++ # the default mode for SDK mouse
scrcpy --mouse-bind=++++:bhsn # the default mode for AOA and UHID
scrcpy --mouse-bind=++bh:++sn # forward right and middle clicks,
# use 4th and 5th for BACK and HOME,
# use Shift+4th and Shift+5th for APP_SWITCH
# and expand notification panel
```
The second sequence of bindings may be omitted. In that case, it is the same as
the first one:
```bash
scrcpy --mouse-bind=bhsn
scrcpy --mouse-bind=bhsn:bhsn # equivalent
```

View file

@ -4,14 +4,14 @@
Download the [latest release]:
- [`scrcpy-win64-v2.5.zip`][direct-win64] (64-bit)
<sub>SHA-256: `345cf04a66a9144281dce72ca4e82adfd2c3092463196e586051df4c69e1507b`</sub>
- [`scrcpy-win32-v2.5.zip`][direct-win32] (32-bit)
<sub>SHA-256: `d56312a92471565fa4f3a6b94e8eb07717c4c90f2c0f05b03ba444e1001806ec`</sub>
- [`scrcpy-win64-v2.6.1.zip`][direct-win64] (64-bit)
<sub>SHA-256: `041fc3abf8578ddcead5a8c4a8be8960b7c4d45b21d3370ee2683605e86a728c`</sub>
- [`scrcpy-win32-v2.6.1.zip`][direct-win32] (32-bit)
<sub>SHA-256: `17a5d4d17230b4c90fad45af6395efda9aea287a03c04e6b4ecc9ceb8134ea04`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-win64-v2.5.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-win32-v2.5.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-win64-v2.6.1.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-win32-v2.6.1.zip
and extract it.

View file

@ -2,8 +2,8 @@
set -e
BUILDDIR=build-auto
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-server-v2.5
PREBUILT_SERVER_SHA256=1488b1105d6aff534873a26bf610cd2aea06ee867dd7a4d9c6bb2c091396eb15
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-server-v2.6.1
PREBUILT_SERVER_SHA256=ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b
echo "[scrcpy] Downloading prebuilt server..."
wget "$PREBUILT_SERVER_URL" -O scrcpy-server

View file

@ -1,5 +1,5 @@
project('scrcpy', 'c',
version: '2.5',
version: '2.6.1',
meson_version: '>= 0.48',
default_options: [
'c_std=c11',

View file

@ -24,7 +24,7 @@ SERVER_BUILD_DIR := build-server
WIN32_BUILD_DIR := build-win32
WIN64_BUILD_DIR := build-win64
VERSION := $(shell git describe --tags --always)
VERSION := $(shell git describe --tags --exclude='*install-release' --always)
DIST := dist
WIN32_TARGET_DIR := scrcpy-win32-$(VERSION)

View file

@ -7,8 +7,8 @@ android {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 34
versionCode 20500
versionName "2.5"
versionCode 20601
versionName "2.6.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {

View file

@ -12,7 +12,7 @@
set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=2.5
SCRCPY_VERSION_NAME=2.6.1
PLATFORM=${ANDROID_PLATFORM:-34}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0}
@ -50,14 +50,29 @@ cd "$SERVER_DIR/src/main/aidl"
android/content/IOnPrimaryClipChangedListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IDisplayFoldListener.aidl
SRC=( \
com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/audio/*.java \
com/genymobile/scrcpy/control/*.java \
com/genymobile/scrcpy/device/*.java \
com/genymobile/scrcpy/util/*.java \
com/genymobile/scrcpy/video/*.java \
com/genymobile/scrcpy/wrappers/*.java \
)
CLASSES=()
for src in "${SRC[@]}"
do
CLASSES+=("${src%.java}.class")
done
echo "Compiling java sources..."
cd ../java
javac -bootclasspath "$ANDROID_JAR" \
-cp "$LAMBDA_JAR:$GEN_DIR" \
-d "$CLASSES_DIR" \
-source 1.8 -target 1.8 \
com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/wrappers/*.java
${SRC[@]}
echo "Dexing..."
cd "$CLASSES_DIR"
@ -68,8 +83,7 @@ then
"$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \
android/view/*.class \
android/content/*.class \
com/genymobile/scrcpy/*.class \
com/genymobile/scrcpy/wrappers/*.class
${CLASSES[@]}
echo "Archiving..."
cd "$BUILD_DIR"
@ -81,8 +95,7 @@ else
--output "$BUILD_DIR/classes.zip" \
android/view/*.class \
android/content/*.class \
com/genymobile/scrcpy/*.class \
com/genymobile/scrcpy/wrappers/*.class
${CLASSES[@]}
cd "$BUILD_DIR"
mv classes.zip "$SERVER_BINARY"

View file

@ -1,7 +0,0 @@
package com.genymobile.scrcpy;
/**
* Exception thrown if audio capture failed on Android 11 specifically because the running App (shell) was not in foreground.
*/
public class AudioCaptureForegroundException extends Exception {
}

View file

@ -1,30 +0,0 @@
package com.genymobile.scrcpy;
import android.media.MediaRecorder;
public enum AudioSource {
OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX),
MIC("mic", MediaRecorder.AudioSource.MIC);
private final String name;
private final int value;
AudioSource(String name, int value) {
this.name = name;
this.value = value;
}
int value() {
return value;
}
static AudioSource findByName(String name) {
for (AudioSource audioSource : AudioSource.values()) {
if (name.equals(audioSource.name)) {
return audioSource;
}
}
return null;
}
}

View file

@ -1,5 +1,10 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;

View file

@ -1,5 +1,15 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.audio.AudioSource;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.CodecOption;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.video.CameraAspectRatio;
import com.genymobile.scrcpy.video.CameraFacing;
import com.genymobile.scrcpy.video.VideoCodec;
import com.genymobile.scrcpy.video.VideoSource;
import android.graphics.Rect;
import java.util.List;
@ -16,6 +26,7 @@ public class Options {
private AudioCodec audioCodec = AudioCodec.OPUS;
private VideoSource videoSource = VideoSource.DISPLAY;
private AudioSource audioSource = AudioSource.OUTPUT;
private boolean audioDup;
private int videoBitRate = 8000000;
private int audioBitRate = 128000;
private int maxFps;
@ -90,6 +101,10 @@ public class Options {
return audioSource;
}
public boolean getAudioDup() {
return audioDup;
}
public int getVideoBitRate() {
return videoBitRate;
}
@ -293,6 +308,9 @@ public class Options {
}
options.audioSource = audioSource;
break;
case "audio_dup":
options.audioDup = Boolean.parseBoolean(value);
break;
case "max_size":
options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8
break;

View file

@ -1,5 +1,29 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.audio.AudioCapture;
import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.audio.AudioDirectCapture;
import com.genymobile.scrcpy.audio.AudioEncoder;
import com.genymobile.scrcpy.audio.AudioPlaybackCapture;
import com.genymobile.scrcpy.audio.AudioRawRecorder;
import com.genymobile.scrcpy.audio.AudioSource;
import com.genymobile.scrcpy.control.ControlChannel;
import com.genymobile.scrcpy.control.Controller;
import com.genymobile.scrcpy.control.DeviceMessage;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.DesktopConnection;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException;
import com.genymobile.scrcpy.video.CameraCapture;
import com.genymobile.scrcpy.video.ScreenCapture;
import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.SurfaceEncoder;
import com.genymobile.scrcpy.video.VideoSource;
import android.os.BatteryManager;
import android.os.Build;
@ -120,7 +144,7 @@ public final class Server {
final Device device = camera ? null : new Device(options);
Workarounds.apply(audio, camera);
Workarounds.apply();
List<AsyncProcessor> asyncProcessors = new ArrayList<>();
@ -142,7 +166,14 @@ public final class Server {
if (audio) {
AudioCodec audioCodec = options.getAudioCodec();
AudioCapture audioCapture = new AudioCapture(options.getAudioSource());
AudioSource audioSource = options.getAudioSource();
AudioCapture audioCapture;
if (audioSource.isDirect()) {
audioCapture = new AudioDirectCapture(audioSource);
} else {
audioCapture = new AudioPlaybackCapture(options.getAudioDup());
}
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta());
AsyncProcessor audioRecorder;
if (audioCodec == AudioCodec.RAW) {
@ -248,7 +279,7 @@ public final class Server {
Ln.i(LogUtils.buildDisplayListMessage());
}
if (options.getListCameras() || options.getListCameraSizes()) {
Workarounds.apply(false, true);
Workarounds.apply();
Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes()));
}
// Just print the requested data, do not mirror

View file

@ -1,5 +1,8 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.audio.AudioCaptureException;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Application;
@ -48,62 +51,18 @@ public final class Workarounds {
// not instantiable
}
public static void apply(boolean audio, boolean camera) {
boolean mustFillConfigurationController = false;
boolean mustFillAppInfo = false;
boolean mustFillAppContext = false;
if (Build.BRAND.equalsIgnoreCase("meizu")) {
// Workarounds must be applied for Meizu phones:
// - <https://github.com/Genymobile/scrcpy/issues/240>
// - <https://github.com/Genymobile/scrcpy/issues/365>
// - <https://github.com/Genymobile/scrcpy/issues/2656>
//
// But only apply when strictly necessary, since workarounds can cause other issues:
// - <https://github.com/Genymobile/scrcpy/issues/940>
// - <https://github.com/Genymobile/scrcpy/issues/994>
mustFillAppInfo = true;
} else if (Build.BRAND.equalsIgnoreCase("honor")) {
// More workarounds must be applied for Honor devices:
// - <https://github.com/Genymobile/scrcpy/issues/4015>
//
// The system context must not be set for all devices, because it would cause other problems:
// - <https://github.com/Genymobile/scrcpy/issues/4015#issuecomment-1595382142>
// - <https://github.com/Genymobile/scrcpy/issues/3805#issuecomment-1596148031>
mustFillAppInfo = true;
mustFillAppContext = true;
}
if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
// Before Android 11, audio is not supported.
// Since Android 12, we can properly set a context on the AudioRecord.
// Only on Android 11 we must fill the application context for the AudioRecord to work.
mustFillAppContext = true;
}
if (camera) {
mustFillAppInfo = true;
mustFillAppContext = true;
}
public static void apply() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(),
// which requires a non-null ConfigurationController.
// ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions.
// <https://github.com/Genymobile/scrcpy/issues/4467>
mustFillConfigurationController = true;
}
if (mustFillConfigurationController) {
// Must be call before fillAppContext() because it is necessary to get a valid system context
// Must be called before fillAppContext() because it is necessary to get a valid system context.
fillConfigurationController();
}
if (mustFillAppInfo) {
fillAppInfo();
}
if (mustFillAppContext) {
fillAppContext();
}
fillAppInfo();
fillAppContext();
}
@SuppressWarnings("deprecation")
@ -191,7 +150,8 @@ public final class Workarounds {
@TargetApi(Build.VERSION_CODES.R)
@SuppressLint("WrongConstant,MissingPermission")
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) {
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) throws
AudioCaptureException {
// 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
@ -332,8 +292,8 @@ public final class Workarounds {
return audioRecord;
} catch (Exception e) {
Ln.e("Failed to invoke AudioRecord.<init>.", e);
throw new RuntimeException("Cannot create AudioRecord");
Ln.e("Cannot create AudioRecord", e);
throw new AudioCaptureException();
}
}
}

View file

@ -0,0 +1,20 @@
package com.genymobile.scrcpy.audio;
import android.media.MediaCodec;
import java.nio.ByteBuffer;
public interface AudioCapture {
void checkCompatibility() throws AudioCaptureException;
void start() throws AudioCaptureException;
void stop();
/**
* Read a chunk of {@link AudioConfig#MAX_READ_SIZE} samples.
*
* @param outDirectBuffer The target buffer
* @param outBufferInfo The info to provide to MediaCodec
* @return the number of bytes actually read.
*/
int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo);
}

View file

@ -0,0 +1,12 @@
package com.genymobile.scrcpy.audio;
/**
* Exception for any audio capture issue.
* <p/>
* This includes the case where audio capture failed on Android 11 specifically because the running App (Shell) was not in foreground.
* <p/>
* Its purpose is to disable audio without errors (that's why the exception is empty, any error message must be printed by the caller before
* throwing the exception).
*/
public class AudioCaptureException extends Exception {
}

View file

@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.util.Codec;
import android.media.MediaFormat;

View file

@ -0,0 +1,29 @@
package com.genymobile.scrcpy.audio;
import android.media.AudioFormat;
public final class AudioConfig {
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 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;
// Never read more than 1024 samples, even if the buffer is bigger (that would increase latency).
// A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we
// receive 4 successive blocks without waiting, then we wait for the 4 next ones).
public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE;
private AudioConfig() {
// Not instantiable
}
public static AudioFormat createAudioFormat() {
AudioFormat.Builder builder = new AudioFormat.Builder();
builder.setEncoding(ENCODING);
builder.setSampleRate(SAMPLE_RATE);
builder.setChannelMask(CHANNEL_CONFIG);
return builder.build();
}
}

View file

@ -1,55 +1,48 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Workarounds;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Intent;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
import android.media.MediaCodec;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.SystemClock;
import java.nio.ByteBuffer;
public final class AudioCapture {
public class AudioDirectCapture implements AudioCapture {
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 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;
// Never read more than 1024 samples, even if the buffer is bigger (that would increase latency).
// A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we
// receive 4 successive blocks without waiting, then we wait for the 4 next ones).
public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE;
private static final long ONE_SAMPLE_US = (1000000 + SAMPLE_RATE - 1) / SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS)
private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE;
private static final int CHANNEL_CONFIG = AudioConfig.CHANNEL_CONFIG;
private static final int CHANNELS = AudioConfig.CHANNELS;
private static final int CHANNEL_MASK = AudioConfig.CHANNEL_MASK;
private static final int ENCODING = AudioConfig.ENCODING;
private final int audioSource;
private AudioRecord recorder;
private AudioRecordReader reader;
private final AudioTimestamp timestamp = new AudioTimestamp();
private long previousRecorderTimestamp = -1;
private long previousPts = 0;
private long nextPts = 0;
public AudioCapture(AudioSource audioSource) {
this.audioSource = audioSource.value();
public AudioDirectCapture(AudioSource audioSource) {
this.audioSource = getAudioSourceValue(audioSource);
}
private static AudioFormat createAudioFormat() {
AudioFormat.Builder builder = new AudioFormat.Builder();
builder.setEncoding(ENCODING);
builder.setSampleRate(SAMPLE_RATE);
builder.setChannelMask(CHANNEL_CONFIG);
return builder.build();
private static int getAudioSourceValue(AudioSource audioSource) {
switch (audioSource) {
case OUTPUT:
return MediaRecorder.AudioSource.REMOTE_SUBMIX;
case MIC:
return MediaRecorder.AudioSource.MIC;
default:
throw new IllegalArgumentException("Unsupported audio source: " + audioSource);
}
}
@TargetApi(Build.VERSION_CODES.M)
@ -61,7 +54,7 @@ public final class AudioCapture {
builder.setContext(FakeContext.get());
}
builder.setAudioSource(audioSource);
builder.setAudioFormat(createAudioFormat());
builder.setAudioFormat(AudioConfig.createAudioFormat());
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING);
// This buffer size does not impact latency
builder.setBufferSizeInBytes(8 * minBufferSize);
@ -86,7 +79,7 @@ public final class AudioCapture {
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
}
private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureForegroundException {
private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureException {
while (attempts-- > 0) {
// Wait for activity to start
SystemClock.sleep(delayMs);
@ -98,7 +91,7 @@ public final class AudioCapture {
Ln.e("Failed to start audio capture");
Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting "
+ "scrcpy.");
throw new AudioCaptureForegroundException();
throw new AudioCaptureException();
} else {
Ln.d("Failed to start audio capture, retrying...");
}
@ -106,7 +99,7 @@ public final class AudioCapture {
}
}
private void startRecording() {
private void startRecording() throws AudioCaptureException {
try {
recorder = createAudioRecord(audioSource);
} catch (NullPointerException e) {
@ -116,9 +109,19 @@ public final class AudioCapture {
recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING);
}
recorder.startRecording();
reader = new AudioRecordReader(recorder);
}
public void start() throws AudioCaptureForegroundException {
@Override
public void checkCompatibility() throws AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
throw new AudioCaptureException();
}
}
@Override
public void start() throws AudioCaptureException {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
startWorkaroundAndroid11();
try {
@ -131,6 +134,7 @@ public final class AudioCapture {
}
}
@Override
public void stop() {
if (recorder != null) {
// Will call .stop() if necessary, without throwing an IllegalStateException
@ -138,42 +142,9 @@ public final class AudioCapture {
}
}
@Override
@TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer directBuffer, MediaCodec.BufferInfo outBufferInfo) {
int r = recorder.read(directBuffer, MAX_READ_SIZE);
if (r <= 0) {
return r;
}
long pts;
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) {
pts = timestamp.nanoTime / 1000;
previousRecorderTimestamp = timestamp.nanoTime;
} else {
if (nextPts == 0) {
Ln.w("Could not get initial audio timestamp");
nextPts = System.nanoTime() / 1000;
}
// compute from previous timestamp and packet size
pts = nextPts;
}
long durationUs = r * 1000000L / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE);
nextPts = pts + durationUs;
if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) {
// Audio PTS may come from two sources:
// - recorder.getTimestamp() if the call works;
// - an estimation from the previous PTS and the packet size as a fallback.
//
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
pts = previousPts + ONE_SAMPLE_US;
}
previousPts = pts;
outBufferInfo.set(0, r, pts, 0);
return r;
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
return reader.read(outDirectBuffer, outBufferInfo);
}
}

View file

@ -1,4 +1,14 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.util.Codec;
import com.genymobile.scrcpy.util.CodecOption;
import com.genymobile.scrcpy.util.CodecUtils;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.util.IO;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.device.Streamer;
import android.annotation.TargetApi;
import android.media.MediaCodec;
@ -34,8 +44,8 @@ public final class AudioEncoder implements AsyncProcessor {
}
}
private static final int SAMPLE_RATE = AudioCapture.SAMPLE_RATE;
private static final int CHANNELS = AudioCapture.CHANNELS;
private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE;
private static final int CHANNELS = AudioConfig.CHANNELS;
private final AudioCapture capture;
private final Streamer streamer;
@ -122,7 +132,7 @@ public final class AudioEncoder implements AsyncProcessor {
} catch (ConfigurationException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
fatalError = true;
} catch (AudioCaptureForegroundException e) {
} catch (AudioCaptureException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
} catch (IOException e) {
Ln.e("Audio encoding error", e);
@ -166,7 +176,7 @@ public final class AudioEncoder implements AsyncProcessor {
}
@TargetApi(Build.VERSION_CODES.M)
public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException {
private void encode() throws IOException, ConfigurationException, AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
@ -177,6 +187,8 @@ public final class AudioEncoder implements AsyncProcessor {
boolean mediaCodecStarted = false;
try {
capture.checkCompatibility(); // throws an AudioCaptureException on error
Codec codec = streamer.getCodec();
mediaCodec = createMediaCodec(codec, encoderName);

View file

@ -0,0 +1,137 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaCodec;
import android.os.Build;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
public final class AudioPlaybackCapture implements AudioCapture {
private final boolean keepPlayingOnDevice;
private AudioRecord recorder;
private AudioRecordReader reader;
public AudioPlaybackCapture(boolean keepPlayingOnDevice) {
this.keepPlayingOnDevice = keepPlayingOnDevice;
}
@SuppressLint("PrivateApi")
private AudioRecord createAudioRecord() throws AudioCaptureException {
// See <https://github.com/Genymobile/scrcpy/issues/4380>
try {
Class<?> audioMixingRuleClass = Class.forName("android.media.audiopolicy.AudioMixingRule");
Class<?> audioMixingRuleBuilderClass = Class.forName("android.media.audiopolicy.AudioMixingRule$Builder");
// AudioMixingRule.Builder audioMixingRuleBuilder = new AudioMixingRule.Builder();
Object audioMixingRuleBuilder = audioMixingRuleBuilderClass.getConstructor().newInstance();
// audioMixingRuleBuilder.setTargetMixRole(AudioMixingRule.MIX_ROLE_PLAYERS);
int mixRolePlayersConstant = audioMixingRuleClass.getField("MIX_ROLE_PLAYERS").getInt(null);
Method setTargetMixRoleMethod = audioMixingRuleBuilderClass.getMethod("setTargetMixRole", int.class);
setTargetMixRoleMethod.invoke(audioMixingRuleBuilder, mixRolePlayersConstant);
AudioAttributes attributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
// audioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE, attributes);
int ruleMatchAttributeUsageConstant = audioMixingRuleClass.getField("RULE_MATCH_ATTRIBUTE_USAGE").getInt(null);
Method addMixRuleMethod = audioMixingRuleBuilderClass.getMethod("addMixRule", int.class, Object.class);
addMixRuleMethod.invoke(audioMixingRuleBuilder, ruleMatchAttributeUsageConstant, attributes);
// AudioMixingRule audioMixingRule = builder.build();
Object audioMixingRule = audioMixingRuleBuilderClass.getMethod("build").invoke(audioMixingRuleBuilder);
// audioMixingRuleBuilder.voiceCommunicationCaptureAllowed(true);
Method voiceCommunicationCaptureAllowedMethod = audioMixingRuleBuilderClass.getMethod("voiceCommunicationCaptureAllowed", boolean.class);
voiceCommunicationCaptureAllowedMethod.invoke(audioMixingRuleBuilder, true);
Class<?> audioMixClass = Class.forName("android.media.audiopolicy.AudioMix");
Class<?> audioMixBuilderClass = Class.forName("android.media.audiopolicy.AudioMix$Builder");
// AudioMix.Builder audioMixBuilder = new AudioMix.Builder(audioMixingRule);
Object audioMixBuilder = audioMixBuilderClass.getConstructor(audioMixingRuleClass).newInstance(audioMixingRule);
// audioMixBuilder.setFormat(createAudioFormat());
Method setFormat = audioMixBuilder.getClass().getMethod("setFormat", AudioFormat.class);
setFormat.invoke(audioMixBuilder, AudioConfig.createAudioFormat());
String routeFlagName = keepPlayingOnDevice ? "ROUTE_FLAG_LOOP_BACK_RENDER" : "ROUTE_FLAG_LOOP_BACK";
int routeFlags = audioMixClass.getField(routeFlagName).getInt(null);
// audioMixBuilder.setRouteFlags(routeFlag);
Method setRouteFlags = audioMixBuilder.getClass().getMethod("setRouteFlags", int.class);
setRouteFlags.invoke(audioMixBuilder, routeFlags);
// AudioMix audioMix = audioMixBuilder.build();
Object audioMix = audioMixBuilderClass.getMethod("build").invoke(audioMixBuilder);
Class<?> audioPolicyClass = Class.forName("android.media.audiopolicy.AudioPolicy");
Class<?> audioPolicyBuilderClass = Class.forName("android.media.audiopolicy.AudioPolicy$Builder");
// AudioPolicy.Builder audioPolicyBuilder = new AudioPolicy.Builder();
Object audioPolicyBuilder = audioPolicyBuilderClass.getConstructor(Context.class).newInstance(FakeContext.get());
// audioPolicyBuilder.addMix(audioMix);
Method addMixMethod = audioPolicyBuilderClass.getMethod("addMix", audioMixClass);
addMixMethod.invoke(audioPolicyBuilder, audioMix);
// AudioPolicy audioPolicy = audioPolicyBuilder.build();
Object audioPolicy = audioPolicyBuilderClass.getMethod("build").invoke(audioPolicyBuilder);
// AudioManager.registerAudioPolicyStatic(audioPolicy);
Method registerAudioPolicyStaticMethod = AudioManager.class.getDeclaredMethod("registerAudioPolicyStatic", audioPolicyClass);
registerAudioPolicyStaticMethod.setAccessible(true);
int result = (int) registerAudioPolicyStaticMethod.invoke(null, audioPolicy);
if (result != 0) {
throw new RuntimeException("registerAudioPolicy() returned " + result);
}
// audioPolicy.createAudioRecordSink(audioPolicy);
Method createAudioRecordSinkClass = audioPolicyClass.getMethod("createAudioRecordSink", audioMixClass);
return (AudioRecord) createAudioRecordSinkClass.invoke(audioPolicy, audioMix);
} catch (Exception e) {
Ln.e("Could not capture audio playback", e);
throw new AudioCaptureException();
}
}
@Override
public void checkCompatibility() throws AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
Ln.w("Audio disabled: audio playback capture source not supported before Android 13");
throw new AudioCaptureException();
}
}
@Override
public void start() throws AudioCaptureException {
recorder = createAudioRecord();
recorder.startRecording();
reader = new AudioRecordReader(recorder);
}
@Override
public void stop() {
if (recorder != null) {
// Will call .stop() if necessary, without throwing an IllegalStateException
recorder.release();
}
}
@Override
@TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
return reader.read(outDirectBuffer, outBufferInfo);
}
}

View file

@ -1,4 +1,9 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.util.IO;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Streamer;
import android.media.MediaCodec;
import android.os.Build;
@ -18,14 +23,14 @@ public final class AudioRawRecorder implements AsyncProcessor {
this.streamer = streamer;
}
private void record() throws IOException, AudioCaptureForegroundException {
private void record() throws IOException, AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
return;
}
final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioCapture.MAX_READ_SIZE);
final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioConfig.MAX_READ_SIZE);
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
try {
@ -64,7 +69,7 @@ public final class AudioRawRecorder implements AsyncProcessor {
boolean fatalError = false;
try {
record();
} catch (AudioCaptureForegroundException e) {
} catch (AudioCaptureException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
} catch (Throwable t) {
Ln.e("Audio recording error", t);

View file

@ -0,0 +1,67 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.TargetApi;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
import android.media.MediaCodec;
import android.os.Build;
import java.nio.ByteBuffer;
public class AudioRecordReader {
private static final long ONE_SAMPLE_US =
(1000000 + AudioConfig.SAMPLE_RATE - 1) / AudioConfig.SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS)
private final AudioRecord recorder;
private final AudioTimestamp timestamp = new AudioTimestamp();
private long previousRecorderTimestamp = -1;
private long previousPts = 0;
private long nextPts = 0;
public AudioRecordReader(AudioRecord recorder) {
this.recorder = recorder;
}
@TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
int r = recorder.read(outDirectBuffer, AudioConfig.MAX_READ_SIZE);
if (r <= 0) {
return r;
}
long pts;
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) {
pts = timestamp.nanoTime / 1000;
previousRecorderTimestamp = timestamp.nanoTime;
} else {
if (nextPts == 0) {
Ln.w("Could not get initial audio timestamp");
nextPts = System.nanoTime() / 1000;
}
// compute from previous timestamp and packet size
pts = nextPts;
}
long durationUs = r * 1000000L / (AudioConfig.CHANNELS * AudioConfig.BYTES_PER_SAMPLE * AudioConfig.SAMPLE_RATE);
nextPts = pts + durationUs;
if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) {
// Audio PTS may come from two sources:
// - recorder.getTimestamp() if the call works;
// - an estimation from the previous PTS and the packet size as a fallback.
//
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
pts = previousPts + ONE_SAMPLE_US;
}
previousPts = pts;
outBufferInfo.set(0, r, pts, 0);
return r;
}
}

View file

@ -0,0 +1,27 @@
package com.genymobile.scrcpy.audio;
public enum AudioSource {
OUTPUT("output"),
MIC("mic"),
PLAYBACK("playback");
private final String name;
AudioSource(String name) {
this.name = name;
}
public boolean isDirect() {
return this != PLAYBACK;
}
public static AudioSource findByName(String name) {
for (AudioSource audioSource : AudioSource.values()) {
if (name.equals(audioSource.name)) {
return audioSource;
}
}
return null;
}
}

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import android.net.LocalSocket;

View file

@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.device.Position;
/**
* Union of all supported event types, identified by their {@code type}.

View file

@ -1,4 +1,8 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.util.Binary;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Position;
import java.io.EOFException;
import java.io.IOException;

View file

@ -1,5 +1,11 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.CleanUp;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Point;
import com.genymobile.scrcpy.device.Position;
import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
@ -22,7 +28,6 @@ public class Controller implements AsyncProcessor {
// control_msg.h values of the pointerId field in inject_touch_event message
private static final int POINTER_ID_MOUSE = -1;
private static final int POINTER_ID_VIRTUAL_MOUSE = -3;
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
@ -273,8 +278,9 @@ public class Controller implements AsyncProcessor {
pointer.setPressure(pressure);
int source;
if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) {
// real mouse event (forced by the client when --forward-on-click)
boolean activeSecondaryButtons = ((actionButton | buttons) & ~MotionEvent.BUTTON_PRIMARY) != 0;
if (pointerId == POINTER_ID_MOUSE && (action == MotionEvent.ACTION_HOVER_MOVE || activeSecondaryButtons)) {
// real mouse event, or event incompatible with a finger
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE;
source = InputDevice.SOURCE_MOUSE;
pointer.setUp(buttons == 0);

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
public final class DeviceMessage {

View file

@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.util.Ln;
import java.io.IOException;
import java.util.concurrent.ArrayBlockingQueue;

View file

@ -1,4 +1,7 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.StringUtils;
import java.io.IOException;
import java.io.OutputStream;

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import java.util.HashMap;
import java.util.Map;

View file

@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.device.Point;
public class Pointer {

View file

@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.device.Point;
import android.view.MotionEvent;

View file

@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.util.Ln;
import android.os.Build;
import android.os.HandlerThread;

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
public class ConfigurationException extends Exception {
public ConfigurationException(String message) {

View file

@ -1,4 +1,8 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import com.genymobile.scrcpy.control.ControlChannel;
import com.genymobile.scrcpy.util.IO;
import com.genymobile.scrcpy.util.StringUtils;
import android.net.LocalServerSocket;
import android.net.LocalSocket;

View file

@ -1,5 +1,9 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.video.ScreenInfo;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.DisplayControl;
import com.genymobile.scrcpy.wrappers.InputManager;
@ -319,10 +323,22 @@ public final class Device {
* @param mode one of the {@code POWER_MODE_*} constants
*/
public static boolean setScreenPowerMode(int mode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
if (applyToMultiPhysicalDisplays
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
&& Build.BRAND.equalsIgnoreCase("honor")
&& SurfaceControl.hasGetBuildInDisplayMethod()) {
// Workaround for Honor devices with Android 14:
// - <https://github.com/Genymobile/scrcpy/issues/4823>
// - <https://github.com/Genymobile/scrcpy/issues/4943>
applyToMultiPhysicalDisplays = false;
}
if (applyToMultiPhysicalDisplays) {
// On Android 14, these internal methods have been moved to DisplayControl
boolean useDisplayControl =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasPhysicalDisplayIdsMethod();
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasGetPhysicalDisplayIdsMethod();
// Change the power mode for all physical displays
long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds();

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
public final class DisplayInfo {
private final int displayId;

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import java.util.Objects;

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import java.util.Objects;

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import android.graphics.Rect;

View file

@ -1,4 +1,8 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.util.Codec;
import com.genymobile.scrcpy.util.IO;
import android.media.MediaCodec;

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
public final class Binary {
private Binary() {

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
public interface Codec {

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import java.util.ArrayList;
import java.util.List;

View file

@ -1,4 +1,7 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.video.VideoCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import java.io.IOException;
import java.util.Arrays;

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import android.os.Handler;

View file

@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.BuildConfig;
import android.system.ErrnoException;
import android.system.Os;

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import android.util.Log;
@ -19,7 +19,7 @@ public final class Ln {
private static final PrintStream CONSOLE_OUT = new PrintStream(new FileOutputStream(FileDescriptor.out));
private static final PrintStream CONSOLE_ERR = new PrintStream(new FileOutputStream(FileDescriptor.err));
enum Level {
public enum Level {
VERBOSE, DEBUG, INFO, WARN, ERROR
}

View file

@ -1,5 +1,7 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager;

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
public class SettingsException extends Exception {
private static String createMessage(String method, String table, String key, String value) {

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
public final class StringUtils {
private StringUtils() {

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
public final class CameraAspectRatio {
private static final float SENSOR = -1;

View file

@ -1,5 +1,8 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.util.HandlerExecutor;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
import android.annotation.SuppressLint;
import android.hardware.camera2.CameraCharacteristics;
@ -21,7 +21,7 @@ public enum CameraFacing {
return value;
}
static CameraFacing findByName(String name) {
public static CameraFacing findByName(String name) {
for (CameraFacing facing : CameraFacing.values()) {
if (name.equals(facing.name)) {
return facing;

View file

@ -1,5 +1,8 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;

View file

@ -1,4 +1,9 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.BuildConfig;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Size;
import android.graphics.Rect;

View file

@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.device.Size;
import android.view.Surface;

View file

@ -1,4 +1,15 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.util.Codec;
import com.genymobile.scrcpy.util.CodecOption;
import com.genymobile.scrcpy.util.CodecUtils;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.util.IO;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.device.Streamer;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;

View file

@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.util.Codec;
import android.annotation.SuppressLint;
import android.media.MediaFormat;

View file

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
public enum VideoSource {
DISPLAY("display"),
@ -10,7 +10,7 @@ public enum VideoSource {
this.name = name;
}
static VideoSource findByName(String name) {
public static VideoSource findByName(String name) {
for (VideoSource videoSource : VideoSource.values()) {
if (name.equals(videoSource.name)) {
return videoSource;

View file

@ -1,7 +1,7 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;

View file

@ -1,7 +1,7 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.content.ClipData;
import android.content.IOnPrimaryClipChangedListener;
@ -38,38 +38,61 @@ public final class ClipboardManager {
if (getPrimaryClipMethod == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
} else {
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
getMethodVersion = 0;
} catch (NoSuchMethodException e1) {
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
getMethodVersion = 1;
} catch (NoSuchMethodException e2) {
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
getMethodVersion = 2;
} catch (NoSuchMethodException e3) {
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
getMethodVersion = 3;
} catch (NoSuchMethodException e4) {
try {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class);
getMethodVersion = 4;
} catch (NoSuchMethodException e5) {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class,
boolean.class);
getMethodVersion = 5;
}
}
}
}
}
return getPrimaryClipMethod;
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
getMethodVersion = 0;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
getMethodVersion = 1;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
getMethodVersion = 2;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
getMethodVersion = 3;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class);
getMethodVersion = 4;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class);
getMethodVersion = 5;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, String.class);
getMethodVersion = 6;
}
return getPrimaryClipMethod;
}
@ -78,27 +101,37 @@ public final class ClipboardManager {
if (setPrimaryClipMethod == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
} else {
try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
setMethodVersion = 0;
} catch (NoSuchMethodException e1) {
try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
setMethodVersion = 1;
} catch (NoSuchMethodException e2) {
try {
setPrimaryClipMethod = manager.getClass()
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class);
setMethodVersion = 2;
} catch (NoSuchMethodException e3) {
setPrimaryClipMethod = manager.getClass()
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class);
setMethodVersion = 3;
}
}
}
return setPrimaryClipMethod;
}
try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
setMethodVersion = 0;
return setPrimaryClipMethod;
} catch (NoSuchMethodException e1) {
// fall-through
}
try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
setMethodVersion = 1;
return setPrimaryClipMethod;
} catch (NoSuchMethodException e2) {
// fall-through
}
try {
setPrimaryClipMethod = manager.getClass()
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class);
setMethodVersion = 2;
return setPrimaryClipMethod;
} catch (NoSuchMethodException e3) {
// fall-through
}
setPrimaryClipMethod = manager.getClass()
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class);
setMethodVersion = 3;
}
return setPrimaryClipMethod;
}
@ -120,8 +153,10 @@ public final class ClipboardManager {
case 4:
// The last boolean parameter is "userOperate"
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
default:
case 5:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true);
default:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, null);
}
}

View file

@ -1,8 +1,8 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.SettingsException;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.SettingsException;
import android.annotation.SuppressLint;
import android.content.AttributionSource;

View file

@ -1,6 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;

View file

@ -1,9 +1,9 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Command;
import com.genymobile.scrcpy.DisplayInfo;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.Size;
import com.genymobile.scrcpy.util.Command;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Size;
import android.annotation.SuppressLint;
import android.hardware.display.VirtualDisplay;

View file

@ -1,6 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.view.InputEvent;

View file

@ -1,6 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.os.Build;

View file

@ -1,6 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.os.IInterface;

View file

@ -1,6 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.graphics.Rect;
@ -94,6 +94,15 @@ public final class SurfaceControl {
return getBuiltInDisplayMethod;
}
public static boolean hasGetBuildInDisplayMethod() {
try {
getGetBuiltInDisplayMethod();
return true;
} catch (NoSuchMethodException e) {
return false;
}
}
public static IBinder getBuiltInDisplay() {
try {
Method method = getGetBuiltInDisplayMethod();
@ -134,7 +143,7 @@ public final class SurfaceControl {
return getPhysicalDisplayIdsMethod;
}
public static boolean hasPhysicalDisplayIdsMethod() {
public static boolean hasGetPhysicalDisplayIdsMethod() {
try {
getGetPhysicalDisplayIdsMethod();
return true;

View file

@ -1,6 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.TargetApi;
import android.os.IInterface;

Some files were not shown because too many files have changed in this diff Show more