mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-04-20 11:35:57 +00:00
Merge branch 'Genymobile:master' into master
This commit is contained in:
commit
3ef6b50f10
106 changed files with 1114 additions and 490 deletions
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
github: [rom1v]
|
||||
liberapay: rom1v
|
||||
custom: ["https://paypal.me/rom2v"]
|
8
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
8
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: Question
|
||||
about: Ask a question about scrcpy
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
23
README.md
23
README.md
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]'
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
24
app/scrcpy.1
24
app/scrcpy.1
|
@ -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
|
||||
|
|
166
app/src/cli.c
166
app/src/cli.c
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
24
doc/audio.md
24
doc/audio.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
52
doc/mouse.md
52
doc/mouse.md
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
project('scrcpy', 'c',
|
||||
version: '2.5',
|
||||
version: '2.6.1',
|
||||
meson_version: '>= 0.48',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.audio;
|
||||
|
||||
import com.genymobile.scrcpy.util.Codec;
|
||||
|
||||
import android.media.MediaFormat;
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.control;
|
||||
|
||||
import android.net.LocalSocket;
|
||||
|
|
@ -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}.
|
|
@ -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;
|
|
@ -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);
|
|
@ -1,4 +1,4 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.control;
|
||||
|
||||
public final class DeviceMessage {
|
||||
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,4 +1,4 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.control;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
|
@ -1,4 +1,6 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.control;
|
||||
|
||||
import com.genymobile.scrcpy.device.Point;
|
||||
|
||||
public class Pointer {
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.control;
|
||||
|
||||
import com.genymobile.scrcpy.device.Point;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
|
@ -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;
|
|
@ -1,4 +1,4 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.device;
|
||||
|
||||
public class ConfigurationException extends Exception {
|
||||
public ConfigurationException(String message) {
|
|
@ -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;
|
|
@ -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();
|
|
@ -1,4 +1,4 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.device;
|
||||
|
||||
public final class DisplayInfo {
|
||||
private final int displayId;
|
|
@ -1,4 +1,4 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.device;
|
||||
|
||||
import java.util.Objects;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.device;
|
||||
|
||||
import java.util.Objects;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.device;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
public final class Binary {
|
||||
private Binary() {
|
|
@ -1,4 +1,4 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
public interface Codec {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
|
@ -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;
|
|
@ -1,4 +1,4 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
|
@ -1,4 +1,4 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
import android.os.Handler;
|
||||
|
|
@ -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;
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
|
@ -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) {
|
|
@ -1,4 +1,4 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
public final class StringUtils {
|
||||
private StringUtils() {
|
|
@ -1,4 +1,4 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.video;
|
||||
|
||||
public final class CameraAspectRatio {
|
||||
private static final float SENSOR = -1;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.video;
|
||||
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
|
||||
import android.view.Surface;
|
||||
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import android.os.IInterface;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue