diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..b567129a
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+github: [rom1v]
+liberapay: rom1v
+custom: ["https://paypal.me/rom2v"]
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
new file mode 100644
index 00000000..14dc373a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -0,0 +1,8 @@
+---
+name: Question
+about: Ask a question about scrcpy
+title: ''
+labels: ''
+assignees: ''
+
+---
diff --git a/README.md b/README.md
index 3185652b..67fdf364 100644
--- a/README.md
+++ b/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)
@@ -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)
diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy
index b35ea5e4..e0928cbd 100644
--- a/app/data/bash-completion/scrcpy
+++ b/app/data/bash-completion/scrcpy
@@ -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)
diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy
index 5afca977..0f06ba4b 100644
--- a/app/data/zsh-completion/_scrcpy
+++ b/app/data/zsh-completion/_scrcpy
@@ -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]'
diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh
index 589f93e5..0a42bc1f 100755
--- a/app/deps/sdl.sh
+++ b/app/deps/sdl.sh
@@ -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"
diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc
index 717d9cb2..9e0d90c2 100644
--- a/app/scrcpy-windows.rc
+++ b/app/scrcpy-windows.rc
@@ -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"
diff --git a/app/scrcpy.1 b/app/scrcpy.1
index cf8dfa7f..de2b8ac6 100644
--- a/app/scrcpy.1
+++ b/app/scrcpy.1
@@ -49,6 +49,12 @@ The list of possible codec options is available in the Android documentation:
+.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
diff --git a/app/src/cli.c b/app/src/cli.c
index 08a4aa3f..dd1b6799 100644
--- a/app/src/cli.c
+++ b/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: "
"",
},
+ {
+ .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;
diff --git a/app/src/control_msg.c b/app/src/control_msg.c
index b3da5fe5..9b0fab67 100644
--- a/app/src/control_msg.c
+++ b/app/src/control_msg.c
@@ -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;
diff --git a/app/src/control_msg.h b/app/src/control_msg.h
index cd1340ef..80714096 100644
--- a/app/src/control_msg.h
+++ b/app/src/control_msg.h
@@ -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,
diff --git a/app/src/controller.c b/app/src/controller.c
index edd767eb..d50e1921 100644
--- a/app/src/controller.c
+++ b/app/src/controller.c
@@ -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
diff --git a/app/src/controller.h b/app/src/controller.h
index 353d4d0d..57ad79b3 100644
--- a/app/src/controller.h
+++ b/app/src/controller.h
@@ -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
diff --git a/app/src/display.c b/app/src/display.c
index 9f5fb0c6..39018834 100644
--- a/app/src/display.c
+++ b/app/src/display.c
@@ -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) {
diff --git a/app/src/input_events.h b/app/src/input_events.h
index ed77bcb4..bbf4372f 100644
--- a/app/src/input_events.h
+++ b/app/src/input_events.h
@@ -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
diff --git a/app/src/input_manager.c b/app/src/input_manager.c
index 43b10d2d..d3c94d03 100644
--- a/app/src/input_manager.c
+++ b/app/src/input_manager.c
@@ -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);
diff --git a/app/src/input_manager.h b/app/src/input_manager.h
index 03c42fe6..88558549 100644
--- a/app/src/input_manager.h
+++ b/app/src/input_manager.h
@@ -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.
diff --git a/app/src/options.c b/app/src/options.c
index 5556d1f9..6fca6ad5 100644
--- a/app/src/options.c
+++ b/app/src/options.c
@@ -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
diff --git a/app/src/options.h b/app/src/options.h
index f840a989..140d12b1 100644
--- a/app/src/options.h
+++ b/app/src/options.h
@@ -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;
diff --git a/app/src/receiver.c b/app/src/receiver.c
index fb923ac4..3e572067 100644
--- a/app/src/receiver.c
+++ b/app/src/receiver.c
@@ -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;
}
diff --git a/app/src/receiver.h b/app/src/receiver.h
index ef83978f..b1ae4fde 100644
--- a/app/src/receiver.h
+++ b/app/src/receiver.h
@@ -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
diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c
index 5e78dbf3..43864661 100644
--- a/app/src/scrcpy.c
+++ b/app/src/scrcpy.c
@@ -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);
}
}
diff --git a/app/src/server.c b/app/src/server.c
index 4d55e994..41517f18 100644
--- a/app/src/server.c
+++ b/app/src/server.c
@@ -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);
diff --git a/app/src/server.h b/app/src/server.h
index 062af0a9..cffa510e 100644
--- a/app/src/server.h
+++ b/app/src/server.h
@@ -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;
diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c
index 33500e0c..5c4f97f0 100644
--- a/app/src/usb/screen_otg.c
+++ b/app/src/usb/screen_otg.c
@@ -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);
diff --git a/doc/audio.md b/doc/audio.md
index 0c0409a9..750163e0 100644
--- a/doc/audio.md
+++ b/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
diff --git a/doc/build.md b/doc/build.md
index a35910f8..15e0ffff 100644
--- a/doc/build.md
+++ b/doc/build.md
@@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server
- - [`scrcpy-server-v2.5`][direct-scrcpy-server]
- SHA-256: `1488b1105d6aff534873a26bf610cd2aea06ee867dd7a4d9c6bb2c091396eb15`
+ - [`scrcpy-server-v2.6.1`][direct-scrcpy-server]
+ SHA-256: `ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b`
-[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:
diff --git a/doc/mouse.md b/doc/mouse.md
index 1c62ddd0..ec4aea63 100644
--- a/doc/mouse.md
+++ b/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
+Shift key (e.g. Shift+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 Shift 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
```
diff --git a/doc/windows.md b/doc/windows.md
index 139c3419..65ec2b45 100644
--- a/doc/windows.md
+++ b/doc/windows.md
@@ -4,14 +4,14 @@
Download the [latest release]:
- - [`scrcpy-win64-v2.5.zip`][direct-win64] (64-bit)
- SHA-256: `345cf04a66a9144281dce72ca4e82adfd2c3092463196e586051df4c69e1507b`
- - [`scrcpy-win32-v2.5.zip`][direct-win32] (32-bit)
- SHA-256: `d56312a92471565fa4f3a6b94e8eb07717c4c90f2c0f05b03ba444e1001806ec`
+ - [`scrcpy-win64-v2.6.1.zip`][direct-win64] (64-bit)
+ SHA-256: `041fc3abf8578ddcead5a8c4a8be8960b7c4d45b21d3370ee2683605e86a728c`
+ - [`scrcpy-win32-v2.6.1.zip`][direct-win32] (32-bit)
+ SHA-256: `17a5d4d17230b4c90fad45af6395efda9aea287a03c04e6b4ecc9ceb8134ea04`
[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.
diff --git a/install_release.sh b/install_release.sh
index 2bd6d7e6..2aad8cdc 100755
--- a/install_release.sh
+++ b/install_release.sh
@@ -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
diff --git a/meson.build b/meson.build
index 1d11e574..b532006a 100644
--- a/meson.build
+++ b/meson.build
@@ -1,5 +1,5 @@
project('scrcpy', 'c',
- version: '2.5',
+ version: '2.6.1',
meson_version: '>= 0.48',
default_options: [
'c_std=c11',
diff --git a/release.mk b/release.mk
index 89f3da21..dd544bae 100644
--- a/release.mk
+++ b/release.mk
@@ -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)
diff --git a/server/build.gradle b/server/build.gradle
index d17ffcb2..decacd3f 100644
--- a/server/build.gradle
+++ b/server/build.gradle
@@ -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 {
diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh
index 74bbd8ae..5ee7af30 100755
--- a/server/build_without_gradle.sh
+++ b/server/build_without_gradle.sh
@@ -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"
diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java b/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java
deleted file mode 100644
index baa7d846..00000000
--- a/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java
+++ /dev/null
@@ -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 {
-}
diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/AudioSource.java
deleted file mode 100644
index 466ea297..00000000
--- a/server/src/main/java/com/genymobile/scrcpy/AudioSource.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java
index f9b1efd6..1b8d4248 100644
--- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java
+++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java
index 9b1d8d8d..2f86d8ce 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Options.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Options.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java
index 587a46df..7817fdf5 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Server.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Server.java
@@ -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 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
diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java
index 448e7099..8fc38555 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java
@@ -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:
- // -
- // -
- // -
- //
- // But only apply when strictly necessary, since workarounds can cause other issues:
- // -
- // -
- mustFillAppInfo = true;
- } else if (Build.BRAND.equalsIgnoreCase("honor")) {
- // More workarounds must be applied for Honor devices:
- // -
- //
- // The system context must not be set for all devices, because it would cause other problems:
- // -
- // -
- 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.
//
- 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..", e);
- throw new RuntimeException("Cannot create AudioRecord");
+ Ln.e("Cannot create AudioRecord", e);
+ throw new AudioCaptureException();
}
}
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java
new file mode 100644
index 00000000..62903f83
--- /dev/null
+++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java
@@ -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);
+}
diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureException.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureException.java
new file mode 100644
index 00000000..4b0b7e83
--- /dev/null
+++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureException.java
@@ -0,0 +1,12 @@
+package com.genymobile.scrcpy.audio;
+
+/**
+ * Exception for any audio capture issue.
+ *
+ * This includes the case where audio capture failed on Android 11 specifically because the running App (Shell) was not in foreground.
+ *
+ * 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 {
+}
diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCodec.java
similarity index 93%
rename from server/src/main/java/com/genymobile/scrcpy/AudioCodec.java
rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioCodec.java
index b4ea3680..8f9e59b3 100644
--- a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java
+++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCodec.java
@@ -1,4 +1,6 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.audio;
+
+import com.genymobile.scrcpy.util.Codec;
import android.media.MediaFormat;
diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java
new file mode 100644
index 00000000..c77165a7
--- /dev/null
+++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java
@@ -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();
+ }
+}
diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java
similarity index 55%
rename from server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java
index 3934ad49..361c7bac 100644
--- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
+++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java
@@ -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);
}
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java
similarity index 93%
rename from server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java
rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java
index 0b59369b..8230e054 100644
--- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java
+++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java
@@ -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);
diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java
new file mode 100644
index 00000000..e38493f2
--- /dev/null
+++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java
@@ -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
+ 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);
+ }
+}
diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java
similarity index 88%
rename from server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java
rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java
index 7e052f32..323caae4 100644
--- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java
+++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java
@@ -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);
diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java
new file mode 100644
index 00000000..80286831
--- /dev/null
+++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java
@@ -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;
+ }
+}
diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java
new file mode 100644
index 00000000..6082f20e
--- /dev/null
+++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java
@@ -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;
+ }
+}
diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlChannel.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java
similarity index 96%
rename from server/src/main/java/com/genymobile/scrcpy/ControlChannel.java
rename to server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java
index 4677cfda..f24ca117 100644
--- a/server/src/main/java/com/genymobile/scrcpy/ControlChannel.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.control;
import android.net.LocalSocket;
diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java
similarity index 98%
rename from server/src/main/java/com/genymobile/scrcpy/ControlMessage.java
rename to server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java
index bcbacb4b..c414f2a5 100644
--- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java
@@ -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}.
diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java
similarity index 98%
rename from server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java
rename to server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java
index 1761d228..f5cfee75 100644
--- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java
similarity index 96%
rename from server/src/main/java/com/genymobile/scrcpy/Controller.java
rename to server/src/main/java/com/genymobile/scrcpy/control/Controller.java
index 87faf8ba..1494c10a 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Controller.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java
@@ -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);
diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java
similarity index 97%
rename from server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java
rename to server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java
index a8987eb6..079a7a04 100644
--- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.control;
public final class DeviceMessage {
diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageSender.java
similarity index 94%
rename from server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java
rename to server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageSender.java
index af14bb4e..dc5e6be0 100644
--- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageSender.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java
similarity index 93%
rename from server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java
rename to server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java
index f5d57c98..6bf53bed 100644
--- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/KeyComposition.java b/server/src/main/java/com/genymobile/scrcpy/control/KeyComposition.java
similarity index 99%
rename from server/src/main/java/com/genymobile/scrcpy/KeyComposition.java
rename to server/src/main/java/com/genymobile/scrcpy/control/KeyComposition.java
index 2f2835c9..5b988f53 100644
--- a/server/src/main/java/com/genymobile/scrcpy/KeyComposition.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/KeyComposition.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.control;
import java.util.HashMap;
import java.util.Map;
diff --git a/server/src/main/java/com/genymobile/scrcpy/Pointer.java b/server/src/main/java/com/genymobile/scrcpy/control/Pointer.java
similarity index 92%
rename from server/src/main/java/com/genymobile/scrcpy/Pointer.java
rename to server/src/main/java/com/genymobile/scrcpy/control/Pointer.java
index b89cc256..02e33e10 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Pointer.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/Pointer.java
@@ -1,4 +1,6 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.control;
+
+import com.genymobile.scrcpy.device.Point;
public class Pointer {
diff --git a/server/src/main/java/com/genymobile/scrcpy/PointersState.java b/server/src/main/java/com/genymobile/scrcpy/control/PointersState.java
similarity index 97%
rename from server/src/main/java/com/genymobile/scrcpy/PointersState.java
rename to server/src/main/java/com/genymobile/scrcpy/control/PointersState.java
index d8daaff2..a12da71d 100644
--- a/server/src/main/java/com/genymobile/scrcpy/PointersState.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/PointersState.java
@@ -1,4 +1,6 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.control;
+
+import com.genymobile.scrcpy.device.Point;
import android.view.MotionEvent;
diff --git a/server/src/main/java/com/genymobile/scrcpy/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java
similarity index 98%
rename from server/src/main/java/com/genymobile/scrcpy/UhidManager.java
rename to server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java
index a39288a5..b1e6a9b9 100644
--- a/server/src/main/java/com/genymobile/scrcpy/UhidManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java b/server/src/main/java/com/genymobile/scrcpy/device/ConfigurationException.java
similarity index 78%
rename from server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java
rename to server/src/main/java/com/genymobile/scrcpy/device/ConfigurationException.java
index 76c8f52e..17729342 100644
--- a/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java
+++ b/server/src/main/java/com/genymobile/scrcpy/device/ConfigurationException.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.device;
public class ConfigurationException extends Exception {
public ConfigurationException(String message) {
diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java
similarity index 97%
rename from server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java
rename to server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java
index d693ad61..db75aec6 100644
--- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java
+++ b/server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java
similarity index 94%
rename from server/src/main/java/com/genymobile/scrcpy/Device.java
rename to server/src/main/java/com/genymobile/scrcpy/device/Device.java
index 8d0ee231..5a1083fd 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Device.java
+++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java
@@ -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:
+ // -
+ // -
+ 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();
diff --git a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java
similarity index 95%
rename from server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java
rename to server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java
index 4b8036f8..2973710d 100644
--- a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java
+++ b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.device;
public final class DisplayInfo {
private final int displayId;
diff --git a/server/src/main/java/com/genymobile/scrcpy/Point.java b/server/src/main/java/com/genymobile/scrcpy/device/Point.java
similarity index 95%
rename from server/src/main/java/com/genymobile/scrcpy/Point.java
rename to server/src/main/java/com/genymobile/scrcpy/device/Point.java
index c2a30fa8..361b9958 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Point.java
+++ b/server/src/main/java/com/genymobile/scrcpy/device/Point.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.device;
import java.util.Objects;
diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/device/Position.java
similarity index 97%
rename from server/src/main/java/com/genymobile/scrcpy/Position.java
rename to server/src/main/java/com/genymobile/scrcpy/device/Position.java
index 2d298645..7ce4e256 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Position.java
+++ b/server/src/main/java/com/genymobile/scrcpy/device/Position.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.device;
import java.util.Objects;
diff --git a/server/src/main/java/com/genymobile/scrcpy/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java
similarity index 96%
rename from server/src/main/java/com/genymobile/scrcpy/Size.java
rename to server/src/main/java/com/genymobile/scrcpy/device/Size.java
index fd4b6971..bc9dce1c 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Size.java
+++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.device;
import android.graphics.Rect;
diff --git a/server/src/main/java/com/genymobile/scrcpy/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/device/Streamer.java
similarity index 97%
rename from server/src/main/java/com/genymobile/scrcpy/Streamer.java
rename to server/src/main/java/com/genymobile/scrcpy/device/Streamer.java
index 8b6c9dcc..f54d0567 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Streamer.java
+++ b/server/src/main/java/com/genymobile/scrcpy/device/Streamer.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/Binary.java b/server/src/main/java/com/genymobile/scrcpy/util/Binary.java
similarity index 96%
rename from server/src/main/java/com/genymobile/scrcpy/Binary.java
rename to server/src/main/java/com/genymobile/scrcpy/util/Binary.java
index 29534f59..f46ba695 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Binary.java
+++ b/server/src/main/java/com/genymobile/scrcpy/util/Binary.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.util;
public final class Binary {
private Binary() {
diff --git a/server/src/main/java/com/genymobile/scrcpy/Codec.java b/server/src/main/java/com/genymobile/scrcpy/util/Codec.java
similarity index 82%
rename from server/src/main/java/com/genymobile/scrcpy/Codec.java
rename to server/src/main/java/com/genymobile/scrcpy/util/Codec.java
index 7e905af3..a363bd8b 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Codec.java
+++ b/server/src/main/java/com/genymobile/scrcpy/util/Codec.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.util;
public interface Codec {
diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java b/server/src/main/java/com/genymobile/scrcpy/util/CodecOption.java
similarity index 98%
rename from server/src/main/java/com/genymobile/scrcpy/CodecOption.java
rename to server/src/main/java/com/genymobile/scrcpy/util/CodecOption.java
index 22c45a90..bed2be9a 100644
--- a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java
+++ b/server/src/main/java/com/genymobile/scrcpy/util/CodecOption.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.util;
import java.util.ArrayList;
import java.util.List;
diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java
similarity index 95%
rename from server/src/main/java/com/genymobile/scrcpy/CodecUtils.java
rename to server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java
index afb6f904..5b0c95e8 100644
--- a/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java
+++ b/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/Command.java b/server/src/main/java/com/genymobile/scrcpy/util/Command.java
similarity index 97%
rename from server/src/main/java/com/genymobile/scrcpy/Command.java
rename to server/src/main/java/com/genymobile/scrcpy/util/Command.java
index 362504ff..b26158e6 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Command.java
+++ b/server/src/main/java/com/genymobile/scrcpy/util/Command.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.util;
import java.io.IOException;
import java.util.Arrays;
diff --git a/server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java b/server/src/main/java/com/genymobile/scrcpy/util/HandlerExecutor.java
similarity index 93%
rename from server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java
rename to server/src/main/java/com/genymobile/scrcpy/util/HandlerExecutor.java
index 1f5f0a4f..03309989 100644
--- a/server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java
+++ b/server/src/main/java/com/genymobile/scrcpy/util/HandlerExecutor.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.util;
import android.os.Handler;
diff --git a/server/src/main/java/com/genymobile/scrcpy/IO.java b/server/src/main/java/com/genymobile/scrcpy/util/IO.java
similarity index 96%
rename from server/src/main/java/com/genymobile/scrcpy/IO.java
rename to server/src/main/java/com/genymobile/scrcpy/util/IO.java
index 4a55c152..ab3fa59f 100644
--- a/server/src/main/java/com/genymobile/scrcpy/IO.java
+++ b/server/src/main/java/com/genymobile/scrcpy/util/IO.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/util/Ln.java
similarity index 98%
rename from server/src/main/java/com/genymobile/scrcpy/Ln.java
rename to server/src/main/java/com/genymobile/scrcpy/util/Ln.java
index cdd57b9f..c0700125 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Ln.java
+++ b/server/src/main/java/com/genymobile/scrcpy/util/Ln.java
@@ -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
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java
similarity index 98%
rename from server/src/main/java/com/genymobile/scrcpy/LogUtils.java
rename to server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java
index 1ffb19d3..aee1594a 100644
--- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java
+++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/Settings.java b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java
similarity index 98%
rename from server/src/main/java/com/genymobile/scrcpy/Settings.java
rename to server/src/main/java/com/genymobile/scrcpy/util/Settings.java
index 1b5e5f98..d9e82d62 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Settings.java
+++ b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/SettingsException.java b/server/src/main/java/com/genymobile/scrcpy/util/SettingsException.java
similarity index 92%
rename from server/src/main/java/com/genymobile/scrcpy/SettingsException.java
rename to server/src/main/java/com/genymobile/scrcpy/util/SettingsException.java
index 36ef63ee..87fa3884 100644
--- a/server/src/main/java/com/genymobile/scrcpy/SettingsException.java
+++ b/server/src/main/java/com/genymobile/scrcpy/util/SettingsException.java
@@ -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) {
diff --git a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/StringUtils.java
similarity index 94%
rename from server/src/main/java/com/genymobile/scrcpy/StringUtils.java
rename to server/src/main/java/com/genymobile/scrcpy/util/StringUtils.java
index dac05466..8b19ca3d 100644
--- a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java
+++ b/server/src/main/java/com/genymobile/scrcpy/util/StringUtils.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.util;
public final class StringUtils {
private StringUtils() {
diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraAspectRatio.java
similarity index 96%
rename from server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java
rename to server/src/main/java/com/genymobile/scrcpy/video/CameraAspectRatio.java
index 4fdf4c74..bf1cba5d 100644
--- a/server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java
+++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraAspectRatio.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.video;
public final class CameraAspectRatio {
private static final float SENSOR = -1;
diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java
similarity index 98%
rename from server/src/main/java/com/genymobile/scrcpy/CameraCapture.java
rename to server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java
index df3cf7c4..7d2e2055 100644
--- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java
+++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraFacing.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraFacing.java
similarity index 89%
rename from server/src/main/java/com/genymobile/scrcpy/CameraFacing.java
rename to server/src/main/java/com/genymobile/scrcpy/video/CameraFacing.java
index b7e8daa5..f818e665 100644
--- a/server/src/main/java/com/genymobile/scrcpy/CameraFacing.java
+++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraFacing.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java
similarity index 96%
rename from server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java
rename to server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java
index 090c96f0..fbeca2af 100644
--- a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java
+++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java
similarity index 96%
rename from server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java
rename to server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java
index 8e5b401f..ba537b17 100644
--- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java
+++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java
similarity index 95%
rename from server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java
rename to server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java
index e300e4d6..3118ddc8 100644
--- a/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java
+++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java
@@ -1,4 +1,6 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.video;
+
+import com.genymobile.scrcpy.device.Size;
import android.view.Surface;
diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java
similarity index 95%
rename from server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java
rename to server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java
index 4a0fdf4e..8fe0b227 100644
--- a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java
+++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoCodec.java
similarity index 93%
rename from server/src/main/java/com/genymobile/scrcpy/VideoCodec.java
rename to server/src/main/java/com/genymobile/scrcpy/video/VideoCodec.java
index fa787a99..5d528da1 100644
--- a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java
+++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoCodec.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoSource.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoSource.java
similarity index 80%
rename from server/src/main/java/com/genymobile/scrcpy/VideoSource.java
rename to server/src/main/java/com/genymobile/scrcpy/video/VideoSource.java
index b5a74fbe..53b54a52 100644
--- a/server/src/main/java/com/genymobile/scrcpy/VideoSource.java
+++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoSource.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java
index d4bee165..bb1ca0d4 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java
index ed5c8d75..c5f007fe 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java
@@ -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);
}
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java
index a03f824e..7e92ac50 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java
index ba3e9ee0..cc9d5526 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java
index 2ff82d04..dd92330c 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java
index 16ecb09f..5c5ba56c 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java
index 36d5f1ac..0a56f347 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java
index af217da2..ca80dde2 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java
@@ -1,6 +1,6 @@
package com.genymobile.scrcpy.wrappers;
-import com.genymobile.scrcpy.Ln;
+import com.genymobile.scrcpy.util.Ln;
import android.os.IInterface;
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java
index f0e351a2..038e7ca0 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java
@@ -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;
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java
index 44394ba9..4c769e85 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java
@@ -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;
diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java
similarity index 99%
rename from server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java
rename to server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java
index 0c8086f7..1737730f 100644
--- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java
+++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java
@@ -1,4 +1,6 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.control;
+
+import com.genymobile.scrcpy.device.Device;
import android.view.KeyEvent;
import android.view.MotionEvent;
diff --git a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java
similarity index 98%
rename from server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java
rename to server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java
index d7f926ba..ff1a2fbc 100644
--- a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java
+++ b/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.control;
import org.junit.Assert;
import org.junit.Test;
diff --git a/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java b/server/src/test/java/com/genymobile/scrcpy/util/BinaryTest.java
similarity index 98%
rename from server/src/test/java/com/genymobile/scrcpy/BinaryTest.java
rename to server/src/test/java/com/genymobile/scrcpy/util/BinaryTest.java
index 569a2f2c..7ee95ac5 100644
--- a/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java
+++ b/server/src/test/java/com/genymobile/scrcpy/util/BinaryTest.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.util;
import org.junit.Assert;
import org.junit.Test;
diff --git a/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java b/server/src/test/java/com/genymobile/scrcpy/util/CodecOptionsTest.java
similarity index 99%
rename from server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java
rename to server/src/test/java/com/genymobile/scrcpy/util/CodecOptionsTest.java
index ad802258..ffd8e32e 100644
--- a/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java
+++ b/server/src/test/java/com/genymobile/scrcpy/util/CodecOptionsTest.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.util;
import org.junit.Assert;
import org.junit.Test;
diff --git a/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java b/server/src/test/java/com/genymobile/scrcpy/util/CommandParserTest.java
similarity index 99%
rename from server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java
rename to server/src/test/java/com/genymobile/scrcpy/util/CommandParserTest.java
index de996a07..7e1d55b5 100644
--- a/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java
+++ b/server/src/test/java/com/genymobile/scrcpy/util/CommandParserTest.java
@@ -1,5 +1,6 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.util;
+import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import android.view.Display;
diff --git a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java b/server/src/test/java/com/genymobile/scrcpy/util/StringUtilsTest.java
similarity index 97%
rename from server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java
rename to server/src/test/java/com/genymobile/scrcpy/util/StringUtilsTest.java
index 89799c5e..c72b112a 100644
--- a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java
+++ b/server/src/test/java/com/genymobile/scrcpy/util/StringUtilsTest.java
@@ -1,4 +1,4 @@
-package com.genymobile.scrcpy;
+package com.genymobile.scrcpy.util;
import org.junit.Assert;
import org.junit.Test;