mirror of
https://github.com/Genymobile/scrcpy.git
synced 2026-02-24 07:14:29 +01:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b81e0f92b5 | ||
|
|
be40ee5dd9 | ||
|
|
0498459c1f | ||
|
|
9787fe5d26 | ||
|
|
98d30288f7 | ||
|
|
d74cfd5711 | ||
|
|
cd3a5d50b6 | ||
|
|
772f42134a | ||
|
|
38256d8ff9 | ||
|
|
4e1cf13a50 | ||
|
|
696402c68c | ||
|
|
dc169e425e | ||
|
|
13fc75902a | ||
|
|
454beaa757 | ||
|
|
1a9ffb3814 | ||
|
|
ac16be54c8 | ||
|
|
8a02e3c2f5 | ||
|
|
283326b2f6 | ||
|
|
ca4f50c5ef | ||
|
|
7a3fe830d4 | ||
|
|
ee414231ed | ||
|
|
41ed40f5f9 | ||
|
|
d2cc930975 | ||
|
|
52f5d08d1f | ||
|
|
70bfa2cf39 | ||
|
|
38f779d9d3 | ||
|
|
8cd63cb63e | ||
|
|
cc309a2b34 | ||
|
|
91a4a74641 | ||
|
|
48f38c4bb6 | ||
|
|
6875e9aa88 | ||
|
|
c5ed2cfc28 | ||
|
|
1a0d300786 | ||
|
|
d2447b5c19 | ||
|
|
5900e9e39c | ||
|
|
882003f314 | ||
|
|
db9dc6ae83 | ||
|
|
e0f37f834b | ||
|
|
89b624770c | ||
|
|
79227af89f | ||
|
|
5d12d9071d | ||
|
|
b7add42154 | ||
|
|
dd1bfae4e0 | ||
|
|
bef2d8473b | ||
|
|
609719bde0 | ||
|
|
3a0703f428 | ||
|
|
245981281e | ||
|
|
1d25338119 | ||
|
|
457c7fe5cf | ||
|
|
7998811fa5 | ||
|
|
7044122fc5 | ||
|
|
c63d9e1803 | ||
|
|
d892a9aac5 | ||
|
|
fd8bef68b7 | ||
|
|
986328ff9e | ||
|
|
0ba9d35705 | ||
|
|
cac8e9c821 | ||
|
|
1c7680f689 | ||
|
|
c27d116a66 | ||
|
|
eac711ace6 | ||
|
|
5ae01749bf | ||
|
|
1fd57ede1f | ||
|
|
48fc18e380 | ||
|
|
ea6a94d355 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -84,7 +84,7 @@ jobs:
|
||||
run: release/test_client.sh
|
||||
|
||||
build-linux-x86_64:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check architecture
|
||||
run: |
|
||||
|
||||
5
FAQ.md
5
FAQ.md
@@ -166,14 +166,13 @@ Rebooting the device is necessary once this option is set.
|
||||
|
||||
### Special characters do not work
|
||||
|
||||
The default text injection method is [limited to ASCII characters][text-input].
|
||||
A trick allows to also inject some [accented characters][accented-characters],
|
||||
The default text injection method is limited to ASCII characters. A trick allows
|
||||
to also inject some [accented characters][accented-characters],
|
||||
but that's all. See [#37].
|
||||
|
||||
To avoid the problem, [change the keyboard mode to simulate a physical
|
||||
keyboard][hid].
|
||||
|
||||
[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode
|
||||
[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters
|
||||
[#37]: https://github.com/Genymobile/scrcpy/issues/37
|
||||
[hid]: doc/keyboard.md#physical-keyboard-simulation
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -188,7 +188,7 @@
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2024 Romain Vimont
|
||||
Copyright (C) 2018-2025 Romain Vimont
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
26
README.md
26
README.md
@@ -2,16 +2,16 @@
|
||||
source for the project. Do not download releases from random websites, even if
|
||||
their name contains `scrcpy`.**
|
||||
|
||||
# scrcpy (v3.1)
|
||||
# scrcpy (v3.3)
|
||||
|
||||
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||
|
||||
_pronounced "**scr**een **c**o**py**"_
|
||||
|
||||
This application mirrors Android devices (video and audio) connected via
|
||||
USB or [over TCP/IP](doc/connection.md#tcpip-wireless), and allows to control the
|
||||
device with the keyboard and the mouse of the computer. It does not require any
|
||||
_root_ access. It works on _Linux_, _Windows_ and _macOS_.
|
||||
This application mirrors Android devices (video and audio) connected via USB or
|
||||
[TCP/IP](doc/connection.md#tcpip-wireless) and allows control using the
|
||||
computer's keyboard and mouse. It does not require _root_ access or an app
|
||||
installed on the device. It works on _Linux_, _Windows_, and _macOS_.
|
||||
|
||||

|
||||
|
||||
@@ -58,7 +58,7 @@ Make sure you [enabled USB debugging][enable-adb] on your device(s).
|
||||
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.
|
||||
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
|
||||
@@ -78,6 +78,16 @@ Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md).
|
||||
- [macOS](doc/macos.md)
|
||||
|
||||
|
||||
## Must-know tips
|
||||
|
||||
- [Reducing resolution](doc/video.md#size) may greatly improve performance
|
||||
(`scrcpy -m1024`)
|
||||
- [_Right-click_](doc/mouse.md#mouse-bindings) triggers `BACK`
|
||||
- [_Middle-click_](doc/mouse.md#mouse-bindings) triggers `HOME`
|
||||
- <kbd>Alt</kbd>+<kbd>f</kbd> toggles [fullscreen](doc/window.md#fullscreen)
|
||||
- There are many other [shortcuts](doc/shortcuts.md)
|
||||
|
||||
|
||||
## Usage examples
|
||||
|
||||
There are a lot of options, [documented](#user-documentation) in separate pages.
|
||||
@@ -197,10 +207,10 @@ work][donate]:
|
||||
|
||||
[donate]: https://blog.rom1v.com/about/#support-my-open-source-work
|
||||
|
||||
## Licence
|
||||
## License
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2024 Romain Vimont
|
||||
Copyright (C) 2018-2025 Romain Vimont
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -23,6 +23,7 @@ _scrcpy() {
|
||||
-d --select-usb
|
||||
--disable-screensaver
|
||||
--display-id=
|
||||
--display-ime-policy=
|
||||
--display-orientation=
|
||||
-e --select-tcpip
|
||||
-f --fullscreen
|
||||
@@ -121,7 +122,7 @@ _scrcpy() {
|
||||
return
|
||||
;;
|
||||
--audio-source)
|
||||
COMPREPLY=($(compgen -W 'output mic playback' -- "$cur"))
|
||||
COMPREPLY=($(compgen -W 'output playback mic mic-unprocessed mic-camcorder mic-voice-recognition mic-voice-communication voice-call voice-call-uplink voice-call-downlink voice-performance' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--camera-facing)
|
||||
@@ -148,6 +149,10 @@ _scrcpy() {
|
||||
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--display-ime-policy)
|
||||
COMPREPLY=($(compgen -W 'local fallback hide' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--record-orientation)
|
||||
COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur"))
|
||||
return
|
||||
@@ -200,6 +205,7 @@ _scrcpy() {
|
||||
|-p|--port \
|
||||
|--push-target \
|
||||
|--rotation \
|
||||
|--screen-off-timeout \
|
||||
|--tunnel-host \
|
||||
|--tunnel-port \
|
||||
|--v4l2-buffer \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#compdef -N scrcpy -N scrcpy.exe
|
||||
#compdef scrcpy scrcpy.exe
|
||||
#
|
||||
# name: scrcpy
|
||||
# auth: hltdev [hltdev8642@gmail.com]
|
||||
@@ -11,12 +11,12 @@ arguments=(
|
||||
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
|
||||
'--angle=[Rotate the video content by a custom angle, in degrees]'
|
||||
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
|
||||
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
|
||||
'--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 playback)'
|
||||
'--audio-source=[Select the audio source]:source:(output playback mic mic-unprocessed mic-camcorder mic-voice-recognition mic-voice-communication voice-call voice-call-uplink voice-call-downlink voice-performance)'
|
||||
'--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]'
|
||||
@@ -30,14 +30,15 @@ arguments=(
|
||||
{-d,--select-usb}'[Use USB device]'
|
||||
'--disable-screensaver[Disable screensaver while scrcpy is running]'
|
||||
'--display-id=[Specify the display id to mirror]'
|
||||
'--display-ime-policy[Set the policy for selecting where the IME should be displayed]'
|
||||
'--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
|
||||
{-e,--select-tcpip}'[Use TCP/IP device]'
|
||||
{-f,--fullscreen}'[Start in fullscreen]'
|
||||
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
|
||||
'-G[Use UHID/AOA gamepad (same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode)]'
|
||||
'-G[Use UHID/AOA gamepad \(same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode\)]'
|
||||
'--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)'
|
||||
{-h,--help}'[Print the help]'
|
||||
'-K[Use UHID/AOA keyboard (same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode)]'
|
||||
'-K[Use UHID/AOA keyboard \(same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode\)]'
|
||||
'--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)'
|
||||
'--kill-adb-on-close[Kill adb when scrcpy terminates]'
|
||||
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
|
||||
@@ -47,7 +48,7 @@ arguments=(
|
||||
'--list-displays[List displays available on the device]'
|
||||
'--list-encoders[List video and audio encoders available on the device]'
|
||||
{-m,--max-size=}'[Limit both the width and height of the video to value]'
|
||||
'-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]'
|
||||
'-M[Use UHID/AOA mouse \(same as --mouse=uhid or --mouse=aoa, depending on OTG mode\)]'
|
||||
'--max-fps=[Limit the frame rate of screen capture]'
|
||||
'--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)'
|
||||
'--mouse-bind=[Configure bindings of secondary clicks]'
|
||||
|
||||
@@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
|
||||
VERSION=35.0.2
|
||||
VERSION=36.0.0
|
||||
FILENAME=platform-tools_r$VERSION-linux.zip
|
||||
PROJECT_DIR=platform-tools-$VERSION-linux
|
||||
SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a
|
||||
SHA256SUM=0ead642c943ffe79701fccca8f5f1c69c4ce4f43df2eefee553f6ccb27cbfbe8
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
|
||||
VERSION=35.0.2
|
||||
VERSION=36.0.0
|
||||
FILENAME=platform-tools_r$VERSION-darwin.zip
|
||||
PROJECT_DIR=platform-tools-$VERSION-darwin
|
||||
SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78
|
||||
SHA256SUM=b241878e6ec20650b041bf715ea05f7d5dc73bd24529464bd9cf68946e3132bd
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
|
||||
VERSION=35.0.2
|
||||
VERSION=36.0.0
|
||||
FILENAME=platform-tools_r$VERSION-win.zip
|
||||
PROJECT_DIR=platform-tools-$VERSION-windows
|
||||
SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9
|
||||
SHA256SUM=24bd8bebbbb58b9870db202b5c6775c4a49992632021c60750d9d8ec8179d5f0
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ cd "$DEPS_DIR"
|
||||
. common
|
||||
process_args "$@"
|
||||
|
||||
VERSION=7.1
|
||||
VERSION=7.1.1
|
||||
FILENAME=ffmpeg-$VERSION.tar.xz
|
||||
PROJECT_DIR=ffmpeg-$VERSION
|
||||
SHA256SUM=40973D44970DBC83EF302B0609F2E74982BE2D85916DD2EE7472D30678A7ABE6
|
||||
SHA256SUM=733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ cd "$DEPS_DIR"
|
||||
. common
|
||||
process_args "$@"
|
||||
|
||||
VERSION=1.0.27
|
||||
VERSION=1.0.29
|
||||
FILENAME=libusb-$VERSION.tar.gz
|
||||
PROJECT_DIR=libusb-$VERSION
|
||||
SHA256SUM=e8f18a7a36ecbb11fb820bd71540350d8f61bcd9db0d2e8c18a6fb80b214a3de
|
||||
SHA256SUM=7c2dd39c0b2589236e48c93247c986ae272e27570942b4163cb00a060fcf1b74
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ cd "$DEPS_DIR"
|
||||
. common
|
||||
process_args "$@"
|
||||
|
||||
VERSION=2.30.10
|
||||
VERSION=2.32.8
|
||||
FILENAME=SDL-$VERSION.tar.gz
|
||||
PROJECT_DIR=SDL-release-$VERSION
|
||||
SHA256SUM=35a8b9c4f3635d85762b904ac60ca4e0806bff89faeb269caafbe80860d67168
|
||||
SHA256SUM=dd35e05644ae527848d02433bec24dd0ea65db59faecf1a0e5d1880c533dac2c
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ BEGIN
|
||||
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
||||
VALUE "OriginalFilename", "scrcpy.exe"
|
||||
VALUE "ProductName", "scrcpy"
|
||||
VALUE "ProductVersion", "3.1"
|
||||
VALUE "ProductVersion", "3.3"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
39
app/scrcpy.1
39
app/scrcpy.1
@@ -67,13 +67,19 @@ The available encoders can be listed by \fB\-\-list\-encoders\fR.
|
||||
|
||||
.TP
|
||||
.BI "\-\-audio\-source " source
|
||||
Select the audio source (output, mic or playback).
|
||||
Select the audio source. Possible values are:
|
||||
|
||||
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.
|
||||
- "output": forwards the whole audio output, and disables playback on the device.
|
||||
- "playback": captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured).
|
||||
- "mic": captures the microphone.
|
||||
- "mic-unprocessed": captures the microphone unprocessed (raw) sound.
|
||||
- "mic-camcorder": captures the microphone tuned for video recording, with the same orientation as the camera if available.
|
||||
- "mic-voice-recognition": captures the microphone tuned for voice recognition.
|
||||
- "mic-voice-communication": captures the microphone tuned for voice communications (it will for instance take advantage of echo cancellation or automatic gain control if available).
|
||||
- "voice-call": captures voice call.
|
||||
- "voice-call-uplink": captures voice call uplink only.
|
||||
- "voice-call-downlink": captures voice call downlink only.
|
||||
- "voice-performance": captures audio meant to be processed for live performance (karaoke), includes both the microphone and the device playback.
|
||||
|
||||
Default is output.
|
||||
|
||||
@@ -161,6 +167,19 @@ The available display ids can be listed by \fB\-\-list\-displays\fR.
|
||||
|
||||
Default is 0.
|
||||
|
||||
.TP
|
||||
.BI "\-\-display\-ime\-policy " value
|
||||
Set the policy for selecting where the IME should be displayed.
|
||||
|
||||
Possible values are "local", "fallback" and "hide":
|
||||
|
||||
- "local" means that the IME should appear on the local display.
|
||||
- "fallback" means that the IME should appear on a fallback display (the default display).
|
||||
- "hide" means that the IME should be hidden.
|
||||
|
||||
By default, the IME policy is left unchanged.
|
||||
|
||||
|
||||
.TP
|
||||
.BI "\-\-display\-orientation " value
|
||||
Set the initial display orientation.
|
||||
@@ -389,7 +408,7 @@ Disable video playback on the computer.
|
||||
|
||||
.TP
|
||||
.B \-\-no\-window
|
||||
Disable scrcpy window. Implies --no-video-playback and --no-control.
|
||||
Disable scrcpy window. Implies --no-video-playback.
|
||||
|
||||
.TP
|
||||
.BI "\-\-orientation " value
|
||||
@@ -491,6 +510,10 @@ The device serial number. Mandatory only if several devices are connected to adb
|
||||
.B \-S, \-\-turn\-screen\-off
|
||||
Turn the device screen off immediately.
|
||||
|
||||
.TP
|
||||
.B "\-\-screen\-off\-timeout " seconds
|
||||
Set the screen off timeout while scrcpy is running (restore the initial value on exit).
|
||||
|
||||
.TP
|
||||
.BI "\-\-shortcut\-mod " key\fR[+...]][,...]
|
||||
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
|
||||
@@ -829,7 +852,7 @@ Report bugs to <https://github.com/Genymobile/scrcpy/issues>.
|
||||
.SH COPYRIGHT
|
||||
Copyright \(co 2018 Genymobile <https://www.genymobile.com>
|
||||
|
||||
Copyright \(co 2018\-2024 Romain Vimont <rom@rom1v.com>
|
||||
Copyright \(co 2018\-2025 Romain Vimont <rom@rom1v.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0.
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ show_adb_installation_msg(void) {
|
||||
} pkg_managers[] = {
|
||||
{"apt", "apt install adb"},
|
||||
{"apt-get", "apt-get install adb"},
|
||||
{"brew", "brew cask install android-platform-tools"},
|
||||
{"brew", "brew install --cask android-platform-tools"},
|
||||
{"dnf", "dnf install android-tools"},
|
||||
{"emerge", "emerge dev-util/android-tools"},
|
||||
{"pacman", "pacman -S android-tools"},
|
||||
|
||||
@@ -76,8 +76,10 @@ sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out,
|
||||
// Wait until the buffer is filled up to at least target_buffering
|
||||
// before playing
|
||||
if (buffered_samples < ar->target_buffering) {
|
||||
LOGV("[Audio] Inserting initial buffering silence: %" PRIu32
|
||||
#ifdef SC_AUDIO_REGULATOR_DEBUG
|
||||
LOGD("[Audio] Inserting initial buffering silence: %" PRIu32
|
||||
" samples", out_samples);
|
||||
#endif
|
||||
// Delay playback starting to reach the target buffering. Fill the
|
||||
// whole buffer with silence (len is small compared to the
|
||||
// arbitrary margin value).
|
||||
@@ -98,8 +100,10 @@ sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out,
|
||||
// dropped to keep the latency minimal. However, this would cause very
|
||||
// audible glitches, so let the clock compensation restore the target
|
||||
// latency.
|
||||
#ifdef SC_AUDIO_REGULATOR_DEBUG
|
||||
LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples",
|
||||
silence);
|
||||
#endif
|
||||
memset(out + TO_BYTES(read), 0, TO_BYTES(silence));
|
||||
|
||||
bool received = atomic_load_explicit(&ar->received,
|
||||
@@ -137,6 +141,36 @@ bool
|
||||
sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
|
||||
SwrContext *swr_ctx = ar->swr_ctx;
|
||||
|
||||
uint32_t input_samples = frame->nb_samples;
|
||||
|
||||
assert(frame->pts >= 0);
|
||||
int64_t pts = frame->pts;
|
||||
if (ar->next_expected_pts && pts - ar->next_expected_pts > 100000) {
|
||||
LOGV("[Audio] Discontinuity detected: %" PRIi64 "µs",
|
||||
pts - ar->next_expected_pts);
|
||||
// More than 100ms: consider it as a discontinuity
|
||||
// (typically because silence packets were not captured)
|
||||
uint32_t can_read = sc_audiobuf_can_read(&ar->buf);
|
||||
if (input_samples + can_read < ar->target_buffering) {
|
||||
// Adjust buffering to the target value directly
|
||||
uint32_t silence = ar->target_buffering - can_read - input_samples;
|
||||
sc_audiobuf_write_silence(&ar->buf, silence);
|
||||
}
|
||||
|
||||
// Reset state
|
||||
ar->avg_buffering.avg = ar->target_buffering;
|
||||
int ret = swr_set_compensation(swr_ctx, 0, 0);
|
||||
(void) ret;
|
||||
assert(!ret); // disabling compensation should never fail
|
||||
ar->compensation_active = false;
|
||||
ar->samples_since_resync = 0;
|
||||
atomic_store_explicit(&ar->underflow, 0, memory_order_relaxed);
|
||||
}
|
||||
|
||||
int64_t packet_duration = input_samples * INT64_C(1000000)
|
||||
/ ar->sample_rate;
|
||||
ar->next_expected_pts = pts + packet_duration;
|
||||
|
||||
int64_t swr_delay = swr_get_delay(swr_ctx, ar->sample_rate);
|
||||
// No need to av_rescale_rnd(), input and output sample rates are the same.
|
||||
// Add more space (256) for clock compensation.
|
||||
@@ -209,6 +243,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
|
||||
if (played) {
|
||||
underflow = atomic_exchange_explicit(&ar->underflow, 0,
|
||||
memory_order_relaxed);
|
||||
ar->underflow_report += underflow;
|
||||
|
||||
max_buffered_samples = ar->target_buffering * 11 / 10
|
||||
+ 60 * ar->sample_rate / 1000 /* 60 ms */;
|
||||
@@ -255,7 +290,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
|
||||
}
|
||||
|
||||
// Number of samples added (or removed, if negative) for compensation
|
||||
int32_t instant_compensation = (int32_t) written - frame->nb_samples;
|
||||
int32_t instant_compensation = (int32_t) written - input_samples;
|
||||
// Inserting silence instantly increases buffering
|
||||
int32_t inserted_silence = (int32_t) underflow;
|
||||
// Dropping input samples instantly decreases buffering
|
||||
@@ -311,7 +346,9 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
|
||||
int abs_max_diff = distance / 50;
|
||||
diff = CLAMP(diff, -abs_max_diff, abs_max_diff);
|
||||
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
|
||||
" compensation=%d", ar->target_buffering, avg, can_read, diff);
|
||||
" compensation=%d (underflow=%" PRIu32 ")",
|
||||
ar->target_buffering, avg, can_read, diff, ar->underflow_report);
|
||||
ar->underflow_report = 0;
|
||||
|
||||
int ret = swr_set_compensation(swr_ctx, diff, distance);
|
||||
if (ret < 0) {
|
||||
@@ -394,7 +431,9 @@ sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size,
|
||||
atomic_init(&ar->played, false);
|
||||
atomic_init(&ar->received, false);
|
||||
atomic_init(&ar->underflow, 0);
|
||||
ar->underflow_report = 0;
|
||||
ar->compensation_active = false;
|
||||
ar->next_expected_pts = 0;
|
||||
|
||||
return true;
|
||||
|
||||
|
||||
@@ -46,6 +46,9 @@ struct sc_audio_regulator {
|
||||
// Number of silence samples inserted since the last received packet
|
||||
atomic_uint_least32_t underflow;
|
||||
|
||||
// Number of silence samples inserted since the last log
|
||||
uint32_t underflow_report;
|
||||
|
||||
// Non-zero compensation applied (only used by the receiver thread)
|
||||
bool compensation_active;
|
||||
|
||||
@@ -54,6 +57,9 @@ struct sc_audio_regulator {
|
||||
|
||||
// Set to true the first time samples are pulled by the player
|
||||
atomic_bool played;
|
||||
|
||||
// PTS of the next expected packet (useful to detect discontinuities)
|
||||
int64_t next_expected_pts;
|
||||
};
|
||||
|
||||
bool
|
||||
|
||||
135
app/src/cli.c
135
app/src/cli.c
@@ -113,6 +113,7 @@ enum {
|
||||
OPT_ANGLE,
|
||||
OPT_NO_VD_SYSTEM_DECORATIONS,
|
||||
OPT_NO_VD_DESTROY_CONTENT,
|
||||
OPT_DISPLAY_IME_POLICY,
|
||||
};
|
||||
|
||||
struct sc_option {
|
||||
@@ -216,13 +217,31 @@ static const struct sc_option options[] = {
|
||||
.longopt_id = OPT_AUDIO_SOURCE,
|
||||
.longopt = "audio-source",
|
||||
.argdesc = "source",
|
||||
.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 "
|
||||
.text = "Select the audio source. Possible values are:\n"
|
||||
" - \"output\": forwards the whole audio output, and disables "
|
||||
"playback on the device.\n"
|
||||
" - \"playback\": 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"
|
||||
" - \"mic\": captures the microphone.\n"
|
||||
" - \"mic-unprocessed\": captures the microphone unprocessed "
|
||||
"(raw) sound.\n"
|
||||
" - \"mic-camcorder\": captures the microphone tuned for video "
|
||||
"recording, with the same orientation as the camera if "
|
||||
"available.\n"
|
||||
" - \"mic-voice-recognition\": captures the microphone tuned "
|
||||
"for voice recognition.\n"
|
||||
" - \"mic-voice-communication\": captures the microphone tuned "
|
||||
"for voice communications (it will for instance take advantage "
|
||||
"of echo cancellation or automatic gain control if "
|
||||
"available).\n"
|
||||
" - \"voice-call\": captures voice call.\n"
|
||||
" - \"voice-call-uplink\": captures voice call uplink only.\n"
|
||||
" - \"voice-call-downlink\": captures voice call downlink "
|
||||
"only.\n"
|
||||
" - \"voice-performance\": captures audio meant to be "
|
||||
"processed for live performance (karaoke), includes both the "
|
||||
"microphone and the device playback.\n"
|
||||
"Default is output.",
|
||||
},
|
||||
{
|
||||
@@ -366,6 +385,19 @@ static const struct sc_option options[] = {
|
||||
" scrcpy --list-displays\n"
|
||||
"Default is 0.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_DISPLAY_IME_POLICY,
|
||||
.longopt = "display-ime-policy",
|
||||
.argdesc = "value",
|
||||
.text = "Set the policy for selecting where the IME should be "
|
||||
"displayed.\n"
|
||||
"Possible values are \"local\", \"fallback\" and \"hide\".\n"
|
||||
"\"local\" means that the IME should appear on the local "
|
||||
"display.\n"
|
||||
"\"fallback\" means that the IME should appear on a fallback "
|
||||
"display (the default display).\n"
|
||||
"\"hide\" means that the IME should be hidden.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_DISPLAY_ORIENTATION,
|
||||
.longopt = "display-orientation",
|
||||
@@ -689,8 +721,7 @@ static const struct sc_option options[] = {
|
||||
{
|
||||
.longopt_id = OPT_NO_WINDOW,
|
||||
.longopt = "no-window",
|
||||
.text = "Disable scrcpy window. Implies --no-video-playback and "
|
||||
"--no-control.",
|
||||
.text = "Disable scrcpy window. Implies --no-video-playback.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_ORIENTATION,
|
||||
@@ -1615,6 +1646,25 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_display_ime_policy(const char *s, enum sc_display_ime_policy *policy) {
|
||||
if (!strcmp(s, "local")) {
|
||||
*policy = SC_DISPLAY_IME_POLICY_LOCAL;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(s, "fallback")) {
|
||||
*policy = SC_DISPLAY_IME_POLICY_FALLBACK;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(s, "hide")) {
|
||||
*policy = SC_DISPLAY_IME_POLICY_HIDE;
|
||||
return true;
|
||||
}
|
||||
LOGE("Unsupported display IME policy: %s (expected local, fallback or "
|
||||
"hide)", s);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_orientation(const char *s, enum sc_orientation *orientation) {
|
||||
if (!strcmp(s, "0")) {
|
||||
@@ -2004,8 +2054,50 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LOGE("Unsupported audio source: %s (expected output, mic or playback)",
|
||||
optarg);
|
||||
if (!strcmp(optarg, "mic-unprocessed")) {
|
||||
*source = SC_AUDIO_SOURCE_MIC_UNPROCESSED;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(optarg, "mic-camcorder")) {
|
||||
*source = SC_AUDIO_SOURCE_MIC_CAMCORDER;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(optarg, "mic-voice-recognition")) {
|
||||
*source = SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(optarg, "mic-voice-communication")) {
|
||||
*source = SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(optarg, "voice-call")) {
|
||||
*source = SC_AUDIO_SOURCE_VOICE_CALL;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(optarg, "voice-call-uplink")) {
|
||||
*source = SC_AUDIO_SOURCE_VOICE_CALL_UPLINK;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(optarg, "voice-call-downlink")) {
|
||||
*source = SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(optarg, "voice-performance")) {
|
||||
*source = SC_AUDIO_SOURCE_VOICE_PERFORMANCE;
|
||||
return true;
|
||||
}
|
||||
|
||||
LOGE("Unsupported audio source: %s (expected output, mic, playback, "
|
||||
"mic-unprocessed, mic-camcorder, mic-voice-recognition, "
|
||||
"mic-voice-communication, voice-call, voice-call-uplink, "
|
||||
"voice-call-downlink, voice-performance)", optarg);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2723,6 +2815,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
case OPT_NO_VD_SYSTEM_DECORATIONS:
|
||||
opts->vd_system_decorations = false;
|
||||
break;
|
||||
case OPT_DISPLAY_IME_POLICY:
|
||||
if (!parse_display_ime_policy(optarg,
|
||||
&opts->display_ime_policy)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// getopt prints the error message on stderr
|
||||
return false;
|
||||
@@ -2761,9 +2859,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
#endif
|
||||
|
||||
if (!opts->window) {
|
||||
// Without window, there cannot be any video playback or control
|
||||
// Without window, there cannot be any video playback
|
||||
opts->video_playback = false;
|
||||
opts->control = false;
|
||||
// Controls are still possible, allowing for options like
|
||||
// --turn-screen-off
|
||||
}
|
||||
|
||||
if (!opts->video) {
|
||||
@@ -2978,6 +3077,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED) {
|
||||
LOGE("--display-ime-policy is only available with "
|
||||
"--video-source=display");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) {
|
||||
LOGE("Cannot specify both --camera-id and --camera-facing");
|
||||
return false;
|
||||
@@ -3019,6 +3124,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED
|
||||
&& opts->display_id == 0 && !opts->new_display) {
|
||||
LOGE("--display-ime-policy is only supported on a secondary display");
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -75,6 +75,14 @@
|
||||
# define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL
|
||||
#endif
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
# define SCRCPY_SDL_HAS_HINT_APP_NAME
|
||||
#endif
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 14)
|
||||
# define SCRCPY_SDL_HAS_HINT_AUDIO_DEVICE_APP_NAME
|
||||
#endif
|
||||
|
||||
#ifndef HAVE_STRDUP
|
||||
char *strdup(const char *s);
|
||||
#endif
|
||||
|
||||
@@ -127,10 +127,14 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
|
||||
return 32;
|
||||
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
|
||||
write_position(&buf[1], &msg->inject_scroll_event.position);
|
||||
int16_t hscroll =
|
||||
sc_float_to_i16fp(msg->inject_scroll_event.hscroll);
|
||||
int16_t vscroll =
|
||||
sc_float_to_i16fp(msg->inject_scroll_event.vscroll);
|
||||
// Accept values in the range [-16, 16].
|
||||
// Normalize to [-1, 1] in order to use sc_float_to_i16fp().
|
||||
float hscroll_norm = msg->inject_scroll_event.hscroll / 16;
|
||||
hscroll_norm = CLAMP(hscroll_norm, -1, 1);
|
||||
float vscroll_norm = msg->inject_scroll_event.vscroll / 16;
|
||||
vscroll_norm = CLAMP(vscroll_norm, -1, 1);
|
||||
int16_t hscroll = sc_float_to_i16fp(hscroll_norm);
|
||||
int16_t vscroll = sc_float_to_i16fp(vscroll_norm);
|
||||
sc_write16be(&buf[13], (uint16_t) hscroll);
|
||||
sc_write16be(&buf[15], (uint16_t) vscroll);
|
||||
sc_write32be(&buf[17], msg->inject_scroll_event.buttons);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position,
|
||||
// 1 byte for wheel motion
|
||||
#define SC_HID_MOUSE_INPUT_SIZE 4
|
||||
// 1 byte for wheel motion, 1 byte for hozizontal scrolling
|
||||
#define SC_HID_MOUSE_INPUT_SIZE 5
|
||||
|
||||
/**
|
||||
* Mouse descriptor from the specification:
|
||||
@@ -75,6 +75,21 @@ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = {
|
||||
// Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel)
|
||||
0x81, 0x06,
|
||||
|
||||
// Usage Page (Consumer Page)
|
||||
0x05, 0x0C,
|
||||
// Usage(AC Pan)
|
||||
0x0A, 0x38, 0x02,
|
||||
// Logical Minimum (-127)
|
||||
0x15, 0x81,
|
||||
// Logical Maximum (127)
|
||||
0x25, 0x7F,
|
||||
// Report Size (8)
|
||||
0x75, 0x08,
|
||||
// Report Count (1)
|
||||
0x95, 0x01,
|
||||
// Input (Data, Variable, Relative): 1 byte (AC Pan)
|
||||
0x81, 0x06,
|
||||
|
||||
// End Collection
|
||||
0xC0,
|
||||
|
||||
@@ -160,7 +175,8 @@ sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input,
|
||||
data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state);
|
||||
data[1] = CLAMP(event->xrel, -127, 127);
|
||||
data[2] = CLAMP(event->yrel, -127, 127);
|
||||
data[3] = 0; // wheel coordinates only used for scrolling
|
||||
data[3] = 0; // no vertical scrolling
|
||||
data[4] = 0; // no horizontal scrolling
|
||||
}
|
||||
|
||||
void
|
||||
@@ -172,22 +188,27 @@ sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input,
|
||||
data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state);
|
||||
data[1] = 0; // no x motion
|
||||
data[2] = 0; // no y motion
|
||||
data[3] = 0; // wheel coordinates only used for scrolling
|
||||
data[3] = 0; // no vertical scrolling
|
||||
data[4] = 0; // no horizontal scrolling
|
||||
}
|
||||
|
||||
void
|
||||
bool
|
||||
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
|
||||
const struct sc_mouse_scroll_event *event) {
|
||||
if (!event->vscroll_int && !event->hscroll_int) {
|
||||
// Need a full integral value for HID
|
||||
return false;
|
||||
}
|
||||
|
||||
sc_hid_mouse_input_init(hid_input);
|
||||
|
||||
uint8_t *data = hid_input->data;
|
||||
data[0] = 0; // buttons state irrelevant (and unknown)
|
||||
data[1] = 0; // no x motion
|
||||
data[2] = 0; // no y motion
|
||||
// In practice, vscroll is always -1, 0 or 1, but in theory other values
|
||||
// are possible
|
||||
data[3] = CLAMP(event->vscroll, -127, 127);
|
||||
// Horizontal scrolling ignored
|
||||
data[3] = CLAMP(event->vscroll_int, -127, 127);
|
||||
data[4] = CLAMP(event->hscroll_int, -127, 127);
|
||||
return true;
|
||||
}
|
||||
|
||||
void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) {
|
||||
|
||||
@@ -22,7 +22,7 @@ void
|
||||
sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input,
|
||||
const struct sc_mouse_click_event *event);
|
||||
|
||||
void
|
||||
bool
|
||||
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
|
||||
const struct sc_mouse_scroll_event *event);
|
||||
|
||||
|
||||
@@ -393,6 +393,8 @@ struct sc_mouse_scroll_event {
|
||||
struct sc_position position;
|
||||
float hscroll;
|
||||
float vscroll;
|
||||
int32_t hscroll_int;
|
||||
int32_t vscroll_int;
|
||||
uint8_t buttons_state; // bitwise-OR of sc_mouse_button values
|
||||
};
|
||||
|
||||
|
||||
@@ -897,12 +897,14 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
|
||||
struct sc_mouse_scroll_event evt = {
|
||||
.position = sc_input_manager_get_position(im, mouse_x, mouse_y),
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
.hscroll = CLAMP(event->preciseX, -1.0f, 1.0f),
|
||||
.vscroll = CLAMP(event->preciseY, -1.0f, 1.0f),
|
||||
.hscroll = event->preciseX,
|
||||
.vscroll = event->preciseY,
|
||||
#else
|
||||
.hscroll = CLAMP(event->x, -1, 1),
|
||||
.vscroll = CLAMP(event->y, -1, 1),
|
||||
.hscroll = event->x,
|
||||
.vscroll = event->y,
|
||||
#endif
|
||||
.hscroll_int = event->x,
|
||||
.vscroll_int = event->y,
|
||||
.buttons_state = im->mouse_buttons_state,
|
||||
};
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.capture_orientation_lock = SC_ORIENTATION_UNLOCKED,
|
||||
.display_orientation = SC_ORIENTATION_0,
|
||||
.record_orientation = SC_ORIENTATION_0,
|
||||
.display_ime_policy = SC_DISPLAY_IME_POLICY_UNDEFINED,
|
||||
.window_x = SC_WINDOW_POSITION_UNDEFINED,
|
||||
.window_y = SC_WINDOW_POSITION_UNDEFINED,
|
||||
.window_width = 0,
|
||||
|
||||
@@ -59,6 +59,14 @@ enum sc_audio_source {
|
||||
SC_AUDIO_SOURCE_OUTPUT,
|
||||
SC_AUDIO_SOURCE_MIC,
|
||||
SC_AUDIO_SOURCE_PLAYBACK,
|
||||
SC_AUDIO_SOURCE_MIC_UNPROCESSED,
|
||||
SC_AUDIO_SOURCE_MIC_CAMCORDER,
|
||||
SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION,
|
||||
SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION,
|
||||
SC_AUDIO_SOURCE_VOICE_CALL,
|
||||
SC_AUDIO_SOURCE_VOICE_CALL_UPLINK,
|
||||
SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK,
|
||||
SC_AUDIO_SOURCE_VOICE_PERFORMANCE,
|
||||
};
|
||||
|
||||
enum sc_camera_facing {
|
||||
@@ -89,6 +97,13 @@ enum sc_orientation_lock {
|
||||
SC_ORIENTATION_LOCKED_INITIAL, // lock to initial device orientation
|
||||
};
|
||||
|
||||
enum sc_display_ime_policy {
|
||||
SC_DISPLAY_IME_POLICY_UNDEFINED,
|
||||
SC_DISPLAY_IME_POLICY_LOCAL,
|
||||
SC_DISPLAY_IME_POLICY_FALLBACK,
|
||||
SC_DISPLAY_IME_POLICY_HIDE,
|
||||
};
|
||||
|
||||
static inline bool
|
||||
sc_orientation_is_mirror(enum sc_orientation orientation) {
|
||||
assert(!(orientation & ~7));
|
||||
@@ -251,6 +266,7 @@ struct scrcpy_options {
|
||||
enum sc_orientation_lock capture_orientation_lock;
|
||||
enum sc_orientation display_orientation;
|
||||
enum sc_orientation record_orientation;
|
||||
enum sc_display_ime_policy display_ime_policy;
|
||||
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
|
||||
int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
|
||||
uint16_t window_width;
|
||||
|
||||
@@ -107,6 +107,17 @@ sdl_set_hints(const char *render_driver) {
|
||||
LOGW("Could not set render driver");
|
||||
}
|
||||
|
||||
// App name used in various contexts (such as PulseAudio)
|
||||
#if defined(SCRCPY_SDL_HAS_HINT_APP_NAME)
|
||||
if (!SDL_SetHint(SDL_HINT_APP_NAME, "scrcpy")) {
|
||||
LOGW("Could not set app name");
|
||||
}
|
||||
#elif defined(SCRCPY_SDL_HAS_HINT_AUDIO_DEVICE_APP_NAME)
|
||||
if (!SDL_SetHint(SDL_HINT_AUDIO_DEVICE_APP_NAME, "scrcpy")) {
|
||||
LOGW("Could not set audio device app name");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Linear filtering
|
||||
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) {
|
||||
LOGW("Could not enable linear filtering");
|
||||
@@ -165,7 +176,7 @@ sdl_configure(bool video_playback, bool disable_screensaver) {
|
||||
}
|
||||
|
||||
static enum scrcpy_exit_code
|
||||
event_loop(struct scrcpy *s) {
|
||||
event_loop(struct scrcpy *s, bool has_screen) {
|
||||
SDL_Event event;
|
||||
while (SDL_WaitEvent(&event)) {
|
||||
switch (event.type) {
|
||||
@@ -197,7 +208,7 @@ event_loop(struct scrcpy *s) {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (!sc_screen_handle_event(&s->screen, &event)) {
|
||||
if (has_screen && !sc_screen_handle_event(&s->screen, &event)) {
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
}
|
||||
break;
|
||||
@@ -436,6 +447,7 @@ scrcpy(struct scrcpy_options *options) {
|
||||
.control = options->control,
|
||||
.display_id = options->display_id,
|
||||
.new_display = options->new_display,
|
||||
.display_ime_policy = options->display_ime_policy,
|
||||
.video = options->video,
|
||||
.audio = options->audio,
|
||||
.audio_dup = options->audio_dup,
|
||||
@@ -932,7 +944,7 @@ aoa_complete:
|
||||
}
|
||||
}
|
||||
|
||||
ret = event_loop(s);
|
||||
ret = event_loop(s, options->window);
|
||||
terminate_event_loop();
|
||||
LOGD("quit...");
|
||||
|
||||
|
||||
@@ -149,12 +149,43 @@ sc_server_get_audio_source_name(enum sc_audio_source audio_source) {
|
||||
return "mic";
|
||||
case SC_AUDIO_SOURCE_PLAYBACK:
|
||||
return "playback";
|
||||
case SC_AUDIO_SOURCE_MIC_UNPROCESSED:
|
||||
return "mic-unprocessed";
|
||||
case SC_AUDIO_SOURCE_MIC_CAMCORDER:
|
||||
return "mic-camcorder";
|
||||
case SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION:
|
||||
return "mic-voice-recognition";
|
||||
case SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION:
|
||||
return "mic-voice-communication";
|
||||
case SC_AUDIO_SOURCE_VOICE_CALL:
|
||||
return "voice-call";
|
||||
case SC_AUDIO_SOURCE_VOICE_CALL_UPLINK:
|
||||
return "voice-call-uplink";
|
||||
case SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK:
|
||||
return "voice-call-downlink";
|
||||
case SC_AUDIO_SOURCE_VOICE_PERFORMANCE:
|
||||
return "voice-performance";
|
||||
default:
|
||||
assert(!"unexpected audio source");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static const char *
|
||||
sc_server_get_display_ime_policy_name(enum sc_display_ime_policy policy) {
|
||||
switch (policy) {
|
||||
case SC_DISPLAY_IME_POLICY_LOCAL:
|
||||
return "local";
|
||||
case SC_DISPLAY_IME_POLICY_FALLBACK:
|
||||
return "fallback";
|
||||
case SC_DISPLAY_IME_POLICY_HIDE:
|
||||
return "hide";
|
||||
default:
|
||||
assert(!"unexpected display IME policy");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
validate_string(const char *s) {
|
||||
// The parameters values are passed as command line arguments to adb, so
|
||||
@@ -376,6 +407,10 @@ execute_server(struct sc_server *server,
|
||||
VALIDATE_STRING(params->new_display);
|
||||
ADD_PARAM("new_display=%s", params->new_display);
|
||||
}
|
||||
if (params->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED) {
|
||||
ADD_PARAM("display_ime_policy=%s",
|
||||
sc_server_get_display_ime_policy_name(params->display_ime_policy));
|
||||
}
|
||||
if (!params->vd_destroy_content) {
|
||||
ADD_PARAM("vd_destroy_content=false");
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ struct sc_server_params {
|
||||
bool control;
|
||||
uint32_t display_id;
|
||||
const char *new_display;
|
||||
enum sc_display_ime_policy display_ime_policy;
|
||||
bool video;
|
||||
bool audio;
|
||||
bool audio_dup;
|
||||
|
||||
@@ -55,7 +55,9 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
|
||||
struct sc_mouse_uhid *mouse = DOWNCAST(mp);
|
||||
|
||||
struct sc_hid_input hid_input;
|
||||
sc_hid_mouse_generate_input_from_scroll(&hid_input, event);
|
||||
if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll");
|
||||
}
|
||||
|
||||
@@ -42,7 +42,9 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
|
||||
struct sc_mouse_aoa *mouse = DOWNCAST(mp);
|
||||
|
||||
struct sc_hid_input hid_input;
|
||||
sc_hid_mouse_generate_input_from_scroll(&hid_input, event);
|
||||
if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sc_aoa_push_input(mouse->aoa, &hid_input)) {
|
||||
LOGW("Could not push AOA HID input (mouse scroll)");
|
||||
|
||||
@@ -164,8 +164,15 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen,
|
||||
|
||||
struct sc_mouse_scroll_event evt = {
|
||||
// .position not used for HID events
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
.hscroll = event->preciseX,
|
||||
.vscroll = event->preciseY,
|
||||
#else
|
||||
.hscroll = event->x,
|
||||
.vscroll = event->y,
|
||||
#endif
|
||||
.hscroll_int = event->x,
|
||||
.vscroll_int = event->y,
|
||||
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state),
|
||||
};
|
||||
|
||||
|
||||
@@ -116,3 +116,38 @@ sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_,
|
||||
|
||||
return samples_count;
|
||||
}
|
||||
|
||||
uint32_t
|
||||
sc_audiobuf_write_silence(struct sc_audiobuf *buf, uint32_t samples_count) {
|
||||
// Only the writer thread can write head, so memory_order_relaxed is
|
||||
// sufficient
|
||||
uint32_t head = atomic_load_explicit(&buf->head, memory_order_relaxed);
|
||||
|
||||
// The tail cursor is updated after the data is consumed by the reader
|
||||
uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire);
|
||||
|
||||
uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size;
|
||||
if (!can_write) {
|
||||
return 0;
|
||||
}
|
||||
if (samples_count > can_write) {
|
||||
samples_count = can_write;
|
||||
}
|
||||
|
||||
uint32_t right_count = buf->alloc_size - head;
|
||||
if (right_count > samples_count) {
|
||||
right_count = samples_count;
|
||||
}
|
||||
memset(buf->data + (head * buf->sample_size), 0,
|
||||
right_count * buf->sample_size);
|
||||
|
||||
if (samples_count > right_count) {
|
||||
uint32_t left_count = samples_count - right_count;
|
||||
memset(buf->data, 0, left_count * buf->sample_size);
|
||||
}
|
||||
|
||||
uint32_t new_head = (head + samples_count) % buf->alloc_size;
|
||||
atomic_store_explicit(&buf->head, new_head, memory_order_release);
|
||||
|
||||
return samples_count;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ uint32_t
|
||||
sc_audiobuf_write(struct sc_audiobuf *buf, const void *from,
|
||||
uint32_t samples_count);
|
||||
|
||||
uint32_t
|
||||
sc_audiobuf_write_silence(struct sc_audiobuf *buf, uint32_t samples);
|
||||
|
||||
static inline uint32_t
|
||||
sc_audiobuf_capacity(struct sc_audiobuf *buf) {
|
||||
assert(buf->alloc_size);
|
||||
|
||||
@@ -113,6 +113,14 @@ static void test_audiobuf_partial_read_write(void) {
|
||||
uint32_t expected2[] = {4, 5, 6, 1, 2, 3, 4, 1, 2, 3};
|
||||
assert(!memcmp(data, expected2, 12));
|
||||
|
||||
w = sc_audiobuf_write_silence(&buf, 4);
|
||||
assert(w == 4);
|
||||
|
||||
r = sc_audiobuf_read(&buf, data, 4);
|
||||
assert(r == 4);
|
||||
uint32_t expected3[] = {0, 0, 0, 0};
|
||||
assert(!memcmp(data, expected3, 4));
|
||||
|
||||
sc_audiobuf_destroy(&buf);
|
||||
}
|
||||
|
||||
|
||||
14
doc/audio.md
14
doc/audio.md
@@ -66,6 +66,20 @@ the computer:
|
||||
scrcpy --audio-source=mic --no-video --no-playback --record=file.opus
|
||||
```
|
||||
|
||||
Many sources are available:
|
||||
|
||||
- `output` (default): forwards the whole audio output, and disables playback on the device (mapped to [`REMOTE_SUBMIX`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#REMOTE_SUBMIX)).
|
||||
- `playback`: captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured).
|
||||
- `mic`: captures the microphone (mapped to [`MIC`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#MIC)).
|
||||
- `mic-unprocessed`: captures the microphone unprocessed (raw) sound (mapped to [`UNPROCESSED`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#UNPROCESSED)).
|
||||
- `mic-camcorder`: captures the microphone tuned for video recording, with the same orientation as the camera if available (mapped to [`CAMCORDER`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#CAMCORDER)).
|
||||
- `mic-voice-recognition`: captures the microphone tuned for voice recognition (mapped to [`VOICE_RECOGNITION`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_RECOGNITION)).
|
||||
- `mic-voice-communication`: captures the microphone tuned for voice communications (it will for instance take advantage of echo cancellation or automatic gain control if available) (mapped to [`VOICE_COMMUNICATION`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION)).
|
||||
- `voice-call`: captures voice call (mapped to [`VOICE_CALL`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_CALL)).
|
||||
- `voice-call-uplink`: captures voice call uplink only (mapped to [`VOICE_UPLINK`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_UPLINK)).
|
||||
- `voice-call-downlink`: captures voice call downlink only (mapped to [`VOICE_DOWNLINK`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_DOWNLINK)).
|
||||
- `voice-performance`: captures audio meant to be processed for live performance (karaoke), includes both the microphone and the device playback (mapped to [`VOICE_PERFORMANCE`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_PERFORMANCE)).
|
||||
|
||||
### Duplication
|
||||
|
||||
An alternative device audio capture method is also available (only for Android
|
||||
|
||||
@@ -233,10 +233,10 @@ install` must be run as root)._
|
||||
|
||||
#### Option 2: Use prebuilt server
|
||||
|
||||
- [`scrcpy-server-v3.1`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0`</sub>
|
||||
- [`scrcpy-server-v3.3`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `351cb2edc7e4c2c75f09a7933fdabcf137be52e2602df154f24ec02db46e9e51`</sub>
|
||||
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-server-v3.1
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-server-v3.3
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
|
||||
@@ -113,16 +113,17 @@ with the device IP address you found)_.
|
||||
7. Run `scrcpy` as usual.
|
||||
8. Run `adb disconnect` once you're done.
|
||||
|
||||
Since Android 11, a [wireless debugging option][adb-wireless] allows to bypass
|
||||
having to physically connect your device directly to your computer.
|
||||
Since Android 11, a [wireless debugging option][adb-wireless] allows you to
|
||||
bypass having to physically connect your device to your computer.
|
||||
|
||||
[adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line
|
||||
|
||||
|
||||
## Autostart
|
||||
|
||||
A small tool (by the scrcpy author) allows to run arbitrary commands whenever a
|
||||
new Android device is connected: [AutoAdb]. It can be used to start scrcpy:
|
||||
A small tool (by the scrcpy author) allows you to run arbitrary commands
|
||||
whenever a new Android device is connected: [AutoAdb]. It can be used to start
|
||||
scrcpy:
|
||||
|
||||
```bash
|
||||
autoadb scrcpy -s '{}'
|
||||
|
||||
@@ -34,6 +34,31 @@ adb shell settings put global stay_on_while_plugged_in 0
|
||||
```
|
||||
|
||||
|
||||
## Screen off timeout
|
||||
|
||||
The Android screen automatically turns off after some delay.
|
||||
|
||||
To change this delay while scrcpy is running:
|
||||
|
||||
```bash
|
||||
scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes)
|
||||
```
|
||||
|
||||
The initial value is restored on exit.
|
||||
|
||||
It is possible to change this setting manually:
|
||||
|
||||
```bash
|
||||
# get the current screen_off_timeout value
|
||||
adb shell settings get system screen_off_timeout
|
||||
# set a new value (in milliseconds)
|
||||
adb shell settings put system screen_off_timeout 30000
|
||||
```
|
||||
|
||||
Note that the Android value is in milliseconds, but the scrcpy command line
|
||||
argument is in seconds.
|
||||
|
||||
|
||||
## Turn screen off
|
||||
|
||||
It is possible to turn the device screen off while mirroring on start with a
|
||||
@@ -71,31 +96,6 @@ adb shell cmd display power-on 0
|
||||
```
|
||||
|
||||
|
||||
## Screen off timeout
|
||||
|
||||
The Android screen automatically turns off after some delay.
|
||||
|
||||
To change this delay while scrcpy is running:
|
||||
|
||||
```bash
|
||||
scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes)
|
||||
```
|
||||
|
||||
The initial value is restored on exit.
|
||||
|
||||
It is possible to change this setting manually:
|
||||
|
||||
```bash
|
||||
# get the current screen_off_timeout value
|
||||
adb shell settings get system screen_off_timeout
|
||||
# set a new value (in milliseconds)
|
||||
adb shell settings put system screen_off_timeout 30000
|
||||
```
|
||||
|
||||
Note that the Android value is in milliseconds, but the scrcpy command line
|
||||
argument is in seconds.
|
||||
|
||||
|
||||
## Show touches
|
||||
|
||||
For presentations, it may be useful to show physical touches (on the physical
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
Download a static build of the [latest release]:
|
||||
|
||||
- [`scrcpy-linux-x86_64-v3.1.tar.gz`][direct-linux-x86_64] (x86_64)
|
||||
<sub>SHA-256: `37dba54092ed9ec6b2f8f95432f61b8ea124aec9f1e9f2b3d22d4b10bb04c59a`</sub>
|
||||
- [`scrcpy-linux-x86_64-v3.3.tar.gz`][direct-linux-x86_64] (x86_64)
|
||||
<sub>SHA-256: `a0abf37003c3c47a53c1b2a12420296a2b0ee323cf3610fd6fbf9d9bab9d99f3`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-linux-x86_64-v3.1.tar.gz
|
||||
[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-linux-x86_64-v3.3.tar.gz
|
||||
|
||||
and extract it.
|
||||
|
||||
@@ -27,7 +27,7 @@ Scrcpy is packaged in several distributions and package managers:
|
||||
- Arch Linux: `pacman -S scrcpy`
|
||||
- Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy`
|
||||
- Gentoo: `emerge scrcpy`
|
||||
- Snap: `snap install scrcpy`
|
||||
- Snap: ~~`snap install scrcpy`~~ _(obsolete version)_
|
||||
- … (see [repology](https://repology.org/project/scrcpy/versions))
|
||||
|
||||
|
||||
|
||||
12
doc/macos.md
12
doc/macos.md
@@ -6,15 +6,15 @@
|
||||
|
||||
Download a static build of the [latest release]:
|
||||
|
||||
- [`scrcpy-macos-aarch64-v3.1.tar.gz`][direct-macos-aarch64] (aarch64)
|
||||
<sub>SHA-256: `478618d940421e5f57942f5479d493ecbb38210682937a200f712aee5f235daf`</sub>
|
||||
- [`scrcpy-macos-aarch64-v3.3.tar.gz`][direct-macos-aarch64] (aarch64)
|
||||
<sub>SHA-256: `7a4cdaeb8ba74593edda278c000ddedc8d70a51263a80b16a6345475d42ac21e`</sub>
|
||||
|
||||
- [`scrcpy-macos-x86_64-v3.1.tar.gz`][direct-macos-x86_64] (x86_64)
|
||||
<sub>SHA-256: `acde98e29c273710ffa469371dbca4a728a44c41c380381f8a54e5b5301b9e87`</sub>
|
||||
- [`scrcpy-macos-x86_64-v3.3.tar.gz`][direct-macos-x86_64] (x86_64)
|
||||
<sub>SHA-256: `bb3c13aac166b92539371883a8781aa861a7cd18e3e6077e570ab7a1f562f774`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-macos-aarch64-v3.1.tar.gz
|
||||
[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-macos-x86_64-v3.1.tar.gz
|
||||
[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-macos-aarch64-v3.3.tar.gz
|
||||
[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-macos-x86_64-v3.3.tar.gz
|
||||
|
||||
and extract it.
|
||||
|
||||
|
||||
12
doc/mouse.md
12
doc/mouse.md
@@ -83,9 +83,9 @@ process like the _adb daemon_).
|
||||
## Mouse bindings
|
||||
|
||||
By default, with SDK mouse:
|
||||
- right-click triggers BACK (or POWER on)
|
||||
- middle-click triggers HOME
|
||||
- the 4th click triggers APP_SWITCH
|
||||
- right-click triggers `BACK` (or `POWER` on)
|
||||
- middle-click triggers `HOME`
|
||||
- the 4th click triggers `APP_SWITCH`
|
||||
- the 5th click expands the notification panel
|
||||
|
||||
The secondary clicks may be forwarded to the device instead by pressing the
|
||||
@@ -121,9 +121,9 @@ Each character must be one of the following:
|
||||
|
||||
- `+`: forward the click to the device
|
||||
- `-`: ignore the click
|
||||
- `b`: trigger shortcut BACK (or turn screen on if off)
|
||||
- `h`: trigger shortcut HOME
|
||||
- `s`: trigger shortcut APP_SWITCH
|
||||
- `b`: trigger shortcut `BACK` (or turn screen on if off)
|
||||
- `h`: trigger shortcut `HOME`
|
||||
- `s`: trigger shortcut `APP_SWITCH`
|
||||
- `n`: trigger shortcut "expand notification panel"
|
||||
|
||||
For example:
|
||||
|
||||
@@ -11,6 +11,8 @@ scrcpy --new-display # use the main display size and density
|
||||
scrcpy --new-display=/240 # use the main display size and 240 dpi
|
||||
```
|
||||
|
||||
The new virtual display is destroyed on exit.
|
||||
|
||||
## Start app
|
||||
|
||||
On some devices, a launcher is available in the virtual display.
|
||||
@@ -61,3 +63,15 @@ To move them to the main display instead, use:
|
||||
```
|
||||
scrcpy --new-display --no-vd-destroy-content
|
||||
```
|
||||
|
||||
|
||||
## Display IME policy
|
||||
|
||||
By default, the virtual display IME appears on the default display.
|
||||
|
||||
To make it appear on the local display, use `--display-ime-policy=local`:
|
||||
|
||||
```bash
|
||||
scrcpy --display-id=1 --display-ime-policy=local
|
||||
scrcpy --new-display --display-ime-policy=local
|
||||
```
|
||||
|
||||
@@ -6,20 +6,26 @@
|
||||
|
||||
Download the [latest release]:
|
||||
|
||||
- [`scrcpy-win64-v3.1.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `0c05ea395d95cfe36bee974eeb435a3db87ea5594ff738370d5dc3068a9538ca`</sub>
|
||||
- [`scrcpy-win32-v3.1.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `2b4674ef76719680ac5a9b482d1943bdde3fa25821ad2e98f3c40c347d00d560`</sub>
|
||||
- [`scrcpy-win64-v3.3.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `a120cb4be7cde2891af38e83d2008173a0b6b6b5e344b2dfe668d0f892999933`</sub>
|
||||
- [`scrcpy-win32-v3.3.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `e409ab83f8c57bd6ac741d652635cab7699fcf3d384e233833872f117b993ca6`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-win64-v3.1.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-win32-v3.1.zip
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-win64-v3.3.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-win32-v3.3.zip
|
||||
|
||||
and extract it.
|
||||
|
||||
|
||||
### From a package manager
|
||||
|
||||
From [WinGet] (ADB and other dependencies will be installed alongside scrcpy):
|
||||
|
||||
```bash
|
||||
winget install --exact Genymobile.scrcpy
|
||||
```
|
||||
|
||||
From [Chocolatey]:
|
||||
|
||||
```bash
|
||||
@@ -29,12 +35,12 @@ choco install adb # if you don't have it yet
|
||||
|
||||
From [Scoop]:
|
||||
|
||||
|
||||
```bash
|
||||
scoop install scrcpy
|
||||
scoop install adb # if you don't have it yet
|
||||
```
|
||||
|
||||
[WinGet]: https://github.com/microsoft/winget-cli
|
||||
[Chocolatey]: https://chocolatey.org/
|
||||
[Scoop]: https://scoop.sh
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
set -e
|
||||
|
||||
BUILDDIR=build-auto
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-server-v3.1
|
||||
PREBUILT_SERVER_SHA256=958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-server-v3.3
|
||||
PREBUILT_SERVER_SHA256=351cb2edc7e4c2c75f09a7933fdabcf137be52e2602df154f24ec02db46e9e51
|
||||
|
||||
echo "[scrcpy] Downloading prebuilt server..."
|
||||
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
project('scrcpy', 'c',
|
||||
version: '3.1',
|
||||
version: '3.3',
|
||||
meson_version: '>= 0.49',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "com.genymobile.scrcpy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 35
|
||||
versionCode 30100
|
||||
versionName "3.1"
|
||||
versionCode 30300
|
||||
versionName "3.3"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
set -e
|
||||
|
||||
SCRCPY_DEBUG=false
|
||||
SCRCPY_VERSION_NAME=3.1
|
||||
SCRCPY_VERSION_NAME=3.3
|
||||
|
||||
PLATFORM=${ANDROID_PLATFORM:-35}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0}
|
||||
@@ -47,10 +47,8 @@ EOF
|
||||
|
||||
echo "Generating java from aidl..."
|
||||
cd "$SERVER_DIR/src/main/aidl"
|
||||
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IRotationWatcher.aidl
|
||||
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \
|
||||
android/content/IOnPrimaryClipChangedListener.aidl
|
||||
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl
|
||||
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. -p "$ANDROID_AIDL" \
|
||||
android/view/IDisplayWindowListener.aidl
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.view;
|
||||
|
||||
/**
|
||||
* {@hide}
|
||||
*/
|
||||
oneway interface IDisplayFoldListener
|
||||
{
|
||||
/** Called when the foldedness of a display changes */
|
||||
void onDisplayFoldChanged(int displayId, boolean folded);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/* //device/java/android/android/hardware/ISensorListener.aidl
|
||||
**
|
||||
** Copyright 2008, The Android Open Source Project
|
||||
**
|
||||
** Licensed under the Apache License, Version 2.0 (the "License");
|
||||
** you may not use this file except in compliance with the License.
|
||||
** You may obtain a copy of the License at
|
||||
**
|
||||
** http://www.apache.org/licenses/LICENSE-2.0
|
||||
**
|
||||
** Unless required by applicable law or agreed to in writing, software
|
||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
*/
|
||||
|
||||
package android.view;
|
||||
|
||||
/**
|
||||
* {@hide}
|
||||
*/
|
||||
interface IRotationWatcher {
|
||||
oneway void onRotationChanged(int rotation);
|
||||
}
|
||||
@@ -4,8 +4,10 @@ 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 com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Looper;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
|
||||
@@ -97,18 +99,31 @@ public final class CleanUp {
|
||||
}
|
||||
}
|
||||
|
||||
boolean powerOffScreen = options.getPowerOffScreenOnClose();
|
||||
int displayId = options.getDisplayId();
|
||||
|
||||
int restoreDisplayImePolicy = -1;
|
||||
if (displayId > 0) {
|
||||
int displayImePolicy = options.getDisplayImePolicy();
|
||||
if (displayImePolicy != -1) {
|
||||
int currentDisplayImePolicy = ServiceManager.getWindowManager().getDisplayImePolicy(displayId);
|
||||
if (currentDisplayImePolicy != displayImePolicy) {
|
||||
ServiceManager.getWindowManager().setDisplayImePolicy(displayId, displayImePolicy);
|
||||
restoreDisplayImePolicy = currentDisplayImePolicy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean powerOffScreen = options.getPowerOffScreenOnClose();
|
||||
|
||||
try {
|
||||
run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout);
|
||||
run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout, restoreDisplayImePolicy);
|
||||
} catch (IOException e) {
|
||||
Ln.e("Clean up I/O exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout)
|
||||
throws IOException {
|
||||
private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout,
|
||||
int restoreDisplayImePolicy) throws IOException {
|
||||
String[] cmd = {
|
||||
"app_process",
|
||||
"/",
|
||||
@@ -118,6 +133,7 @@ public final class CleanUp {
|
||||
String.valueOf(disableShowTouches),
|
||||
String.valueOf(powerOffScreen),
|
||||
String.valueOf(restoreScreenOffTimeout),
|
||||
String.valueOf(restoreDisplayImePolicy),
|
||||
};
|
||||
|
||||
ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||
@@ -164,6 +180,11 @@ public final class CleanUp {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static void prepareMainLooper() {
|
||||
Looper.prepareMainLooper();
|
||||
}
|
||||
|
||||
public static void main(String... args) {
|
||||
try {
|
||||
// Start a new session to avoid being terminated along with the server process on some devices
|
||||
@@ -173,11 +194,15 @@ public final class CleanUp {
|
||||
}
|
||||
unlinkSelf();
|
||||
|
||||
// Needed for workarounds
|
||||
prepareMainLooper();
|
||||
|
||||
int displayId = Integer.parseInt(args[0]);
|
||||
int restoreStayOn = Integer.parseInt(args[1]);
|
||||
boolean disableShowTouches = Boolean.parseBoolean(args[2]);
|
||||
boolean powerOffScreen = Boolean.parseBoolean(args[3]);
|
||||
int restoreScreenOffTimeout = Integer.parseInt(args[4]);
|
||||
int restoreDisplayImePolicy = Integer.parseInt(args[5]);
|
||||
|
||||
// Dynamic option
|
||||
boolean restoreDisplayPower = false;
|
||||
@@ -223,6 +248,11 @@ public final class CleanUp {
|
||||
}
|
||||
}
|
||||
|
||||
if (restoreDisplayImePolicy != -1) {
|
||||
Ln.i("Restoring \"display IME policy\"");
|
||||
ServiceManager.getWindowManager().setDisplayImePolicy(displayId, restoreDisplayImePolicy);
|
||||
}
|
||||
|
||||
// Change the power of the main display when mirroring a virtual display
|
||||
int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0;
|
||||
if (Device.isScreenOn(targetDisplayId)) {
|
||||
|
||||
@@ -2,8 +2,10 @@ package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.AttributionSource;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
@@ -11,6 +13,8 @@ import android.content.IContentProvider;
|
||||
import android.os.Binder;
|
||||
import android.os.Process;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public final class FakeContext extends ContextWrapper {
|
||||
|
||||
public static final String PACKAGE_NAME = "com.android.shell";
|
||||
@@ -91,4 +95,25 @@ public final class FakeContext extends ContextWrapper {
|
||||
public ContentResolver getContentResolver() {
|
||||
return contentResolver;
|
||||
}
|
||||
|
||||
@SuppressLint("SoonBlockedPrivateApi")
|
||||
@Override
|
||||
public Object getSystemService(String name) {
|
||||
Object service = super.getSystemService(name);
|
||||
if (service == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Context.CLIPBOARD_SERVICE.equals(name)) {
|
||||
try {
|
||||
Field field = ClipboardManager.class.getDeclaredField("mContext");
|
||||
field.setAccessible(true);
|
||||
field.set(service, this);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 com.genymobile.scrcpy.wrappers.WindowManager;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.util.Pair;
|
||||
@@ -48,6 +49,7 @@ public class Options {
|
||||
private boolean showTouches;
|
||||
private boolean stayAwake;
|
||||
private int screenOffTimeout = -1;
|
||||
private int displayImePolicy = -1;
|
||||
private List<CodecOption> videoCodecOptions;
|
||||
private List<CodecOption> audioCodecOptions;
|
||||
|
||||
@@ -186,6 +188,10 @@ public class Options {
|
||||
return screenOffTimeout;
|
||||
}
|
||||
|
||||
public int getDisplayImePolicy() {
|
||||
return displayImePolicy;
|
||||
}
|
||||
|
||||
public List<CodecOption> getVideoCodecOptions() {
|
||||
return videoCodecOptions;
|
||||
}
|
||||
@@ -482,6 +488,9 @@ public class Options {
|
||||
options.captureOrientationLock = pair.first;
|
||||
options.captureOrientation = pair.second;
|
||||
break;
|
||||
case "display_ime_policy":
|
||||
options.displayImePolicy = parseDisplayImePolicy(value);
|
||||
break;
|
||||
case "send_device_meta":
|
||||
options.sendDeviceMeta = Boolean.parseBoolean(value);
|
||||
break;
|
||||
@@ -626,4 +635,17 @@ public class Options {
|
||||
|
||||
return Pair.create(lock, Orientation.getByName(value));
|
||||
}
|
||||
|
||||
private static int parseDisplayImePolicy(String value) {
|
||||
switch (value) {
|
||||
case "local":
|
||||
return WindowManager.DISPLAY_IME_POLICY_LOCAL;
|
||||
case "fallback":
|
||||
return WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
|
||||
case "hide":
|
||||
return WindowManager.DISPLAY_IME_POLICY_HIDE;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid display IME policy: " + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,13 @@ import com.genymobile.scrcpy.video.SurfaceCapture;
|
||||
import com.genymobile.scrcpy.video.SurfaceEncoder;
|
||||
import com.genymobile.scrcpy.video.VideoSource;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -55,17 +58,7 @@ public final class Server {
|
||||
this.fatalError = true;
|
||||
}
|
||||
if (running == 0 || this.fatalError) {
|
||||
notify();
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void await() {
|
||||
try {
|
||||
while (running > 0 && !fatalError) {
|
||||
wait();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
Looper.getMainLooper().quitSafely();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,9 +73,15 @@ public final class Server {
|
||||
throw new ConfigurationException("Camera mirroring is not supported");
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10 && options.getNewDisplay() != null) {
|
||||
Ln.e("New virtual display is not supported before Android 10");
|
||||
throw new ConfigurationException("New virtual display is not supported");
|
||||
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
|
||||
if (options.getNewDisplay() != null) {
|
||||
Ln.e("New virtual display is not supported before Android 10");
|
||||
throw new ConfigurationException("New virtual display is not supported");
|
||||
}
|
||||
if (options.getDisplayImePolicy() != -1) {
|
||||
Ln.e("Display IME policy is not supported before Android 10");
|
||||
throw new ConfigurationException("Display IME policy is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
CleanUp cleanUp = null;
|
||||
@@ -166,7 +165,7 @@ public final class Server {
|
||||
});
|
||||
}
|
||||
|
||||
completion.await();
|
||||
Looper.loop(); // interrupted by the Completion implementation
|
||||
} finally {
|
||||
if (cleanUp != null) {
|
||||
cleanUp.interrupt();
|
||||
@@ -195,6 +194,21 @@ public final class Server {
|
||||
}
|
||||
}
|
||||
|
||||
private static void prepareMainLooper() {
|
||||
// Like Looper.prepareMainLooper(), but with quitAllowed set to true
|
||||
Looper.prepare();
|
||||
synchronized (Looper.class) {
|
||||
try {
|
||||
@SuppressLint("DiscouragedPrivateApi")
|
||||
Field field = Looper.class.getDeclaredField("sMainLooper");
|
||||
field.setAccessible(true);
|
||||
field.set(null, Looper.myLooper());
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String... args) {
|
||||
int status = 0;
|
||||
try {
|
||||
@@ -215,6 +229,8 @@ public final class Server {
|
||||
Ln.e("Exception on thread " + t, e);
|
||||
});
|
||||
|
||||
prepareMainLooper();
|
||||
|
||||
Options options = Options.parse(args);
|
||||
|
||||
Ln.disableSystemStreams();
|
||||
|
||||
@@ -29,8 +29,6 @@ public final class Workarounds {
|
||||
private static final Object ACTIVITY_THREAD;
|
||||
|
||||
static {
|
||||
prepareMainLooper();
|
||||
|
||||
try {
|
||||
// ActivityThread activityThread = new ActivityThread();
|
||||
ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread");
|
||||
@@ -77,19 +75,6 @@ public final class Workarounds {
|
||||
fillAppContext();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static void prepareMainLooper() {
|
||||
// Some devices internally create a Handler when creating an input Surface, causing an exception:
|
||||
// "Can't create handler inside thread that has not called Looper.prepare()"
|
||||
// <https://github.com/Genymobile/scrcpy/issues/240>
|
||||
//
|
||||
// Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException:
|
||||
// "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue'
|
||||
// on a null object reference"
|
||||
// <https://github.com/Genymobile/scrcpy/issues/921>
|
||||
Looper.prepareMainLooper();
|
||||
}
|
||||
|
||||
private static void fillAppInfo() {
|
||||
try {
|
||||
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
|
||||
|
||||
@@ -12,7 +12,6 @@ import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.media.AudioRecord;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaRecorder;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
|
||||
@@ -32,18 +31,7 @@ public class AudioDirectCapture implements AudioCapture {
|
||||
private AudioRecordReader reader;
|
||||
|
||||
public AudioDirectCapture(AudioSource audioSource) {
|
||||
this.audioSource = getAudioSourceValue(audioSource);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
this.audioSource = audioSource.getDirectAudioSource();
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_23_ANDROID_6_0)
|
||||
|
||||
@@ -55,6 +55,9 @@ public final class AudioEncoder implements AsyncProcessor {
|
||||
private final List<CodecOption> codecOptions;
|
||||
private final String encoderName;
|
||||
|
||||
private boolean recreatePts;
|
||||
private long previousPts;
|
||||
|
||||
// Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4).
|
||||
// So many pending tasks would lead to an unacceptable delay anyway.
|
||||
private final BlockingQueue<InputTask> inputTasks = new ArrayBlockingQueue<>(64);
|
||||
@@ -118,6 +121,9 @@ public final class AudioEncoder implements AsyncProcessor {
|
||||
OutputTask task = outputTasks.take();
|
||||
ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index);
|
||||
try {
|
||||
if (recreatePts) {
|
||||
fixTimestamp(task.bufferInfo);
|
||||
}
|
||||
streamer.writePacket(buffer, task.bufferInfo);
|
||||
} finally {
|
||||
mediaCodec.releaseOutputBuffer(task.index, false);
|
||||
@@ -125,6 +131,25 @@ public final class AudioEncoder implements AsyncProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private void fixTimestamp(MediaCodec.BufferInfo bufferInfo) {
|
||||
assert recreatePts;
|
||||
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
||||
// Config packet, nothing to fix
|
||||
return;
|
||||
}
|
||||
|
||||
long pts = bufferInfo.presentationTimeUs;
|
||||
if (previousPts != 0) {
|
||||
long now = System.nanoTime() / 1000;
|
||||
// This specific encoder produces PTS matching the exact number of samples
|
||||
long duration = pts - previousPts;
|
||||
bufferInfo.presentationTimeUs = now - duration;
|
||||
}
|
||||
|
||||
previousPts = pts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(TerminationListener listener) {
|
||||
thread = new Thread(() -> {
|
||||
@@ -194,6 +219,12 @@ public final class AudioEncoder implements AsyncProcessor {
|
||||
Codec codec = streamer.getCodec();
|
||||
mediaCodec = createMediaCodec(codec, encoderName);
|
||||
|
||||
// The default OPUS and FLAC encoders overwrite the input PTS with a value that matches the number of samples. This is not the behavior
|
||||
// we want: it ignores any audio clock drift and hard silences (packets not produced on silence). To work around this behavior,
|
||||
// regenerate PTS based on the current time and the packet duration.
|
||||
String codecName = mediaCodec.getCanonicalName();
|
||||
recreatePts = "c2.android.opus.encoder".equals(codecName) || "c2.android.flac.encoder".equals(codecName);
|
||||
|
||||
mediaCodecThread = new HandlerThread("media-codec");
|
||||
mediaCodecThread.start();
|
||||
|
||||
|
||||
@@ -1,20 +1,38 @@
|
||||
package com.genymobile.scrcpy.audio;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.media.MediaRecorder;
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
public enum AudioSource {
|
||||
OUTPUT("output"),
|
||||
MIC("mic"),
|
||||
PLAYBACK("playback");
|
||||
OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX),
|
||||
MIC("mic", MediaRecorder.AudioSource.MIC),
|
||||
PLAYBACK("playback", -1),
|
||||
MIC_UNPROCESSED("mic-unprocessed", MediaRecorder.AudioSource.UNPROCESSED),
|
||||
MIC_CAMCORDER("mic-camcorder", MediaRecorder.AudioSource.CAMCORDER),
|
||||
MIC_VOICE_RECOGNITION("mic-voice-recognition", MediaRecorder.AudioSource.VOICE_RECOGNITION),
|
||||
MIC_VOICE_COMMUNICATION("mic-voice-communication", MediaRecorder.AudioSource.VOICE_COMMUNICATION),
|
||||
VOICE_CALL("voice-call", MediaRecorder.AudioSource.VOICE_CALL),
|
||||
VOICE_CALL_UPLINK("voice-call-uplink", MediaRecorder.AudioSource.VOICE_UPLINK),
|
||||
VOICE_CALL_DOWNLINK("voice-call-downlink", MediaRecorder.AudioSource.VOICE_DOWNLINK),
|
||||
VOICE_PERFORMANCE("voice-performance", MediaRecorder.AudioSource.VOICE_PERFORMANCE);
|
||||
|
||||
private final String name;
|
||||
private final int directAudioSource;
|
||||
|
||||
AudioSource(String name) {
|
||||
AudioSource(String name, int directAudioSource) {
|
||||
this.name = name;
|
||||
this.directAudioSource = directAudioSource;
|
||||
}
|
||||
|
||||
public boolean isDirect() {
|
||||
return this != PLAYBACK;
|
||||
}
|
||||
|
||||
public int getDirectAudioSource() {
|
||||
return directAudioSource;
|
||||
}
|
||||
|
||||
public static AudioSource findByName(String name) {
|
||||
for (AudioSource audioSource : AudioSource.values()) {
|
||||
if (name.equals(audioSource.name)) {
|
||||
|
||||
@@ -112,8 +112,9 @@ public class ControlMessageReader {
|
||||
|
||||
private ControlMessage parseInjectScrollEvent() throws IOException {
|
||||
Position position = parsePosition();
|
||||
float hScroll = Binary.i16FixedPointToFloat(dis.readShort());
|
||||
float vScroll = Binary.i16FixedPointToFloat(dis.readShort());
|
||||
// Binary.i16FixedPointToFloat() decodes values assuming the full range is [-1, 1], but the actual range is [-16, 16].
|
||||
float hScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16;
|
||||
float vScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16;
|
||||
int buttons = dis.readInt();
|
||||
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.genymobile.scrcpy.CleanUp;
|
||||
import com.genymobile.scrcpy.Options;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.DeviceApp;
|
||||
import com.genymobile.scrcpy.device.DisplayInfo;
|
||||
import com.genymobile.scrcpy.device.Point;
|
||||
import com.genymobile.scrcpy.device.Position;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
@@ -17,7 +18,6 @@ import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.content.IOnPrimaryClipChangedListener;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
@@ -114,22 +114,20 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
Ln.w("Input events are not supported for secondary displays before Android 10");
|
||||
}
|
||||
|
||||
// Make sure the clipboard manager is always created from the main thread (even if clipboardAutosync is disabled)
|
||||
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
|
||||
if (clipboardAutosync) {
|
||||
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically
|
||||
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
|
||||
if (clipboardManager != null) {
|
||||
clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
|
||||
@Override
|
||||
public void dispatchPrimaryClipChanged() {
|
||||
if (isSettingClipboard.get()) {
|
||||
// This is a notification for the change we are currently applying, ignore it
|
||||
return;
|
||||
}
|
||||
String text = Device.getClipboardText();
|
||||
if (text != null) {
|
||||
DeviceMessage msg = DeviceMessage.createClipboard(text);
|
||||
sender.send(msg);
|
||||
}
|
||||
clipboardManager.addPrimaryClipChangedListener(() -> {
|
||||
if (isSettingClipboard.get()) {
|
||||
// This is a notification for the change we are currently applying, ignore it
|
||||
return;
|
||||
}
|
||||
String text = Device.getClipboardText();
|
||||
if (text != null) {
|
||||
DeviceMessage msg = DeviceMessage.createClipboard(text);
|
||||
sender.send(msg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -156,8 +154,34 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
private UhidManager getUhidManager() {
|
||||
if (uhidManager == null) {
|
||||
uhidManager = new UhidManager(sender);
|
||||
int uhidDisplayId = displayId;
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) {
|
||||
if (displayId == Device.DISPLAY_ID_NONE) {
|
||||
// Mirroring a new virtual display id (using --new-display-id feature) on Android >= 15, where the UHID mouse pointer can be
|
||||
// associated to the virtual display
|
||||
try {
|
||||
// Wait for at most 1 second until a virtual display id is known
|
||||
DisplayData data = waitDisplayData(1000);
|
||||
if (data != null) {
|
||||
uhidDisplayId = data.virtualDisplayId;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String displayUniqueId = null;
|
||||
if (uhidDisplayId > 0) {
|
||||
// Ignore Device.DISPLAY_ID_NONE and 0 (main display)
|
||||
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(uhidDisplayId);
|
||||
if (displayInfo != null) {
|
||||
displayUniqueId = displayInfo.getUniqueId();
|
||||
}
|
||||
}
|
||||
uhidManager = new UhidManager(sender, displayUniqueId);
|
||||
}
|
||||
|
||||
return uhidManager;
|
||||
}
|
||||
|
||||
@@ -699,7 +723,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
if (timeout < 0) {
|
||||
return null;
|
||||
}
|
||||
displayDataAvailable.wait(timeout);
|
||||
if (timeout > 0) {
|
||||
displayDataAvailable.wait(timeout);
|
||||
}
|
||||
data = displayData.get();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.genymobile.scrcpy.control;
|
||||
import com.genymobile.scrcpy.AndroidVersions;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.StringUtils;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.HandlerThread;
|
||||
@@ -31,14 +32,20 @@ public final class UhidManager {
|
||||
|
||||
private static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event)
|
||||
|
||||
// Must be unique across the system
|
||||
private static final String INPUT_PORT = "scrcpy:" + Os.getpid();
|
||||
|
||||
private final String displayUniqueId;
|
||||
|
||||
private final ArrayMap<Integer, FileDescriptor> fds = new ArrayMap<>();
|
||||
private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder());
|
||||
|
||||
private final DeviceMessageSender sender;
|
||||
private final MessageQueue queue;
|
||||
|
||||
public UhidManager(DeviceMessageSender sender) {
|
||||
public UhidManager(DeviceMessageSender sender, String displayUniqueId) {
|
||||
this.sender = sender;
|
||||
this.displayUniqueId = displayUniqueId;
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) {
|
||||
HandlerThread thread = new HandlerThread("UHidManager");
|
||||
thread.start();
|
||||
@@ -52,15 +59,22 @@ public final class UhidManager {
|
||||
try {
|
||||
FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0);
|
||||
try {
|
||||
// First UHID device added
|
||||
boolean firstDevice = fds.isEmpty();
|
||||
|
||||
FileDescriptor old = fds.put(id, fd);
|
||||
if (old != null) {
|
||||
Ln.w("Duplicate UHID id: " + id);
|
||||
close(old);
|
||||
}
|
||||
|
||||
byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc);
|
||||
String phys = mustUseInputPort() ? INPUT_PORT : null;
|
||||
byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc, phys);
|
||||
Os.write(fd, req, 0, req.length);
|
||||
|
||||
if (firstDevice) {
|
||||
addUniqueIdAssociation();
|
||||
}
|
||||
registerUhidListener(id, fd);
|
||||
} catch (Exception e) {
|
||||
close(fd);
|
||||
@@ -148,7 +162,7 @@ public final class UhidManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc) {
|
||||
private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc, String phys) {
|
||||
/*
|
||||
* struct uhid_event {
|
||||
* uint32_t type;
|
||||
@@ -170,17 +184,23 @@ public final class UhidManager {
|
||||
* } __attribute__((__packed__));
|
||||
*/
|
||||
|
||||
byte[] empty = new byte[256];
|
||||
ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder());
|
||||
buf.putInt(UHID_CREATE2);
|
||||
|
||||
String actualName = name.isEmpty() ? "scrcpy" : name;
|
||||
byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8);
|
||||
int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127);
|
||||
assert len <= 127;
|
||||
buf.put(utf8Name, 0, len);
|
||||
buf.put(empty, 0, 256 - len);
|
||||
byte[] nameBytes = actualName.getBytes(StandardCharsets.UTF_8);
|
||||
int nameLen = StringUtils.getUtf8TruncationIndex(nameBytes, 127);
|
||||
assert nameLen <= 127;
|
||||
buf.put(nameBytes, 0, nameLen);
|
||||
|
||||
if (phys != null) {
|
||||
buf.position(4 + 128);
|
||||
byte[] physBytes = phys.getBytes(StandardCharsets.US_ASCII);
|
||||
assert physBytes.length <= 63;
|
||||
buf.put(physBytes);
|
||||
}
|
||||
|
||||
buf.position(4 + 256);
|
||||
buf.putShort((short) reportDesc.length);
|
||||
buf.putShort(BUS_VIRTUAL);
|
||||
buf.putInt(vendorId);
|
||||
@@ -219,15 +239,26 @@ public final class UhidManager {
|
||||
if (fd != null) {
|
||||
unregisterUhidListener(fd);
|
||||
close(fd);
|
||||
|
||||
if (fds.isEmpty()) {
|
||||
// Last UHID device removed
|
||||
removeUniqueIdAssociation();
|
||||
}
|
||||
} else {
|
||||
Ln.w("Closing unknown UHID device: " + id);
|
||||
}
|
||||
}
|
||||
|
||||
public void closeAll() {
|
||||
if (fds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (FileDescriptor fd : fds.values()) {
|
||||
close(fd);
|
||||
}
|
||||
|
||||
removeUniqueIdAssociation();
|
||||
}
|
||||
|
||||
private static void close(FileDescriptor fd) {
|
||||
@@ -237,4 +268,20 @@ public final class UhidManager {
|
||||
Ln.e("Failed to close uhid: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean mustUseInputPort() {
|
||||
return Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15 && displayUniqueId != null;
|
||||
}
|
||||
|
||||
private void addUniqueIdAssociation() {
|
||||
if (mustUseInputPort()) {
|
||||
ServiceManager.getInputManager().addUniqueIdAssociationByPort(INPUT_PORT, displayUniqueId);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeUniqueIdAssociation() {
|
||||
if (mustUseInputPort()) {
|
||||
ServiceManager.getInputManager().removeUniqueIdAssociationByPort(INPUT_PORT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,18 @@ public final class DisplayInfo {
|
||||
private final int layerStack;
|
||||
private final int flags;
|
||||
private final int dpi;
|
||||
private final String uniqueId;
|
||||
|
||||
public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001;
|
||||
|
||||
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi) {
|
||||
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi, String uniqueId) {
|
||||
this.displayId = displayId;
|
||||
this.size = size;
|
||||
this.rotation = rotation;
|
||||
this.layerStack = layerStack;
|
||||
this.flags = flags;
|
||||
this.dpi = dpi;
|
||||
this.uniqueId = uniqueId;
|
||||
}
|
||||
|
||||
public int getDisplayId() {
|
||||
@@ -42,5 +44,8 @@ public final class DisplayInfo {
|
||||
public int getDpi() {
|
||||
return dpi;
|
||||
}
|
||||
}
|
||||
|
||||
public String getUniqueId() {
|
||||
return uniqueId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,11 @@ public enum Orientation {
|
||||
throw new IllegalArgumentException("Unknown orientation: " + name);
|
||||
}
|
||||
|
||||
public static Orientation fromRotation(int rotation) {
|
||||
assert rotation >= 0 && rotation < 4;
|
||||
return values()[rotation];
|
||||
public static Orientation fromRotation(int ccwRotation) {
|
||||
assert ccwRotation >= 0 && ccwRotation < 4;
|
||||
// Display rotation is expressed counter-clockwise, orientation is expressed clockwise
|
||||
int cwRotation = (4 - ccwRotation) % 4;
|
||||
return values()[cwRotation];
|
||||
}
|
||||
|
||||
public boolean isFlipped() {
|
||||
|
||||
@@ -23,7 +23,9 @@ public class DisplaySizeMonitor {
|
||||
|
||||
// On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really
|
||||
// detect it directly, so register a DisplayWindowListener (introduced in Android 11) to listen to configuration changes instead.
|
||||
private static final boolean USE_DEFAULT_METHOD = Build.VERSION.SDK_INT != AndroidVersions.API_34_ANDROID_14;
|
||||
// It has been broken again after an Android 15 upgrade: <https://github.com/Genymobile/scrcpy/issues/5908>
|
||||
// So use the default method only before Android 14.
|
||||
private static final boolean USE_DEFAULT_METHOD = Build.VERSION.SDK_INT < AndroidVersions.API_34_ANDROID_14;
|
||||
|
||||
private DisplayManager.DisplayListenerHandle displayListenerHandle;
|
||||
private HandlerThread handlerThread;
|
||||
|
||||
@@ -49,6 +49,7 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
private Size mainDisplaySize;
|
||||
private int mainDisplayDpi;
|
||||
private int maxSize;
|
||||
private int displayImePolicy;
|
||||
private final Rect crop;
|
||||
private final boolean captureOrientationLocked;
|
||||
private final Orientation captureOrientation;
|
||||
@@ -68,6 +69,7 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
this.newDisplay = options.getNewDisplay();
|
||||
assert newDisplay != null;
|
||||
this.maxSize = options.getMaxSize();
|
||||
this.displayImePolicy = options.getDisplayImePolicy();
|
||||
this.crop = options.getCrop();
|
||||
assert options.getCaptureOrientationLock() != null;
|
||||
this.captureOrientationLocked = options.getCaptureOrientationLock() != Orientation.Lock.Unlocked;
|
||||
@@ -191,6 +193,10 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
|
||||
Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")");
|
||||
|
||||
if (displayImePolicy != -1) {
|
||||
ServiceManager.getWindowManager().setDisplayImePolicy(virtualDisplayId, displayImePolicy);
|
||||
}
|
||||
|
||||
displaySizeMonitor.start(virtualDisplayId, this::invalidate);
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not create display", e);
|
||||
|
||||
@@ -1,270 +1,43 @@
|
||||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.AndroidVersions;
|
||||
import com.genymobile.scrcpy.FakeContext;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.IOnPrimaryClipChangedListener;
|
||||
import android.os.Build;
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import android.content.Context;
|
||||
|
||||
public final class ClipboardManager {
|
||||
private final IInterface manager;
|
||||
private Method getPrimaryClipMethod;
|
||||
private Method setPrimaryClipMethod;
|
||||
private Method addPrimaryClipChangedListener;
|
||||
private int getMethodVersion;
|
||||
private int setMethodVersion;
|
||||
private int addListenerMethodVersion;
|
||||
private final android.content.ClipboardManager manager;
|
||||
|
||||
static ClipboardManager create() {
|
||||
IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard");
|
||||
if (clipboard == null) {
|
||||
android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (manager == null) {
|
||||
// Some devices have no clipboard manager
|
||||
// <https://github.com/Genymobile/scrcpy/issues/1440>
|
||||
// <https://github.com/Genymobile/scrcpy/issues/1556>
|
||||
return null;
|
||||
}
|
||||
return new ClipboardManager(clipboard);
|
||||
return new ClipboardManager(manager);
|
||||
}
|
||||
|
||||
private ClipboardManager(IInterface manager) {
|
||||
private ClipboardManager(android.content.ClipboardManager manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
private Method getGetPrimaryClipMethod() throws NoSuchMethodException {
|
||||
if (getPrimaryClipMethod == null) {
|
||||
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
|
||||
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;
|
||||
}
|
||||
|
||||
private Method getSetPrimaryClipMethod() throws NoSuchMethodException {
|
||||
if (setPrimaryClipMethod == null) {
|
||||
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
|
||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
|
||||
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;
|
||||
}
|
||||
|
||||
private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException {
|
||||
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
|
||||
}
|
||||
|
||||
switch (methodVersion) {
|
||||
case 0:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
case 1:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
case 2:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
||||
case 3:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null);
|
||||
case 4:
|
||||
// The last boolean parameter is "userOperate"
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException {
|
||||
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (methodVersion) {
|
||||
case 0:
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
break;
|
||||
case 1:
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
break;
|
||||
case 2:
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
||||
break;
|
||||
default:
|
||||
// The last boolean parameter is "userOperate"
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
public CharSequence getText() {
|
||||
try {
|
||||
Method method = getGetPrimaryClipMethod();
|
||||
ClipData clipData = getPrimaryClip(method, getMethodVersion, manager);
|
||||
if (clipData == null || clipData.getItemCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
return clipData.getItemAt(0).getText();
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
ClipData clipData = manager.getPrimaryClip();
|
||||
if (clipData == null || clipData.getItemCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
return clipData.getItemAt(0).getText();
|
||||
}
|
||||
|
||||
public boolean setText(CharSequence text) {
|
||||
try {
|
||||
Method method = getSetPrimaryClipMethod();
|
||||
ClipData clipData = ClipData.newPlainText(null, text);
|
||||
setPrimaryClip(method, setMethodVersion, manager, clipData);
|
||||
return true;
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
ClipData clipData = ClipData.newPlainText(null, text);
|
||||
manager.setPrimaryClip(clipData);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener)
|
||||
throws ReflectiveOperationException {
|
||||
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (methodVersion) {
|
||||
case 0:
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
break;
|
||||
case 1:
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
break;
|
||||
default:
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
|
||||
if (addPrimaryClipChangedListener == null) {
|
||||
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
|
||||
} else {
|
||||
try {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
|
||||
addListenerMethodVersion = 0;
|
||||
} catch (NoSuchMethodException e1) {
|
||||
try {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
|
||||
int.class);
|
||||
addListenerMethodVersion = 1;
|
||||
} catch (NoSuchMethodException e2) {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
|
||||
int.class, int.class);
|
||||
addListenerMethodVersion = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return addPrimaryClipChangedListener;
|
||||
}
|
||||
|
||||
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
|
||||
try {
|
||||
Method method = getAddPrimaryClipChangedListener();
|
||||
addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener);
|
||||
return true;
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
public void addPrimaryClipChangedListener(android.content.ClipboardManager.OnPrimaryClipChangedListener listener) {
|
||||
manager.addPrimaryClipChangedListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ public final class DisplayManager {
|
||||
}
|
||||
|
||||
private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
|
||||
private Method getDisplayInfoMethod;
|
||||
private Method createVirtualDisplayMethod;
|
||||
private Method requestDisplayPowerMethod;
|
||||
|
||||
@@ -81,7 +82,7 @@ public final class DisplayManager {
|
||||
int density = Integer.parseInt(m.group(5));
|
||||
int layerStack = Integer.parseInt(m.group(6));
|
||||
|
||||
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density);
|
||||
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density, null);
|
||||
}
|
||||
|
||||
private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) {
|
||||
@@ -95,12 +96,12 @@ public final class DisplayManager {
|
||||
}
|
||||
|
||||
private static int parseDisplayFlags(String text) {
|
||||
Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
|
||||
if (text == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int flags = 0;
|
||||
Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
|
||||
Matcher m = regex.matcher(text);
|
||||
while (m.find()) {
|
||||
String flagString = m.group();
|
||||
@@ -114,9 +115,18 @@ public final class DisplayManager {
|
||||
return flags;
|
||||
}
|
||||
|
||||
// getDisplayInfo() may be used from both the Controller thread and the video (main) thread
|
||||
private synchronized Method getGetDisplayInfoMethod() throws NoSuchMethodException {
|
||||
if (getDisplayInfoMethod == null) {
|
||||
getDisplayInfoMethod = manager.getClass().getMethod("getDisplayInfo", int.class);
|
||||
}
|
||||
return getDisplayInfoMethod;
|
||||
}
|
||||
|
||||
public DisplayInfo getDisplayInfo(int displayId) {
|
||||
try {
|
||||
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId);
|
||||
Method method = getGetDisplayInfoMethod();
|
||||
Object displayInfo = method.invoke(manager, displayId);
|
||||
if (displayInfo == null) {
|
||||
// fallback when displayInfo is null
|
||||
return getDisplayInfoFromDumpsysDisplay(displayId);
|
||||
@@ -129,7 +139,8 @@ public final class DisplayManager {
|
||||
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
|
||||
int flags = cls.getDeclaredField("flags").getInt(displayInfo);
|
||||
int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo);
|
||||
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi);
|
||||
String uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo);
|
||||
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi, uniqueId);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.AndroidVersions;
|
||||
import com.genymobile.scrcpy.FakeContext;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.view.InputEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||
@@ -15,39 +19,28 @@ public final class InputManager {
|
||||
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1;
|
||||
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
|
||||
|
||||
private final Object manager;
|
||||
private Method injectInputEventMethod;
|
||||
private final android.hardware.input.InputManager manager;
|
||||
private long lastPermissionLogDate;
|
||||
|
||||
private static Method injectInputEventMethod;
|
||||
private static Method setDisplayIdMethod;
|
||||
private static Method setActionButtonMethod;
|
||||
private static Method addUniqueIdAssociationByPortMethod;
|
||||
private static Method removeUniqueIdAssociationByPortMethod;
|
||||
|
||||
static InputManager create() {
|
||||
try {
|
||||
Class<?> inputManagerClass = getInputManagerClass();
|
||||
Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance");
|
||||
Object im = getInstanceMethod.invoke(null);
|
||||
return new InputManager(im);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get()
|
||||
.getSystemService(FakeContext.INPUT_SERVICE);
|
||||
return new InputManager(manager);
|
||||
}
|
||||
|
||||
private static Class<?> getInputManagerClass() {
|
||||
try {
|
||||
// Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview
|
||||
return Class.forName("android.hardware.input.InputManagerGlobal");
|
||||
} catch (ClassNotFoundException e) {
|
||||
return android.hardware.input.InputManager.class;
|
||||
}
|
||||
}
|
||||
|
||||
private InputManager(Object manager) {
|
||||
private InputManager(android.hardware.input.InputManager manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
private Method getInjectInputEventMethod() throws NoSuchMethodException {
|
||||
private static Method getInjectInputEventMethod() throws NoSuchMethodException {
|
||||
if (injectInputEventMethod == null) {
|
||||
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
|
||||
injectInputEventMethod = android.hardware.input.InputManager.class.getMethod("injectInputEvent", InputEvent.class, int.class);
|
||||
}
|
||||
return injectInputEventMethod;
|
||||
}
|
||||
@@ -57,6 +50,23 @@ public final class InputManager {
|
||||
Method method = getInjectInputEventMethod();
|
||||
return (boolean) method.invoke(manager, inputEvent, mode);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
if (e instanceof InvocationTargetException) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof SecurityException) {
|
||||
String message = e.getCause().getMessage();
|
||||
if (message != null && message.contains("INJECT_EVENTS permission")) {
|
||||
// Do not flood the console, limit to one permission error log every 3 seconds
|
||||
long now = System.currentTimeMillis();
|
||||
if (lastPermissionLogDate <= now - 3000) {
|
||||
Ln.e(message);
|
||||
Ln.e("Make sure you have enabled \"USB debugging (Security Settings)\" and then rebooted your device.");
|
||||
lastPermissionLogDate = now;
|
||||
}
|
||||
// Do not print the stack trace
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
@@ -97,4 +107,40 @@ public final class InputManager {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getAddUniqueIdAssociationByPortMethod() throws NoSuchMethodException {
|
||||
if (addUniqueIdAssociationByPortMethod == null) {
|
||||
addUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod(
|
||||
"addUniqueIdAssociationByPort", String.class, String.class);
|
||||
}
|
||||
return addUniqueIdAssociationByPortMethod;
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_35_ANDROID_15)
|
||||
public void addUniqueIdAssociationByPort(String inputPort, String uniqueId) {
|
||||
try {
|
||||
Method method = getAddUniqueIdAssociationByPortMethod();
|
||||
method.invoke(manager, inputPort, uniqueId);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Cannot add unique id association by port", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getRemoveUniqueIdAssociationByPortMethod() throws NoSuchMethodException {
|
||||
if (removeUniqueIdAssociationByPortMethod == null) {
|
||||
removeUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod(
|
||||
"removeUniqueIdAssociationByPort", String.class);
|
||||
}
|
||||
return removeUniqueIdAssociationByPortMethod;
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_35_ANDROID_15)
|
||||
public void removeUniqueIdAssociationByPort(String inputPort) {
|
||||
try {
|
||||
Method method = getRemoveUniqueIdAssociationByPortMethod();
|
||||
method.invoke(manager, inputPort);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Cannot remove unique id association by port", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,8 @@ public final class ServiceManager {
|
||||
return windowManager;
|
||||
}
|
||||
|
||||
public static DisplayManager getDisplayManager() {
|
||||
// The DisplayManager may be used from both the Controller thread and the video (main) thread
|
||||
public static synchronized DisplayManager getDisplayManager() {
|
||||
if (displayManager == null) {
|
||||
displayManager = DisplayManager.create();
|
||||
}
|
||||
|
||||
@@ -4,14 +4,20 @@ import com.genymobile.scrcpy.AndroidVersions;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.os.IInterface;
|
||||
import android.view.IDisplayFoldListener;
|
||||
import android.view.IDisplayWindowListener;
|
||||
import android.view.IRotationWatcher;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class WindowManager {
|
||||
|
||||
@SuppressWarnings("checkstyle:LineLength")
|
||||
// <https://android.googlesource.com/platform/frameworks/base.git/+/2103ff441c66772c80c8560e322dcd9a45be7dcd/core/java/android/view/WindowManager.java#692>
|
||||
public static final int DISPLAY_IME_POLICY_LOCAL = 0;
|
||||
public static final int DISPLAY_IME_POLICY_FALLBACK_DISPLAY = 1;
|
||||
public static final int DISPLAY_IME_POLICY_HIDE = 2;
|
||||
|
||||
private final IInterface manager;
|
||||
private Method getRotationMethod;
|
||||
|
||||
@@ -24,6 +30,9 @@ public final class WindowManager {
|
||||
private Method thawDisplayRotationMethod;
|
||||
private int thawDisplayRotationMethodVersion;
|
||||
|
||||
private Method getDisplayImePolicyMethod;
|
||||
private Method setDisplayImePolicyMethod;
|
||||
|
||||
static WindowManager create() {
|
||||
IInterface manager = ServiceManager.getService("window", "android.view.IWindowManager");
|
||||
return new WindowManager(manager);
|
||||
@@ -182,52 +191,6 @@ public final class WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) {
|
||||
try {
|
||||
Class<?> cls = manager.getClass();
|
||||
try {
|
||||
// display parameter added since this commit:
|
||||
// https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
|
||||
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// old version
|
||||
if (displayId != 0) {
|
||||
Ln.e("Secondary display rotation not supported on this device");
|
||||
return;
|
||||
}
|
||||
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not register rotation watcher", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterRotationWatcher(IRotationWatcher rotationWatcher) {
|
||||
try {
|
||||
manager.getClass().getMethod("removeRotationWatcher", IRotationWatcher.class).invoke(manager, rotationWatcher);
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not unregister rotation watcher", e);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_29_ANDROID_10)
|
||||
public void registerDisplayFoldListener(IDisplayFoldListener foldListener) {
|
||||
try {
|
||||
manager.getClass().getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener);
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not register display fold listener", e);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_29_ANDROID_10)
|
||||
public void unregisterDisplayFoldListener(IDisplayFoldListener foldListener) {
|
||||
try {
|
||||
manager.getClass().getMethod("unregisterDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener);
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not unregister display fold listener", e);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_30_ANDROID_11)
|
||||
public int[] registerDisplayWindowListener(IDisplayWindowListener listener) {
|
||||
try {
|
||||
@@ -246,4 +209,59 @@ public final class WindowManager {
|
||||
Ln.e("Could not unregister display window listener", e);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_29_ANDROID_10)
|
||||
private Method getGetDisplayImePolicyMethod() throws NoSuchMethodException {
|
||||
if (getDisplayImePolicyMethod == null) {
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) {
|
||||
getDisplayImePolicyMethod = manager.getClass().getMethod("getDisplayImePolicy", int.class);
|
||||
} else {
|
||||
getDisplayImePolicyMethod = manager.getClass().getMethod("shouldShowIme", int.class);
|
||||
}
|
||||
}
|
||||
return getDisplayImePolicyMethod;
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_29_ANDROID_10)
|
||||
public int getDisplayImePolicy(int displayId) {
|
||||
try {
|
||||
Method method = getGetDisplayImePolicyMethod();
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) {
|
||||
return (int) method.invoke(manager, displayId);
|
||||
}
|
||||
boolean shouldShowIme = (boolean) method.invoke(manager, displayId);
|
||||
return shouldShowIme ? DISPLAY_IME_POLICY_LOCAL : DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_29_ANDROID_10)
|
||||
private Method getSetDisplayImePolicyMethod() throws NoSuchMethodException {
|
||||
if (setDisplayImePolicyMethod == null) {
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) {
|
||||
setDisplayImePolicyMethod = manager.getClass().getMethod("setDisplayImePolicy", int.class, int.class);
|
||||
} else {
|
||||
setDisplayImePolicyMethod = manager.getClass().getMethod("setShouldShowIme", int.class, boolean.class);
|
||||
}
|
||||
}
|
||||
return setDisplayImePolicyMethod;
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_29_ANDROID_10)
|
||||
public void setDisplayImePolicy(int displayId, int displayImePolicy) {
|
||||
try {
|
||||
Method method = getSetDisplayImePolicyMethod();
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) {
|
||||
method.invoke(manager, displayId, displayImePolicy);
|
||||
} else if (displayImePolicy != DISPLAY_IME_POLICY_HIDE) {
|
||||
method.invoke(manager, displayId, displayImePolicy == DISPLAY_IME_POLICY_LOCAL);
|
||||
} else {
|
||||
Ln.w("DISPLAY_IME_POLICY_HIDE is not supported before Android 12");
|
||||
}
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user