Compare commits
51 Commits
orientatio
...
dynamic_cl
Author | SHA1 | Date | |
---|---|---|---|
9efa162949 | |||
be3f949aa5 | |||
f7b4a18b43 | |||
05b5deacad | |||
d25cbc55f2 | |||
3333e67452 | |||
7c53a29d72 | |||
5187f7254e | |||
2ad93d1fc0 | |||
d067a11478 | |||
cd4056d0f3 | |||
6a58891e13 | |||
ec41896c85 | |||
4cd61b5a90 | |||
d2ed4510a7 | |||
604dfd7c6b | |||
af69689ec1 | |||
cbce42336d | |||
c9a4d2b38f | |||
1beec99f82 | |||
5ce8672ebc | |||
3001f8a2d5 | |||
c6ff78f414 | |||
40f2560d98 | |||
26aa28c998 | |||
ef79fcbbd2 | |||
9497f39fb4 | |||
bf056b1fee | |||
bd9292931e | |||
140a49b8be | |||
4135c411af | |||
5e061636f6 | |||
5f3fb843f5 | |||
ce8126f322 | |||
d037b02cc2 | |||
89761213c3 | |||
8db4e78b34 | |||
5d4b8a7e6d | |||
eed06b141a | |||
825d7f72c0 | |||
2370298b61 | |||
67f356f881 | |||
c573bd2a33 | |||
acb2988837 | |||
85a94dd4b5 | |||
94031dfe97 | |||
b43a9e8e7a | |||
a9d6cb5837 | |||
2f92686930 | |||
bb88b60227 | |||
25e33566f5 |
@ -1,4 +1,4 @@
|
||||
# scrcpy (v2.2)
|
||||
# scrcpy (v2.3.1)
|
||||
|
||||
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||
|
||||
|
@ -21,6 +21,7 @@ _scrcpy() {
|
||||
--disable-screensaver
|
||||
--display-buffer=
|
||||
--display-id=
|
||||
--display-orientation=
|
||||
-e --select-tcpip
|
||||
-f --fullscreen
|
||||
--force-adb-forward
|
||||
@ -50,6 +51,7 @@ _scrcpy() {
|
||||
--no-power-on
|
||||
--no-video
|
||||
--no-video-playback
|
||||
--orientation=
|
||||
--otg
|
||||
-p --port=
|
||||
--pause-on-exit
|
||||
@ -61,6 +63,7 @@ _scrcpy() {
|
||||
-r --record=
|
||||
--raw-key-events
|
||||
--record-format=
|
||||
--record-orientation=
|
||||
--render-driver=
|
||||
--require-audio
|
||||
--rotation=
|
||||
@ -112,8 +115,16 @@ _scrcpy() {
|
||||
COMPREPLY=($(compgen -W 'front back external' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--orientation|--display-orientation)
|
||||
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--record-orientation)
|
||||
COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--lock-video-orientation)
|
||||
COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur"))
|
||||
COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--pause-on-exit)
|
||||
@ -132,10 +143,6 @@ _scrcpy() {
|
||||
COMPREPLY=($(compgen -W 'direct3d opengl opengles2 opengles metal software' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--rotation)
|
||||
COMPREPLY=($(compgen -W '0 1 2 3' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--shortcut-mod)
|
||||
# Only auto-complete a single key
|
||||
COMPREPLY=($(compgen -W 'lctrl rctrl lalt ralt lsuper rsuper' -- "$cur"))
|
||||
|
@ -5,7 +5,7 @@ Comment=Display and control your Android device
|
||||
# For some users, the PATH or ADB environment variables are set from the shell
|
||||
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
|
||||
# environment correctly initialized.
|
||||
Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy --pause-on-exit=if-error"
|
||||
Exec=/bin/sh -c "\\$SHELL -i -c 'scrcpy --pause-on-exit=if-error'"
|
||||
Icon=scrcpy
|
||||
Terminal=true
|
||||
Type=Application
|
||||
|
@ -5,7 +5,7 @@ Comment=Display and control your Android device
|
||||
# For some users, the PATH or ADB environment variables are set from the shell
|
||||
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
|
||||
# environment correctly initialized.
|
||||
Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy"
|
||||
Exec=/bin/sh -c "\\$SHELL -i -c scrcpy"
|
||||
Icon=scrcpy
|
||||
Terminal=false
|
||||
Type=Application
|
||||
|
@ -28,6 +28,7 @@ arguments=(
|
||||
'--disable-screensaver[Disable screensaver while scrcpy is running]'
|
||||
'--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]'
|
||||
'--display-id=[Specify the display id to mirror]'
|
||||
'--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]'
|
||||
@ -40,7 +41,7 @@ arguments=(
|
||||
'--list-cameras[List cameras available on the device]'
|
||||
'--list-displays[List displays available on the device]'
|
||||
'--list-encoders[List video and audio encoders available on the device]'
|
||||
'--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)'
|
||||
'--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)'
|
||||
{-m,--max-size=}'[Limit both the width and height of the video to value]'
|
||||
{-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]'
|
||||
'--max-fps=[Limit the frame rate of screen capture]'
|
||||
@ -56,6 +57,7 @@ arguments=(
|
||||
'--no-power-on[Do not power on the device on start]'
|
||||
'--no-video[Disable video forwarding]'
|
||||
'--no-video-playback[Disable video playback]'
|
||||
'--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
|
||||
'--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]'
|
||||
{-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]'
|
||||
'--pause-on-exit=[Make scrcpy pause before exiting]:mode:(true false if-error)'
|
||||
@ -66,9 +68,9 @@ arguments=(
|
||||
{-r,--record=}'[Record screen to file]:record file:_files'
|
||||
'--raw-key-events[Inject key events for all input keys, and ignore text events]'
|
||||
'--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac wav)'
|
||||
'--record-orientation=[Set the record orientation]:orientation values:(0 90 180 270)'
|
||||
'--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)'
|
||||
'--require-audio=[Make scrcpy fail if audio is enabled but does not work]'
|
||||
'--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)'
|
||||
{-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))'
|
||||
{-S,--turn-screen-off}'[Turn the device screen off immediately]'
|
||||
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
|
||||
|
@ -98,77 +98,24 @@ endif
|
||||
|
||||
cc = meson.get_compiler('c')
|
||||
|
||||
crossbuild_windows = meson.is_cross_build() and host_machine.system() == 'windows'
|
||||
dependencies = [
|
||||
dependency('libavformat', version: '>= 57.33'),
|
||||
dependency('libavcodec', version: '>= 57.37'),
|
||||
dependency('libavutil'),
|
||||
dependency('libswresample'),
|
||||
dependency('sdl2', version: '>= 2.0.5'),
|
||||
]
|
||||
|
||||
if not crossbuild_windows
|
||||
|
||||
# native build
|
||||
dependencies = [
|
||||
dependency('libavformat', version: '>= 57.33'),
|
||||
dependency('libavcodec', version: '>= 57.37'),
|
||||
dependency('libavutil'),
|
||||
dependency('libswresample'),
|
||||
dependency('sdl2', version: '>= 2.0.5'),
|
||||
]
|
||||
|
||||
if v4l2_support
|
||||
dependencies += dependency('libavdevice')
|
||||
endif
|
||||
|
||||
if usb_support
|
||||
dependencies += dependency('libusb-1.0')
|
||||
endif
|
||||
|
||||
else
|
||||
# cross-compile mingw32 build (from Linux to Windows)
|
||||
prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2')
|
||||
sdl2_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_sdl2 + '/bin'
|
||||
sdl2_lib_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_sdl2 + '/lib'
|
||||
sdl2_include_dir = 'prebuilt-deps/data/' + prebuilt_sdl2 + '/include'
|
||||
|
||||
sdl2 = declare_dependency(
|
||||
dependencies: [
|
||||
cc.find_library('SDL2', dirs: sdl2_bin_dir),
|
||||
cc.find_library('SDL2main', dirs: sdl2_lib_dir),
|
||||
],
|
||||
include_directories: include_directories(sdl2_include_dir)
|
||||
)
|
||||
|
||||
prebuilt_ffmpeg = meson.get_cross_property('prebuilt_ffmpeg')
|
||||
ffmpeg_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_ffmpeg + '/bin'
|
||||
ffmpeg_include_dir = 'prebuilt-deps/data/' + prebuilt_ffmpeg + '/include'
|
||||
|
||||
ffmpeg = declare_dependency(
|
||||
dependencies: [
|
||||
cc.find_library('avcodec-60', dirs: ffmpeg_bin_dir),
|
||||
cc.find_library('avformat-60', dirs: ffmpeg_bin_dir),
|
||||
cc.find_library('avutil-58', dirs: ffmpeg_bin_dir),
|
||||
cc.find_library('swresample-4', dirs: ffmpeg_bin_dir),
|
||||
],
|
||||
include_directories: include_directories(ffmpeg_include_dir)
|
||||
)
|
||||
|
||||
prebuilt_libusb = meson.get_cross_property('prebuilt_libusb')
|
||||
libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb + '/bin'
|
||||
libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb + '/include'
|
||||
|
||||
libusb = declare_dependency(
|
||||
dependencies: [
|
||||
cc.find_library('msys-usb-1.0', dirs: libusb_bin_dir),
|
||||
],
|
||||
include_directories: include_directories(libusb_include_dir)
|
||||
)
|
||||
|
||||
dependencies = [
|
||||
ffmpeg,
|
||||
sdl2,
|
||||
libusb,
|
||||
cc.find_library('mingw32')
|
||||
]
|
||||
if v4l2_support
|
||||
dependencies += dependency('libavdevice')
|
||||
endif
|
||||
|
||||
if usb_support
|
||||
dependencies += dependency('libusb-1.0')
|
||||
endif
|
||||
|
||||
if host_machine.system() == 'windows'
|
||||
dependencies += cc.find_library('mingw32')
|
||||
dependencies += cc.find_library('ws2_32')
|
||||
endif
|
||||
|
||||
@ -289,6 +236,10 @@ if get_option('buildtype') == 'debug'
|
||||
'tests/test_device_msg_deserialize.c',
|
||||
'src/device_msg.c',
|
||||
]],
|
||||
['test_orientation', [
|
||||
'tests/test_orientation.c',
|
||||
'src/options.c',
|
||||
]],
|
||||
['test_strbuf', [
|
||||
'tests/test_strbuf.c',
|
||||
'src/util/strbuf.c',
|
||||
|
@ -6,11 +6,11 @@ cd "$DIR"
|
||||
mkdir -p "$PREBUILT_DATA_DIR"
|
||||
cd "$PREBUILT_DATA_DIR"
|
||||
|
||||
VERSION=6.1-scrcpy-2
|
||||
VERSION=6.1-scrcpy-3
|
||||
DEP_DIR="ffmpeg-$VERSION"
|
||||
|
||||
FILENAME="$DEP_DIR".7z
|
||||
SHA256SUM=7f25f638dc24a0f5d4af07a088b6a604cf33548900bbfd2f6ce0bae050b7664d
|
||||
SHA256SUM=b646d18a3d543a4e4c46881568213499f22e4454a464e1552f03f2ac9cc3a05a
|
||||
|
||||
if [[ -d "$DEP_DIR" ]]
|
||||
then
|
||||
|
@ -6,9 +6,10 @@ cd "$DIR"
|
||||
mkdir -p "$PREBUILT_DATA_DIR"
|
||||
cd "$PREBUILT_DATA_DIR"
|
||||
|
||||
DEP_DIR=libusb-1.0.26
|
||||
VERSION=1.0.26
|
||||
DEP_DIR="libusb-$VERSION"
|
||||
|
||||
FILENAME=libusb-1.0.26-binaries.7z
|
||||
FILENAME="libusb-$VERSION-binaries.7z"
|
||||
SHA256SUM=9c242696342dbde9cdc47239391f71833939bf9f7aa2bbb28cdaabe890465ec5
|
||||
|
||||
if [[ -d "$DEP_DIR" ]]
|
||||
@ -17,17 +18,22 @@ then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
get_file "https://github.com/libusb/libusb/releases/download/v1.0.26/$FILENAME" "$FILENAME" "$SHA256SUM"
|
||||
get_file "https://github.com/libusb/libusb/releases/download/v$VERSION/$FILENAME" \
|
||||
"$FILENAME" "$SHA256SUM"
|
||||
|
||||
mkdir "$DEP_DIR"
|
||||
cd "$DEP_DIR"
|
||||
|
||||
7z x "../$FILENAME" \
|
||||
libusb-1.0.26-binaries/libusb-MinGW-Win32/bin/msys-usb-1.0.dll \
|
||||
libusb-1.0.26-binaries/libusb-MinGW-Win32/include/ \
|
||||
libusb-1.0.26-binaries/libusb-MinGW-x64/bin/msys-usb-1.0.dll \
|
||||
libusb-1.0.26-binaries/libusb-MinGW-x64/include/
|
||||
"libusb-$VERSION-binaries/libusb-MinGW-Win32/" \
|
||||
"libusb-$VERSION-binaries/libusb-MinGW-Win32/" \
|
||||
"libusb-$VERSION-binaries/libusb-MinGW-x64/" \
|
||||
"libusb-$VERSION-binaries/libusb-MinGW-x64/"
|
||||
|
||||
mv libusb-1.0.26-binaries/libusb-MinGW-Win32 .
|
||||
mv libusb-1.0.26-binaries/libusb-MinGW-x64 .
|
||||
rm -rf libusb-1.0.26-binaries
|
||||
mv "libusb-$VERSION-binaries/libusb-MinGW-Win32" .
|
||||
mv "libusb-$VERSION-binaries/libusb-MinGW-x64" .
|
||||
rm -rf "libusb-$VERSION-binaries"
|
||||
|
||||
# Rename the dll to get the same library name on all platforms
|
||||
mv libusb-MinGW-Win32/bin/msys-usb-1.0.dll libusb-MinGW-Win32/bin/libusb-1.0.dll
|
||||
mv libusb-MinGW-x64/bin/msys-usb-1.0.dll libusb-MinGW-x64/bin/libusb-1.0.dll
|
||||
|
@ -6,10 +6,11 @@ cd "$DIR"
|
||||
mkdir -p "$PREBUILT_DATA_DIR"
|
||||
cd "$PREBUILT_DATA_DIR"
|
||||
|
||||
DEP_DIR=SDL2-2.28.4
|
||||
VERSION=2.28.5
|
||||
DEP_DIR="SDL2-$VERSION"
|
||||
|
||||
FILENAME=SDL2-devel-2.28.4-mingw.tar.gz
|
||||
SHA256SUM=779d091072cf97291f80030f5232d97aa3d48ab0f2c14fe0b9d9a33c593cdc35
|
||||
FILENAME="SDL2-devel-$VERSION-mingw.tar.gz"
|
||||
SHA256SUM=3c0c655c2ebf67cad48fead72761d1601740ded30808952c3274ba223d226c21
|
||||
|
||||
if [[ -d "$DEP_DIR" ]]
|
||||
then
|
||||
@ -17,7 +18,8 @@ then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
get_file "https://libsdl.org/release/$FILENAME" "$FILENAME" "$SHA256SUM"
|
||||
get_file "https://github.com/libsdl-org/SDL/releases/download/release-$VERSION/$FILENAME" \
|
||||
"$FILENAME" "$SHA256SUM"
|
||||
|
||||
mkdir "$DEP_DIR"
|
||||
cd "$DEP_DIR"
|
||||
|
@ -13,7 +13,7 @@ BEGIN
|
||||
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
||||
VALUE "OriginalFilename", "scrcpy.exe"
|
||||
VALUE "ProductName", "scrcpy"
|
||||
VALUE "ProductVersion", "v2.2"
|
||||
VALUE "ProductVersion", "2.3.1"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
44
app/scrcpy.1
44
app/scrcpy.1
@ -124,7 +124,7 @@ Use USB device (if there is exactly one, like adb -d).
|
||||
Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
|
||||
|
||||
.TP
|
||||
.BI "\-\-disable-screensaver"
|
||||
.BI "\-\-disable\-screensaver"
|
||||
Disable screensaver while scrcpy is running.
|
||||
|
||||
.TP
|
||||
@ -141,6 +141,14 @@ The available display ids can be listed by \fB\-\-list\-displays\fR.
|
||||
|
||||
Default is 0.
|
||||
|
||||
.TP
|
||||
.BI "\-\-display\-orientation " value
|
||||
Set the initial display orientation.
|
||||
|
||||
Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270. The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation.
|
||||
|
||||
Default is 0.
|
||||
|
||||
.TP
|
||||
.B \-e, \-\-select\-tcpip
|
||||
Use TCP/IP device (if there is exactly one, like adb -e).
|
||||
@ -207,7 +215,9 @@ List displays available on the device.
|
||||
|
||||
.TP
|
||||
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR]
|
||||
Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise.
|
||||
Lock capture video orientation to \fIvalue\fR.
|
||||
|
||||
Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 90, 180, and 270. The values represent the clockwise rotation from the natural device orientation, in degrees.
|
||||
|
||||
Default is "unlocked".
|
||||
|
||||
@ -289,6 +299,10 @@ Disable video forwarding.
|
||||
.B \-\-no\-video\-playback
|
||||
Disable video playback on the computer.
|
||||
|
||||
.TP
|
||||
.BI "\-\-orientation " value
|
||||
Same as --display-orientation=value --record-orientation=value.
|
||||
|
||||
.TP
|
||||
.B \-\-otg
|
||||
Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable.
|
||||
@ -357,6 +371,14 @@ Inject key events for all input keys, and ignore text events.
|
||||
.BI "\-\-record\-format " format
|
||||
Force recording format (mp4, mkv, m4a, mka, opus, aac, flac or wav).
|
||||
|
||||
.TP
|
||||
.BI "\-\-record\-orientation " value
|
||||
Set the record orientation.
|
||||
|
||||
Possible values are 0, 90, 180 and 270. The number represents the clockwise rotation in degrees.
|
||||
|
||||
Default is 0.
|
||||
|
||||
.TP
|
||||
.BI "\-\-render\-driver " name
|
||||
Request SDL to use the given render driver (this is just a hint).
|
||||
@ -369,10 +391,6 @@ Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "me
|
||||
.B \-\-require\-audio
|
||||
By default, scrcpy mirrors only the video if audio capture fails on the device. This option makes scrcpy fail if audio is enabled but does not work.
|
||||
|
||||
.TP
|
||||
.BI "\-\-rotation " value
|
||||
Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise.
|
||||
|
||||
.TP
|
||||
.BI "\-s, \-\-serial " number
|
||||
The device serial number. Mandatory only if several devices are connected to adb.
|
||||
@ -534,6 +552,14 @@ Rotate display left
|
||||
.B MOD+Right
|
||||
Rotate display right
|
||||
|
||||
.TP
|
||||
.B MOD+Shift+Left, MOD+Shift+Right
|
||||
Flip display horizontally
|
||||
|
||||
.TP
|
||||
.B MOD+Shift+Up, MOD+Shift+Down
|
||||
Flip display vertically
|
||||
|
||||
.TP
|
||||
.B MOD+g
|
||||
Resize window to 1:1 (pixel\-perfect)
|
||||
@ -616,7 +642,11 @@ Enable/disable FPS counter (print frames/second in logs)
|
||||
|
||||
.TP
|
||||
.B Ctrl+click-and-move
|
||||
Pinch-to-zoom from the center of the screen
|
||||
Pinch-to-zoom and rotate from the center of the screen
|
||||
|
||||
.TP
|
||||
.B Shift+click-and-move
|
||||
Tilt (slide vertically with two fingers)
|
||||
|
||||
.TP
|
||||
.B Drag & drop APK file
|
||||
|
@ -458,6 +458,7 @@ sc_adb_list_devices(struct sc_intr *intr, unsigned flags,
|
||||
// in the buffer in a single pass
|
||||
LOGW("Result of \"adb devices -l\" does not fit in 64Kb. "
|
||||
"Please report an issue.");
|
||||
free(buf);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -4,16 +4,16 @@
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "trait/frame_sink.h"
|
||||
#include <util/audiobuf.h>
|
||||
#include <util/average.h>
|
||||
#include <util/thread.h>
|
||||
#include <util/tick.h>
|
||||
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libswresample/swresample.h>
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include "trait/frame_sink.h"
|
||||
#include "util/audiobuf.h"
|
||||
#include "util/average.h"
|
||||
#include "util/thread.h"
|
||||
#include "util/tick.h"
|
||||
|
||||
struct sc_audio_player {
|
||||
struct sc_frame_sink frame_sink;
|
||||
|
||||
|
198
app/src/cli.c
198
app/src/cli.c
@ -90,6 +90,9 @@ enum {
|
||||
OPT_CAMERA_AR,
|
||||
OPT_CAMERA_FPS,
|
||||
OPT_CAMERA_HIGH_SPEED,
|
||||
OPT_DISPLAY_ORIENTATION,
|
||||
OPT_RECORD_ORIENTATION,
|
||||
OPT_ORIENTATION,
|
||||
};
|
||||
|
||||
struct sc_option {
|
||||
@ -309,6 +312,17 @@ static const struct sc_option options[] = {
|
||||
" scrcpy --list-displays\n"
|
||||
"Default is 0.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_DISPLAY_ORIENTATION,
|
||||
.longopt = "display-orientation",
|
||||
.argdesc = "value",
|
||||
.text = "Set the initial display orientation.\n"
|
||||
"Possible values are 0, 90, 180, 270, flip0, flip90, flip180 "
|
||||
"and flip270. The number represents the clockwise rotation "
|
||||
"in degrees; the \"flip\" keyword applies a horizontal flip "
|
||||
"before the rotation.\n"
|
||||
"Default is 0.",
|
||||
},
|
||||
{
|
||||
.shortopt = 'e',
|
||||
.longopt = "select-tcpip",
|
||||
@ -399,11 +413,11 @@ static const struct sc_option options[] = {
|
||||
.longopt = "lock-video-orientation",
|
||||
.argdesc = "value",
|
||||
.optional_arg = true,
|
||||
.text = "Lock video orientation to value.\n"
|
||||
.text = "Lock capture video orientation to value.\n"
|
||||
"Possible values are \"unlocked\", \"initial\" (locked to the "
|
||||
"initial orientation), 0, 1, 2 and 3. Natural device "
|
||||
"orientation is 0, and each increment adds a 90 degrees "
|
||||
"rotation counterclockwise.\n"
|
||||
"initial orientation), 0, 90, 180 and 270. The values "
|
||||
"represent the clockwise rotation from the natural device "
|
||||
"orientation, in degrees.\n"
|
||||
"Default is \"unlocked\".\n"
|
||||
"Passing the option without argument is equivalent to passing "
|
||||
"\"initial\".",
|
||||
@ -512,6 +526,13 @@ static const struct sc_option options[] = {
|
||||
.longopt = "no-video-playback",
|
||||
.text = "Disable video playback on the computer.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_ORIENTATION,
|
||||
.longopt = "orientation",
|
||||
.argdesc = "value",
|
||||
.text = "Same as --display-orientation=value "
|
||||
"--record-orientation=value.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_OTG,
|
||||
.longopt = "otg",
|
||||
@ -597,6 +618,15 @@ static const struct sc_option options[] = {
|
||||
.text = "Force recording format (mp4, mkv, m4a, mka, opus, aac, flac "
|
||||
"or wav).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_RECORD_ORIENTATION,
|
||||
.longopt = "record-orientation",
|
||||
.argdesc = "value",
|
||||
.text = "Set the record orientation.\n"
|
||||
"Possible values are 0, 90, 180 and 270. The number represents "
|
||||
"the clockwise rotation in degrees.\n"
|
||||
"Default is 0.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_RENDER_DRIVER,
|
||||
.longopt = "render-driver",
|
||||
@ -615,12 +645,10 @@ static const struct sc_option options[] = {
|
||||
"is enabled but does not work."
|
||||
},
|
||||
{
|
||||
// deprecated
|
||||
.longopt_id = OPT_ROTATION,
|
||||
.longopt = "rotation",
|
||||
.argdesc = "value",
|
||||
.text = "Set the initial display rotation.\n"
|
||||
"Possible values are 0, 1, 2 and 3. Each increment adds a 90 "
|
||||
"degrees rotation counterclockwise.",
|
||||
},
|
||||
{
|
||||
.shortopt = 's',
|
||||
@ -824,6 +852,14 @@ static const struct sc_shortcut shortcuts[] = {
|
||||
.shortcuts = { "MOD+Right" },
|
||||
.text = "Rotate display right",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "MOD+Shift+Left", "MOD+Shift+Right" },
|
||||
.text = "Flip display horizontally",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "MOD+Shift+Up", "MOD+Shift+Down" },
|
||||
.text = "Flip display vertically",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "MOD+g" },
|
||||
.text = "Resize window to 1:1 (pixel-perfect)",
|
||||
@ -911,7 +947,11 @@ static const struct sc_shortcut shortcuts[] = {
|
||||
},
|
||||
{
|
||||
.shortcuts = { "Ctrl+click-and-move" },
|
||||
.text = "Pinch-to-zoom from the center of the screen",
|
||||
.text = "Pinch-to-zoom and rotate from the center of the screen",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "Shift+click-and-move" },
|
||||
.text = "Tilt (slide vertically with two fingers)",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "Drag & drop APK file" },
|
||||
@ -1382,15 +1422,50 @@ parse_lock_video_orientation(const char *s,
|
||||
return true;
|
||||
}
|
||||
|
||||
long value;
|
||||
bool ok = parse_integer_arg(s, &value, false, 0, 3,
|
||||
"lock video orientation");
|
||||
if (!ok) {
|
||||
return false;
|
||||
if (!strcmp(s, "0")) {
|
||||
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_0;
|
||||
return true;
|
||||
}
|
||||
|
||||
*lock_mode = (enum sc_lock_video_orientation) value;
|
||||
return true;
|
||||
if (!strcmp(s, "90")) {
|
||||
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_90;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(s, "180")) {
|
||||
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_180;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(s, "270")) {
|
||||
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_270;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(s, "1")) {
|
||||
LOGW("--lock-video-orientation=1 is deprecated, use "
|
||||
"--lock-video-orientation=270 instead.");
|
||||
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_270;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(s, "2")) {
|
||||
LOGW("--lock-video-orientation=2 is deprecated, use "
|
||||
"--lock-video-orientation=180 instead.");
|
||||
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_180;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(s, "3")) {
|
||||
LOGW("--lock-video-orientation=3 is deprecated, use "
|
||||
"--lock-video-orientation=90 instead.");
|
||||
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_90;
|
||||
return true;
|
||||
}
|
||||
|
||||
LOGE("Unsupported --lock-video-orientation value: %s (expected initial, "
|
||||
"unlocked, 0, 90, 180 or 270).", s);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
@ -1405,6 +1480,45 @@ parse_rotation(const char *s, uint8_t *rotation) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_orientation(const char *s, enum sc_orientation *orientation) {
|
||||
if (!strcmp(s, "0")) {
|
||||
*orientation = SC_ORIENTATION_0;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(s, "90")) {
|
||||
*orientation = SC_ORIENTATION_90;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(s, "180")) {
|
||||
*orientation = SC_ORIENTATION_180;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(s, "270")) {
|
||||
*orientation = SC_ORIENTATION_270;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(s, "flip0")) {
|
||||
*orientation = SC_ORIENTATION_FLIP_0;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(s, "flip90")) {
|
||||
*orientation = SC_ORIENTATION_FLIP_90;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(s, "flip180")) {
|
||||
*orientation = SC_ORIENTATION_FLIP_180;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(s, "flip270")) {
|
||||
*orientation = SC_ORIENTATION_FLIP_270;
|
||||
return true;
|
||||
}
|
||||
LOGE("Unsupported orientation: %s (expected 0, 90, 180, 270, flip0, "
|
||||
"flip90, flip180 or flip270)", optarg);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_window_position(const char *s, int16_t *position) {
|
||||
// special value for "auto"
|
||||
@ -2008,10 +2122,51 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW;
|
||||
break;
|
||||
case OPT_ROTATION:
|
||||
if (!parse_rotation(optarg, &opts->rotation)) {
|
||||
LOGW("--rotation is deprecated, use --display-orientation "
|
||||
"instead.");
|
||||
uint8_t rotation;
|
||||
if (!parse_rotation(optarg, &rotation)) {
|
||||
return false;
|
||||
}
|
||||
assert(rotation <= 3);
|
||||
switch (rotation) {
|
||||
case 0:
|
||||
opts->display_orientation = SC_ORIENTATION_0;
|
||||
break;
|
||||
case 1:
|
||||
// rotation 1 was 90° counterclockwise, but orientation
|
||||
// is expressed clockwise
|
||||
opts->display_orientation = SC_ORIENTATION_270;
|
||||
break;
|
||||
case 2:
|
||||
opts->display_orientation = SC_ORIENTATION_180;
|
||||
break;
|
||||
case 3:
|
||||
// rotation 3 was 270° counterclockwise, but orientation
|
||||
// is expressed clockwise
|
||||
opts->display_orientation = SC_ORIENTATION_90;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case OPT_DISPLAY_ORIENTATION:
|
||||
if (!parse_orientation(optarg, &opts->display_orientation)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_RECORD_ORIENTATION:
|
||||
if (!parse_orientation(optarg, &opts->record_orientation)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_ORIENTATION: {
|
||||
enum sc_orientation orientation;
|
||||
if (!parse_orientation(optarg, &orientation)) {
|
||||
return false;
|
||||
}
|
||||
opts->display_orientation = orientation;
|
||||
opts->record_orientation = orientation;
|
||||
break;
|
||||
}
|
||||
case OPT_RENDER_DRIVER:
|
||||
opts->render_driver = optarg;
|
||||
break;
|
||||
@ -2243,6 +2398,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
|
||||
if (!opts->video) {
|
||||
opts->video_playback = false;
|
||||
// Do not power on the device on start if video capture is disabled
|
||||
opts->power_on = false;
|
||||
}
|
||||
|
||||
if (!opts->audio) {
|
||||
@ -2378,6 +2535,15 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
}
|
||||
|
||||
if (opts->record_orientation != SC_ORIENTATION_0) {
|
||||
if (sc_orientation_is_mirror(opts->record_orientation)) {
|
||||
LOGE("Record orientation only supports rotation, not "
|
||||
"flipping: %s",
|
||||
sc_orientation_get_name(opts->record_orientation));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (opts->video
|
||||
&& sc_record_format_is_audio_only(opts->record_format)) {
|
||||
LOGE("Audio container does not support video stream");
|
||||
|
@ -3,7 +3,9 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <libavcodec/version.h>
|
||||
#include <libavformat/version.h>
|
||||
#include <libavutil/version.h>
|
||||
#include <SDL2/SDL_version.h>
|
||||
|
||||
#ifndef __WIN32
|
||||
@ -50,6 +52,15 @@
|
||||
# define SCRCPY_LAVU_HAS_CHLAYOUT
|
||||
#endif
|
||||
|
||||
// In ffmpeg/doc/APIchanges:
|
||||
// 2023-10-06 - 5432d2aacad - lavc 60.15.100 - avformat.h
|
||||
// Deprecate AVFormatContext.{nb_,}side_data, av_stream_add_side_data(),
|
||||
// av_stream_new_side_data(), and av_stream_get_side_data(). Side data fields
|
||||
// from AVFormatContext.codecpar should be used from now on.
|
||||
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(60, 15, 100)
|
||||
# define SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA
|
||||
#endif
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 6)
|
||||
// <https://github.com/libsdl-org/SDL/commit/d7a318de563125e5bb465b1000d6bc9576fbc6fc>
|
||||
# define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS
|
||||
|
@ -227,8 +227,9 @@ run_demuxer(void *data) {
|
||||
}
|
||||
|
||||
// Config packets must be merged with the next non-config packet only for
|
||||
// video streams
|
||||
bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO;
|
||||
// H.26x
|
||||
bool must_merge_config_packet = raw_codec_id == SC_CODEC_ID_H264
|
||||
|| raw_codec_id == SC_CODEC_ID_H265;
|
||||
|
||||
struct sc_packet_merger merger;
|
||||
|
||||
|
@ -234,7 +234,7 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame) {
|
||||
|
||||
enum sc_display_result
|
||||
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
|
||||
unsigned rotation) {
|
||||
enum sc_orientation orientation) {
|
||||
SDL_RenderClear(display->renderer);
|
||||
|
||||
if (display->pending.flags) {
|
||||
@ -247,33 +247,33 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
|
||||
SDL_Renderer *renderer = display->renderer;
|
||||
SDL_Texture *texture = display->texture;
|
||||
|
||||
if (rotation == 0) {
|
||||
if (orientation == SC_ORIENTATION_0) {
|
||||
int ret = SDL_RenderCopy(renderer, texture, NULL, geometry);
|
||||
if (ret) {
|
||||
LOGE("Could not render texture: %s", SDL_GetError());
|
||||
return SC_DISPLAY_RESULT_ERROR;
|
||||
}
|
||||
} else {
|
||||
// rotation in RenderCopyEx() is clockwise, while screen->rotation is
|
||||
// counterclockwise (to be consistent with --lock-video-orientation)
|
||||
int cw_rotation = (4 - rotation) % 4;
|
||||
unsigned cw_rotation = sc_orientation_get_rotation(orientation);
|
||||
double angle = 90 * cw_rotation;
|
||||
|
||||
const SDL_Rect *dstrect = NULL;
|
||||
SDL_Rect rect;
|
||||
if (rotation & 1) {
|
||||
if (sc_orientation_is_swap(orientation)) {
|
||||
rect.x = geometry->x + (geometry->w - geometry->h) / 2;
|
||||
rect.y = geometry->y + (geometry->h - geometry->w) / 2;
|
||||
rect.w = geometry->h;
|
||||
rect.h = geometry->w;
|
||||
dstrect = ▭
|
||||
} else {
|
||||
assert(rotation == 2);
|
||||
dstrect = geometry;
|
||||
}
|
||||
|
||||
SDL_RendererFlip flip = sc_orientation_is_mirror(orientation)
|
||||
? SDL_FLIP_HORIZONTAL : 0;
|
||||
|
||||
int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle,
|
||||
NULL, 0);
|
||||
NULL, flip);
|
||||
if (ret) {
|
||||
LOGE("Could not render texture: %s", SDL_GetError());
|
||||
return SC_DISPLAY_RESULT_ERROR;
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
#include "coords.h"
|
||||
#include "opengl.h"
|
||||
#include "options.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
@ -54,6 +55,6 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame);
|
||||
|
||||
enum sc_display_result
|
||||
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
|
||||
unsigned rotation);
|
||||
enum sc_orientation orientation);
|
||||
|
||||
#endif
|
||||
|
@ -76,6 +76,8 @@ sc_input_manager_init(struct sc_input_manager *im,
|
||||
im->sdl_shortcut_mods.count = shortcut_mods->count;
|
||||
|
||||
im->vfinger_down = false;
|
||||
im->vfinger_invert_x = false;
|
||||
im->vfinger_invert_y = false;
|
||||
|
||||
im->last_keycode = SDLK_UNKNOWN;
|
||||
im->last_mod = 0;
|
||||
@ -293,15 +295,11 @@ rotate_device(struct sc_controller *controller) {
|
||||
}
|
||||
|
||||
static void
|
||||
rotate_client_left(struct sc_screen *screen) {
|
||||
unsigned new_rotation = (screen->rotation + 1) % 4;
|
||||
sc_screen_set_rotation(screen, new_rotation);
|
||||
}
|
||||
|
||||
static void
|
||||
rotate_client_right(struct sc_screen *screen) {
|
||||
unsigned new_rotation = (screen->rotation + 3) % 4;
|
||||
sc_screen_set_rotation(screen, new_rotation);
|
||||
apply_orientation_transform(struct sc_screen *screen,
|
||||
enum sc_orientation transform) {
|
||||
enum sc_orientation new_orientation =
|
||||
sc_orientation_apply(screen->orientation, transform);
|
||||
sc_screen_set_orientation(screen, new_orientation);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -351,9 +349,14 @@ simulate_virtual_finger(struct sc_input_manager *im,
|
||||
}
|
||||
|
||||
static struct sc_point
|
||||
inverse_point(struct sc_point point, struct sc_size size) {
|
||||
point.x = size.width - point.x;
|
||||
point.y = size.height - point.y;
|
||||
inverse_point(struct sc_point point, struct sc_size size,
|
||||
bool invert_x, bool invert_y) {
|
||||
if (invert_x) {
|
||||
point.x = size.width - point.x;
|
||||
}
|
||||
if (invert_y) {
|
||||
point.y = size.height - point.y;
|
||||
}
|
||||
return point;
|
||||
}
|
||||
|
||||
@ -421,25 +424,47 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
}
|
||||
return;
|
||||
case SDLK_DOWN:
|
||||
if (controller && !shift) {
|
||||
if (shift) {
|
||||
if (!repeat & down) {
|
||||
apply_orientation_transform(im->screen,
|
||||
SC_ORIENTATION_FLIP_180);
|
||||
}
|
||||
} else if (controller) {
|
||||
// forward repeated events
|
||||
action_volume_down(controller, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_UP:
|
||||
if (controller && !shift) {
|
||||
if (shift) {
|
||||
if (!repeat & down) {
|
||||
apply_orientation_transform(im->screen,
|
||||
SC_ORIENTATION_FLIP_180);
|
||||
}
|
||||
} else if (controller) {
|
||||
// forward repeated events
|
||||
action_volume_up(controller, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_LEFT:
|
||||
if (!shift && !repeat && down) {
|
||||
rotate_client_left(im->screen);
|
||||
if (!repeat && down) {
|
||||
if (shift) {
|
||||
apply_orientation_transform(im->screen,
|
||||
SC_ORIENTATION_FLIP_0);
|
||||
} else {
|
||||
apply_orientation_transform(im->screen,
|
||||
SC_ORIENTATION_270);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_RIGHT:
|
||||
if (!shift && !repeat && down) {
|
||||
rotate_client_right(im->screen);
|
||||
if (!repeat && down) {
|
||||
if (shift) {
|
||||
apply_orientation_transform(im->screen,
|
||||
SC_ORIENTATION_FLIP_0);
|
||||
} else {
|
||||
apply_orientation_transform(im->screen,
|
||||
SC_ORIENTATION_90);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_c:
|
||||
@ -587,7 +612,9 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
|
||||
struct sc_point mouse =
|
||||
sc_screen_convert_window_to_frame_coords(im->screen, event->x,
|
||||
event->y);
|
||||
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size);
|
||||
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size,
|
||||
im->vfinger_invert_x,
|
||||
im->vfinger_invert_y);
|
||||
simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger);
|
||||
}
|
||||
}
|
||||
@ -708,7 +735,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
||||
return;
|
||||
}
|
||||
|
||||
// Pinch-to-zoom simulation.
|
||||
// Pinch-to-zoom, rotate and tilt simulation.
|
||||
//
|
||||
// If Ctrl is hold when the left-click button is pressed, then
|
||||
// pinch-to-zoom mode is enabled: on every mouse event until the left-click
|
||||
@ -717,14 +744,29 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
||||
//
|
||||
// In other words, the center of the rotation/scaling is the center of the
|
||||
// screen.
|
||||
#define CTRL_PRESSED (SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL))
|
||||
//
|
||||
// To simulate a tilt gesture (a vertical slide with two fingers), Shift
|
||||
// can be used instead of Ctrl. The "virtual finger" has a position
|
||||
// inverted with respect to the vertical axis of symmetry in the middle of
|
||||
// the screen.
|
||||
const SDL_Keymod keymod = SDL_GetModState();
|
||||
const bool ctrl_pressed = keymod & KMOD_CTRL;
|
||||
const bool shift_pressed = keymod & KMOD_SHIFT;
|
||||
if (event->button == SDL_BUTTON_LEFT &&
|
||||
((down && !im->vfinger_down && CTRL_PRESSED) ||
|
||||
((down && !im->vfinger_down &&
|
||||
((ctrl_pressed && !shift_pressed) ||
|
||||
(!ctrl_pressed && shift_pressed))) ||
|
||||
(!down && im->vfinger_down))) {
|
||||
struct sc_point mouse =
|
||||
sc_screen_convert_window_to_frame_coords(im->screen, event->x,
|
||||
event->y);
|
||||
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size);
|
||||
if (down) {
|
||||
im->vfinger_invert_x = ctrl_pressed || shift_pressed;
|
||||
im->vfinger_invert_y = ctrl_pressed;
|
||||
}
|
||||
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size,
|
||||
im->vfinger_invert_x,
|
||||
im->vfinger_invert_y);
|
||||
enum android_motionevent_action action = down
|
||||
? AMOTION_EVENT_ACTION_DOWN
|
||||
: AMOTION_EVENT_ACTION_UP;
|
||||
|
@ -32,6 +32,8 @@ struct sc_input_manager {
|
||||
} sdl_shortcut_mods;
|
||||
|
||||
bool vfinger_down;
|
||||
bool vfinger_invert_x;
|
||||
bool vfinger_invert_y;
|
||||
|
||||
// Tracks the number of identical consecutive shortcut key down events.
|
||||
// Not to be confused with event->repeat, which counts the number of
|
||||
|
@ -39,7 +39,8 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.audio_bit_rate = 0,
|
||||
.max_fps = 0,
|
||||
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
|
||||
.rotation = 0,
|
||||
.display_orientation = SC_ORIENTATION_0,
|
||||
.record_orientation = SC_ORIENTATION_0,
|
||||
.window_x = SC_WINDOW_POSITION_UNDEFINED,
|
||||
.window_y = SC_WINDOW_POSITION_UNDEFINED,
|
||||
.window_width = 0,
|
||||
@ -89,3 +90,39 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.camera_high_speed = false,
|
||||
.list = 0,
|
||||
};
|
||||
|
||||
enum sc_orientation
|
||||
sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform) {
|
||||
assert(!(src & ~7));
|
||||
assert(!(transform & ~7));
|
||||
|
||||
unsigned transform_hflip = transform & 4;
|
||||
unsigned transform_rotation = transform & 3;
|
||||
unsigned src_hflip = src & 4;
|
||||
unsigned src_rotation = src & 3;
|
||||
unsigned src_swap = src & 1;
|
||||
if (src_swap && transform_hflip) {
|
||||
// If the src is rotated by 90 or 270 degrees, applying a flipped
|
||||
// transformation requires an additional 180 degrees rotation to
|
||||
// compensate for the inversion of the order of multiplication:
|
||||
//
|
||||
// hflip1 × rotate1 × hflip2 × rotate2
|
||||
// `--------------' `--------------'
|
||||
// src transform
|
||||
//
|
||||
// In the final result, we want all the hflips then all the rotations,
|
||||
// so we must move hflip2 to the left:
|
||||
//
|
||||
// hflip1 × hflip2 × rotate1' × rotate2
|
||||
//
|
||||
// with rotate1' = | rotate1 if src is 0° or 180°
|
||||
// | rotate1 + 180° if src is 90° or 270°
|
||||
|
||||
src_rotation += 2;
|
||||
}
|
||||
|
||||
unsigned result_hflip = src_hflip ^ transform_hflip;
|
||||
unsigned result_rotation = (transform_rotation + src_rotation) % 4;
|
||||
enum sc_orientation result = result_hflip | result_rotation;
|
||||
return result;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
@ -67,14 +68,75 @@ enum sc_camera_facing {
|
||||
SC_CAMERA_FACING_EXTERNAL,
|
||||
};
|
||||
|
||||
// ,----- hflip (applied before the rotation)
|
||||
// | ,--- 180°
|
||||
// | | ,- 90° clockwise
|
||||
// | | |
|
||||
enum sc_orientation { // v v v
|
||||
SC_ORIENTATION_0, // 0 0 0
|
||||
SC_ORIENTATION_90, // 0 0 1
|
||||
SC_ORIENTATION_180, // 0 1 0
|
||||
SC_ORIENTATION_270, // 0 1 1
|
||||
SC_ORIENTATION_FLIP_0, // 1 0 0
|
||||
SC_ORIENTATION_FLIP_90, // 1 0 1
|
||||
SC_ORIENTATION_FLIP_180, // 1 1 0
|
||||
SC_ORIENTATION_FLIP_270, // 1 1 1
|
||||
};
|
||||
|
||||
static inline bool
|
||||
sc_orientation_is_mirror(enum sc_orientation orientation) {
|
||||
assert(!(orientation & ~7));
|
||||
return orientation & 4;
|
||||
}
|
||||
|
||||
// Does the orientation swap width and height?
|
||||
static inline bool
|
||||
sc_orientation_is_swap(enum sc_orientation orientation) {
|
||||
assert(!(orientation & ~7));
|
||||
return orientation & 1;
|
||||
}
|
||||
|
||||
static inline enum sc_orientation
|
||||
sc_orientation_get_rotation(enum sc_orientation orientation) {
|
||||
assert(!(orientation & ~7));
|
||||
return orientation & 3;
|
||||
}
|
||||
|
||||
enum sc_orientation
|
||||
sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform);
|
||||
|
||||
static inline const char *
|
||||
sc_orientation_get_name(enum sc_orientation orientation) {
|
||||
switch (orientation) {
|
||||
case SC_ORIENTATION_0:
|
||||
return "0";
|
||||
case SC_ORIENTATION_90:
|
||||
return "90";
|
||||
case SC_ORIENTATION_180:
|
||||
return "180";
|
||||
case SC_ORIENTATION_270:
|
||||
return "270";
|
||||
case SC_ORIENTATION_FLIP_0:
|
||||
return "flip0";
|
||||
case SC_ORIENTATION_FLIP_90:
|
||||
return "flip90";
|
||||
case SC_ORIENTATION_FLIP_180:
|
||||
return "flip180";
|
||||
case SC_ORIENTATION_FLIP_270:
|
||||
return "flip270";
|
||||
default:
|
||||
return "(unknown)";
|
||||
}
|
||||
}
|
||||
|
||||
enum sc_lock_video_orientation {
|
||||
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
|
||||
// lock the current orientation when scrcpy starts
|
||||
SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2,
|
||||
SC_LOCK_VIDEO_ORIENTATION_0 = 0,
|
||||
SC_LOCK_VIDEO_ORIENTATION_1,
|
||||
SC_LOCK_VIDEO_ORIENTATION_2,
|
||||
SC_LOCK_VIDEO_ORIENTATION_3,
|
||||
SC_LOCK_VIDEO_ORIENTATION_90 = 3,
|
||||
SC_LOCK_VIDEO_ORIENTATION_180 = 2,
|
||||
SC_LOCK_VIDEO_ORIENTATION_270 = 1,
|
||||
};
|
||||
|
||||
enum sc_keyboard_input_mode {
|
||||
@ -157,7 +219,8 @@ struct scrcpy_options {
|
||||
uint32_t audio_bit_rate;
|
||||
uint16_t max_fps;
|
||||
enum sc_lock_video_orientation lock_video_orientation;
|
||||
uint8_t rotation;
|
||||
enum sc_orientation display_orientation;
|
||||
enum sc_orientation record_orientation;
|
||||
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
|
||||
int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
|
||||
uint16_t window_width;
|
||||
|
@ -4,6 +4,7 @@
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/time.h>
|
||||
#include <libavutil/display.h>
|
||||
|
||||
#include "util/log.h"
|
||||
#include "util/str.h"
|
||||
@ -493,6 +494,42 @@ run_recorder(void *data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_set_orientation(AVStream *stream, enum sc_orientation orientation) {
|
||||
assert(!sc_orientation_is_mirror(orientation));
|
||||
|
||||
uint8_t *raw_data;
|
||||
#ifdef SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA
|
||||
AVPacketSideData *sd =
|
||||
av_packet_side_data_new(&stream->codecpar->coded_side_data,
|
||||
&stream->codecpar->nb_coded_side_data,
|
||||
AV_PKT_DATA_DISPLAYMATRIX,
|
||||
sizeof(int32_t) * 9, 0);
|
||||
if (!sd) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
raw_data = sd->data;
|
||||
#else
|
||||
raw_data = av_stream_new_side_data(stream, AV_PKT_DATA_DISPLAYMATRIX,
|
||||
sizeof(int32_t) * 9);
|
||||
if (!raw_data) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
int32_t *matrix = (int32_t *) raw_data;
|
||||
|
||||
unsigned rotation = orientation;
|
||||
unsigned angle = rotation * 90;
|
||||
|
||||
av_display_rotation_set(matrix, angle);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
|
||||
AVCodecContext *ctx) {
|
||||
@ -520,6 +557,16 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
|
||||
|
||||
recorder->video_stream.index = stream->index;
|
||||
|
||||
if (recorder->orientation != SC_ORIENTATION_0) {
|
||||
if (!sc_recorder_set_orientation(stream, recorder->orientation)) {
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGI("Record orientation set to %s",
|
||||
sc_orientation_get_name(recorder->orientation));
|
||||
}
|
||||
|
||||
recorder->video_init = true;
|
||||
sc_cond_signal(&recorder->cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
@ -689,7 +736,10 @@ sc_recorder_stream_init(struct sc_recorder_stream *stream) {
|
||||
bool
|
||||
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||
enum sc_record_format format, bool video, bool audio,
|
||||
enum sc_orientation orientation,
|
||||
const struct sc_recorder_callbacks *cbs, void *cbs_userdata) {
|
||||
assert(!sc_orientation_is_mirror(orientation));
|
||||
|
||||
recorder->filename = strdup(filename);
|
||||
if (!recorder->filename) {
|
||||
LOG_OOM();
|
||||
@ -710,6 +760,8 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||
recorder->video = video;
|
||||
recorder->audio = audio;
|
||||
|
||||
recorder->orientation = orientation;
|
||||
|
||||
sc_vecdeque_init(&recorder->video_queue);
|
||||
sc_vecdeque_init(&recorder->audio_queue);
|
||||
recorder->stopped = false;
|
||||
|
@ -34,6 +34,8 @@ struct sc_recorder {
|
||||
bool audio;
|
||||
bool video;
|
||||
|
||||
enum sc_orientation orientation;
|
||||
|
||||
char *filename;
|
||||
enum sc_record_format format;
|
||||
AVFormatContext *ctx;
|
||||
@ -67,6 +69,7 @@ struct sc_recorder_callbacks {
|
||||
bool
|
||||
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||
enum sc_record_format format, bool video, bool audio,
|
||||
enum sc_orientation orientation,
|
||||
const struct sc_recorder_callbacks *cbs, void *cbs_userdata);
|
||||
|
||||
bool
|
||||
|
@ -419,12 +419,21 @@ scrcpy(struct scrcpy_options *options) {
|
||||
sdl_set_hints(options->render_driver);
|
||||
}
|
||||
|
||||
// Initialize the video subsystem even if --no-video or --no-video-playback
|
||||
// is passed so that clipboard synchronization still works.
|
||||
// <https://github.com/Genymobile/scrcpy/issues/4418>
|
||||
if (SDL_Init(SDL_INIT_VIDEO)) {
|
||||
LOGE("Could not initialize SDL video: %s", SDL_GetError());
|
||||
goto end;
|
||||
if (options->video_playback ||
|
||||
(options->control && options->clipboard_autosync)) {
|
||||
// Initialize the video subsystem even if --no-video or
|
||||
// --no-video-playback is passed so that clipboard synchronization
|
||||
// still works.
|
||||
// <https://github.com/Genymobile/scrcpy/issues/4418>
|
||||
if (SDL_Init(SDL_INIT_VIDEO)) {
|
||||
// If it fails, it is an error only if video playback is enabled
|
||||
if (options->video_playback) {
|
||||
LOGE("Could not initialize SDL video: %s", SDL_GetError());
|
||||
goto end;
|
||||
} else {
|
||||
LOGW("Could not initialize SDL video: %s", SDL_GetError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options->audio_playback) {
|
||||
@ -507,7 +516,8 @@ scrcpy(struct scrcpy_options *options) {
|
||||
};
|
||||
if (!sc_recorder_init(&s->recorder, options->record_filename,
|
||||
options->record_format, options->video,
|
||||
options->audio, &recorder_cbs, NULL)) {
|
||||
options->audio, options->record_orientation,
|
||||
&recorder_cbs, NULL)) {
|
||||
goto end;
|
||||
}
|
||||
recorder_initialized = true;
|
||||
@ -691,7 +701,7 @@ aoa_hid_end:
|
||||
.window_width = options->window_width,
|
||||
.window_height = options->window_height,
|
||||
.window_borderless = options->window_borderless,
|
||||
.rotation = options->rotation,
|
||||
.orientation = options->display_orientation,
|
||||
.mipmaps = options->mipmaps,
|
||||
.fullscreen = options->fullscreen,
|
||||
.start_fps_counter = options->start_fps_counter,
|
||||
|
@ -14,16 +14,16 @@
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink)
|
||||
|
||||
static inline struct sc_size
|
||||
get_rotated_size(struct sc_size size, int rotation) {
|
||||
struct sc_size rotated_size;
|
||||
if (rotation & 1) {
|
||||
rotated_size.width = size.height;
|
||||
rotated_size.height = size.width;
|
||||
get_oriented_size(struct sc_size size, enum sc_orientation orientation) {
|
||||
struct sc_size oriented_size;
|
||||
if (sc_orientation_is_swap(orientation)) {
|
||||
oriented_size.width = size.height;
|
||||
oriented_size.height = size.width;
|
||||
} else {
|
||||
rotated_size.width = size.width;
|
||||
rotated_size.height = size.height;
|
||||
oriented_size.width = size.width;
|
||||
oriented_size.height = size.height;
|
||||
}
|
||||
return rotated_size;
|
||||
return oriented_size;
|
||||
}
|
||||
|
||||
// get the window size in a struct sc_size
|
||||
@ -251,7 +251,7 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
|
||||
}
|
||||
|
||||
enum sc_display_result res =
|
||||
sc_display_render(&screen->display, &screen->rect, screen->rotation);
|
||||
sc_display_render(&screen->display, &screen->rect, screen->orientation);
|
||||
(void) res; // any error already logged
|
||||
}
|
||||
|
||||
@ -379,9 +379,10 @@ sc_screen_init(struct sc_screen *screen,
|
||||
goto error_destroy_frame_buffer;
|
||||
}
|
||||
|
||||
screen->rotation = params->rotation;
|
||||
if (screen->rotation) {
|
||||
LOGI("Initial display rotation set to %u", screen->rotation);
|
||||
screen->orientation = params->orientation;
|
||||
if (screen->orientation != SC_ORIENTATION_0) {
|
||||
LOGI("Initial display orientation set to %s",
|
||||
sc_orientation_get_name(screen->orientation));
|
||||
}
|
||||
|
||||
uint32_t window_flags = SDL_WINDOW_HIDDEN
|
||||
@ -559,19 +560,19 @@ apply_pending_resize(struct sc_screen *screen) {
|
||||
}
|
||||
|
||||
void
|
||||
sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation) {
|
||||
assert(rotation < 4);
|
||||
if (rotation == screen->rotation) {
|
||||
sc_screen_set_orientation(struct sc_screen *screen,
|
||||
enum sc_orientation orientation) {
|
||||
if (orientation == screen->orientation) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct sc_size new_content_size =
|
||||
get_rotated_size(screen->frame_size, rotation);
|
||||
get_oriented_size(screen->frame_size, orientation);
|
||||
|
||||
set_content_size(screen, new_content_size);
|
||||
|
||||
screen->rotation = rotation;
|
||||
LOGI("Display rotation set to %u", rotation);
|
||||
screen->orientation = orientation;
|
||||
LOGI("Display orientation set to %s", sc_orientation_get_name(orientation));
|
||||
|
||||
sc_screen_render(screen, true);
|
||||
}
|
||||
@ -584,7 +585,7 @@ sc_screen_init_size(struct sc_screen *screen) {
|
||||
// The requested size is passed via screen->frame_size
|
||||
|
||||
struct sc_size content_size =
|
||||
get_rotated_size(screen->frame_size, screen->rotation);
|
||||
get_oriented_size(screen->frame_size, screen->orientation);
|
||||
screen->content_size = content_size;
|
||||
|
||||
enum sc_display_result res =
|
||||
@ -604,7 +605,7 @@ prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
|
||||
screen->frame_size = new_frame_size;
|
||||
|
||||
struct sc_size new_content_size =
|
||||
get_rotated_size(new_frame_size, screen->rotation);
|
||||
get_oriented_size(new_frame_size, screen->orientation);
|
||||
set_content_size(screen, new_content_size);
|
||||
|
||||
sc_screen_update_content_rect(screen);
|
||||
@ -843,8 +844,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
|
||||
struct sc_point
|
||||
sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen,
|
||||
int32_t x, int32_t y) {
|
||||
unsigned rotation = screen->rotation;
|
||||
assert(rotation < 4);
|
||||
enum sc_orientation orientation = screen->orientation;
|
||||
|
||||
int32_t w = screen->content_size.width;
|
||||
int32_t h = screen->content_size.height;
|
||||
@ -855,27 +855,43 @@ sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen,
|
||||
x = (int64_t) (x - screen->rect.x) * w / screen->rect.w;
|
||||
y = (int64_t) (y - screen->rect.y) * h / screen->rect.h;
|
||||
|
||||
// rotate
|
||||
struct sc_point result;
|
||||
switch (rotation) {
|
||||
case 0:
|
||||
switch (orientation) {
|
||||
case SC_ORIENTATION_0:
|
||||
result.x = x;
|
||||
result.y = y;
|
||||
break;
|
||||
case 1:
|
||||
result.x = h - y;
|
||||
result.y = x;
|
||||
break;
|
||||
case 2:
|
||||
result.x = w - x;
|
||||
result.y = h - y;
|
||||
break;
|
||||
default:
|
||||
assert(rotation == 3);
|
||||
case SC_ORIENTATION_90:
|
||||
result.x = y;
|
||||
result.y = w - x;
|
||||
break;
|
||||
case SC_ORIENTATION_180:
|
||||
result.x = w - x;
|
||||
result.y = h - y;
|
||||
break;
|
||||
case SC_ORIENTATION_270:
|
||||
result.x = h - y;
|
||||
result.y = x;
|
||||
break;
|
||||
case SC_ORIENTATION_FLIP_0:
|
||||
result.x = w - x;
|
||||
result.y = y;
|
||||
break;
|
||||
case SC_ORIENTATION_FLIP_90:
|
||||
result.x = h - y;
|
||||
result.y = w - x;
|
||||
break;
|
||||
case SC_ORIENTATION_FLIP_180:
|
||||
result.x = x;
|
||||
result.y = h - y;
|
||||
break;
|
||||
default:
|
||||
assert(orientation == SC_ORIENTATION_FLIP_270);
|
||||
result.x = y;
|
||||
result.y = x;
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
#include "frame_buffer.h"
|
||||
#include "input_manager.h"
|
||||
#include "opengl.h"
|
||||
#include "options.h"
|
||||
#include "trait/key_processor.h"
|
||||
#include "trait/frame_sink.h"
|
||||
#include "trait/mouse_processor.h"
|
||||
@ -49,8 +50,8 @@ struct sc_screen {
|
||||
// fullscreen (meaningful only when resize_pending is true)
|
||||
struct sc_size windowed_content_size;
|
||||
|
||||
// client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise)
|
||||
unsigned rotation;
|
||||
// client orientation
|
||||
enum sc_orientation orientation;
|
||||
// rectangle of the content (excluding black borders)
|
||||
struct SDL_Rect rect;
|
||||
bool has_frame;
|
||||
@ -86,7 +87,7 @@ struct sc_screen_params {
|
||||
|
||||
bool window_borderless;
|
||||
|
||||
uint8_t rotation;
|
||||
enum sc_orientation orientation;
|
||||
bool mipmaps;
|
||||
|
||||
bool fullscreen;
|
||||
@ -129,9 +130,10 @@ sc_screen_resize_to_fit(struct sc_screen *screen);
|
||||
void
|
||||
sc_screen_resize_to_pixel_perfect(struct sc_screen *screen);
|
||||
|
||||
// set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise)
|
||||
// set the display orientation
|
||||
void
|
||||
sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation);
|
||||
sc_screen_set_orientation(struct sc_screen *screen,
|
||||
enum sc_orientation orientation);
|
||||
|
||||
// react to SDL events
|
||||
// If this function returns false, scrcpy must exit with an error.
|
||||
|
@ -62,7 +62,7 @@ scrcpy_otg(struct scrcpy_options *options) {
|
||||
// Minimal SDL initialization
|
||||
if (SDL_Init(SDL_INIT_EVENTS)) {
|
||||
LOGE("Could not initialize SDL: %s", SDL_GetError());
|
||||
return false;
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
}
|
||||
|
||||
atexit(SDL_Quit);
|
||||
|
91
app/tests/test_orientation.c
Normal file
91
app/tests/test_orientation.c
Normal file
@ -0,0 +1,91 @@
|
||||
#include "common.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include "options.h"
|
||||
|
||||
static void test_transforms(void) {
|
||||
#define O(X) SC_ORIENTATION_ ## X
|
||||
#define ASSERT_TRANSFORM(SRC, TR, RES) \
|
||||
assert(sc_orientation_apply(O(SRC), O(TR)) == O(RES));
|
||||
|
||||
ASSERT_TRANSFORM(0, 0, 0);
|
||||
ASSERT_TRANSFORM(0, 90, 90);
|
||||
ASSERT_TRANSFORM(0, 180, 180);
|
||||
ASSERT_TRANSFORM(0, 270, 270);
|
||||
ASSERT_TRANSFORM(0, FLIP_0, FLIP_0);
|
||||
ASSERT_TRANSFORM(0, FLIP_90, FLIP_90);
|
||||
ASSERT_TRANSFORM(0, FLIP_180, FLIP_180);
|
||||
ASSERT_TRANSFORM(0, FLIP_270, FLIP_270);
|
||||
|
||||
ASSERT_TRANSFORM(90, 0, 90);
|
||||
ASSERT_TRANSFORM(90, 90, 180);
|
||||
ASSERT_TRANSFORM(90, 180, 270);
|
||||
ASSERT_TRANSFORM(90, 270, 0);
|
||||
ASSERT_TRANSFORM(90, FLIP_0, FLIP_270);
|
||||
ASSERT_TRANSFORM(90, FLIP_90, FLIP_0);
|
||||
ASSERT_TRANSFORM(90, FLIP_180, FLIP_90);
|
||||
ASSERT_TRANSFORM(90, FLIP_270, FLIP_180);
|
||||
|
||||
ASSERT_TRANSFORM(180, 0, 180);
|
||||
ASSERT_TRANSFORM(180, 90, 270);
|
||||
ASSERT_TRANSFORM(180, 180, 0);
|
||||
ASSERT_TRANSFORM(180, 270, 90);
|
||||
ASSERT_TRANSFORM(180, FLIP_0, FLIP_180);
|
||||
ASSERT_TRANSFORM(180, FLIP_90, FLIP_270);
|
||||
ASSERT_TRANSFORM(180, FLIP_180, FLIP_0);
|
||||
ASSERT_TRANSFORM(180, FLIP_270, FLIP_90);
|
||||
|
||||
ASSERT_TRANSFORM(270, 0, 270);
|
||||
ASSERT_TRANSFORM(270, 90, 0);
|
||||
ASSERT_TRANSFORM(270, 180, 90);
|
||||
ASSERT_TRANSFORM(270, 270, 180);
|
||||
ASSERT_TRANSFORM(270, FLIP_0, FLIP_90);
|
||||
ASSERT_TRANSFORM(270, FLIP_90, FLIP_180);
|
||||
ASSERT_TRANSFORM(270, FLIP_180, FLIP_270);
|
||||
ASSERT_TRANSFORM(270, FLIP_270, FLIP_0);
|
||||
|
||||
ASSERT_TRANSFORM(FLIP_0, 0, FLIP_0);
|
||||
ASSERT_TRANSFORM(FLIP_0, 90, FLIP_90);
|
||||
ASSERT_TRANSFORM(FLIP_0, 180, FLIP_180);
|
||||
ASSERT_TRANSFORM(FLIP_0, 270, FLIP_270);
|
||||
ASSERT_TRANSFORM(FLIP_0, FLIP_0, 0);
|
||||
ASSERT_TRANSFORM(FLIP_0, FLIP_90, 90);
|
||||
ASSERT_TRANSFORM(FLIP_0, FLIP_180, 180);
|
||||
ASSERT_TRANSFORM(FLIP_0, FLIP_270, 270);
|
||||
|
||||
ASSERT_TRANSFORM(FLIP_90, 0, FLIP_90);
|
||||
ASSERT_TRANSFORM(FLIP_90, 90, FLIP_180);
|
||||
ASSERT_TRANSFORM(FLIP_90, 180, FLIP_270);
|
||||
ASSERT_TRANSFORM(FLIP_90, 270, FLIP_0);
|
||||
ASSERT_TRANSFORM(FLIP_90, FLIP_0, 270);
|
||||
ASSERT_TRANSFORM(FLIP_90, FLIP_90, 0);
|
||||
ASSERT_TRANSFORM(FLIP_90, FLIP_180, 90);
|
||||
ASSERT_TRANSFORM(FLIP_90, FLIP_270, 180);
|
||||
|
||||
ASSERT_TRANSFORM(FLIP_180, 0, FLIP_180);
|
||||
ASSERT_TRANSFORM(FLIP_180, 90, FLIP_270);
|
||||
ASSERT_TRANSFORM(FLIP_180, 180, FLIP_0);
|
||||
ASSERT_TRANSFORM(FLIP_180, 270, FLIP_90);
|
||||
ASSERT_TRANSFORM(FLIP_180, FLIP_0, 180);
|
||||
ASSERT_TRANSFORM(FLIP_180, FLIP_90, 270);
|
||||
ASSERT_TRANSFORM(FLIP_180, FLIP_180, 0);
|
||||
ASSERT_TRANSFORM(FLIP_180, FLIP_270, 90);
|
||||
|
||||
ASSERT_TRANSFORM(FLIP_270, 0, FLIP_270);
|
||||
ASSERT_TRANSFORM(FLIP_270, 90, FLIP_0);
|
||||
ASSERT_TRANSFORM(FLIP_270, 180, FLIP_90);
|
||||
ASSERT_TRANSFORM(FLIP_270, 270, FLIP_180);
|
||||
ASSERT_TRANSFORM(FLIP_270, FLIP_0, 90);
|
||||
ASSERT_TRANSFORM(FLIP_270, FLIP_90, 180);
|
||||
ASSERT_TRANSFORM(FLIP_270, FLIP_180, 270);
|
||||
ASSERT_TRANSFORM(FLIP_270, FLIP_270, 0);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
test_transforms();
|
||||
return 0;
|
||||
}
|
@ -6,7 +6,7 @@ c = 'i686-w64-mingw32-gcc'
|
||||
cpp = 'i686-w64-mingw32-g++'
|
||||
ar = 'i686-w64-mingw32-ar'
|
||||
strip = 'i686-w64-mingw32-strip'
|
||||
pkgconfig = 'i686-w64-mingw32-pkg-config'
|
||||
pkg-config = 'i686-w64-mingw32-pkg-config'
|
||||
windres = 'i686-w64-mingw32-windres'
|
||||
|
||||
[host_machine]
|
||||
@ -14,8 +14,3 @@ system = 'windows'
|
||||
cpu_family = 'x86'
|
||||
cpu = 'i686'
|
||||
endian = 'little'
|
||||
|
||||
[properties]
|
||||
prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy-2/win32'
|
||||
prebuilt_sdl2 = 'SDL2-2.28.4/i686-w64-mingw32'
|
||||
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'
|
||||
|
@ -6,7 +6,7 @@ c = 'x86_64-w64-mingw32-gcc'
|
||||
cpp = 'x86_64-w64-mingw32-g++'
|
||||
ar = 'x86_64-w64-mingw32-ar'
|
||||
strip = 'x86_64-w64-mingw32-strip'
|
||||
pkgconfig = 'x86_64-w64-mingw32-pkg-config'
|
||||
pkg-config = 'x86_64-w64-mingw32-pkg-config'
|
||||
windres = 'x86_64-w64-mingw32-windres'
|
||||
|
||||
[host_machine]
|
||||
@ -14,8 +14,3 @@ system = 'windows'
|
||||
cpu_family = 'x86'
|
||||
cpu = 'x86_64'
|
||||
endian = 'little'
|
||||
|
||||
[properties]
|
||||
prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy-2/win64'
|
||||
prebuilt_sdl2 = 'SDL2-2.28.4/x86_64-w64-mingw32'
|
||||
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'
|
||||
|
@ -233,10 +233,10 @@ install` must be run as root)._
|
||||
|
||||
#### Option 2: Use prebuilt server
|
||||
|
||||
- [`scrcpy-server-v2.2`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `c85c4aa84305efb69115cd497a120ebdd10258993b4cf123a8245b3d99d49874`</sub>
|
||||
- [`scrcpy-server-v2.3.1`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b`</sub>
|
||||
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-server-v2.2
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
|
@ -18,6 +18,17 @@ scrcpy --video-source=display --audio-source=mic # force display AND micropho
|
||||
scrcpy --video-source=camera --audio-source=output # force camera AND device audio output
|
||||
```
|
||||
|
||||
Audio can be disabled:
|
||||
|
||||
```bash
|
||||
# audio not captured at all
|
||||
scrcpy --video-source=camera --no-audio
|
||||
scrcpy --video-source=camera --no-audio --record=file.mp4
|
||||
|
||||
# audio captured and recorded, but not played
|
||||
scrcpy --video-source=camera --no-audio-playback --record=file.mp4
|
||||
```
|
||||
|
||||
|
||||
## List
|
||||
|
||||
@ -101,6 +112,16 @@ scrcpy --video-source=camera --camera-size=1920x1080 -m3000 # error
|
||||
```
|
||||
|
||||
|
||||
## Rotation
|
||||
|
||||
To rotate the captured video, use the [video orientation](video.md#orientation)
|
||||
option:
|
||||
|
||||
```
|
||||
scrcpy --video-source=camera --camera-size=1920x1080 --orientation=90
|
||||
```
|
||||
|
||||
|
||||
## Frame rate
|
||||
|
||||
By default, camera is captured at Android's default frame rate (30 fps).
|
||||
|
@ -85,7 +85,7 @@ way as <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>).
|
||||
To disable automatic clipboard synchronization, use
|
||||
`--no-clipboard-autosync`.
|
||||
|
||||
## Pinch-to-zoom
|
||||
## Pinch-to-zoom, rotate and tilt simulation
|
||||
|
||||
To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_.
|
||||
|
||||
@ -93,8 +93,12 @@ More precisely, hold down <kbd>Ctrl</kbd> while pressing the left-click button.
|
||||
Until the left-click button is released, all mouse movements scale and rotate
|
||||
the content (if supported by the app) relative to the center of the screen.
|
||||
|
||||
To simulate a tilt gesture: <kbd>Shift</kbd>+_click-and-move-up-or-down_.
|
||||
|
||||
Technically, _scrcpy_ generates additional touch events from a "virtual finger"
|
||||
at a location inverted through the center of the screen.
|
||||
at a location inverted through the center of the screen. When pressing
|
||||
<kbd>Ctrl</kbd> the x and y coordinates are inverted. Using <kbd>Shift</kbd>
|
||||
only inverts x.
|
||||
|
||||
|
||||
## Key repeat
|
||||
|
@ -50,6 +50,12 @@ scrcpy --record=file --record-format=mkv
|
||||
```
|
||||
|
||||
|
||||
## Rotation
|
||||
|
||||
The video can be recorded rotated. See [video
|
||||
orientation](video.md#orientation).
|
||||
|
||||
|
||||
## No playback
|
||||
|
||||
To disable playback while recording:
|
||||
|
@ -26,6 +26,8 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
|
||||
| Switch fullscreen mode | <kbd>MOD</kbd>+<kbd>f</kbd>
|
||||
| Rotate display left | <kbd>MOD</kbd>+<kbd>←</kbd> _(left)_
|
||||
| Rotate display right | <kbd>MOD</kbd>+<kbd>→</kbd> _(right)_
|
||||
| Flip display horizontally | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>←</kbd> _(left)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>→</kbd> _(right)_
|
||||
| Flip display vertically | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>↑</kbd> _(up)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>↓</kbd> _(down)_
|
||||
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
|
||||
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
|
||||
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
|
||||
@ -47,7 +49,8 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
|
||||
| Synchronize clipboards and paste⁵ | <kbd>MOD</kbd>+<kbd>v</kbd>
|
||||
| Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
|
||||
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
|
||||
| Pinch-to-zoom | <kbd>Ctrl</kbd>+_click-and-move_
|
||||
| Pinch-to-zoom/rotate | <kbd>Ctrl</kbd>+_click-and-move_
|
||||
| Tilt (slide vertically with 2 fingers) | <kbd>Shift</kbd>+_click-and-move_
|
||||
| Drag & drop APK file | Install APK from computer
|
||||
| Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device)
|
||||
|
||||
|
@ -21,6 +21,13 @@ This will create a new video device in `/dev/videoN`, where `N` is an integer
|
||||
(more [options](https://github.com/umlaeute/v4l2loopback#options) are available
|
||||
to create several devices or devices with specific IDs).
|
||||
|
||||
If you encounter problems detecting your device with Chrome/WebRTC, you can try
|
||||
`exclusive_caps` mode:
|
||||
|
||||
```
|
||||
sudo modprobe v4l2loopback exclusive_caps=1
|
||||
```
|
||||
|
||||
To list the enabled devices:
|
||||
|
||||
```bash
|
||||
|
45
doc/video.md
45
doc/video.md
@ -97,39 +97,50 @@ scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc'
|
||||
```
|
||||
|
||||
|
||||
## Rotation
|
||||
## Orientation
|
||||
|
||||
The rotation may be applied at 3 different levels:
|
||||
The orientation may be applied at 3 different levels:
|
||||
- The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the
|
||||
device to switch between portrait and landscape (the current running app may
|
||||
refuse, if it does not support the requested orientation).
|
||||
- `--lock-video-orientation` changes the mirroring orientation (the orientation
|
||||
of the video sent from the device to the computer). This affects the
|
||||
recording.
|
||||
- `--rotation` rotates only the window content. This only affects the display,
|
||||
not the recording. It may be changed dynamically at any time using the
|
||||
[shortcuts](shortcuts.md) <kbd>MOD</kbd>+<kbd>←</kbd> and
|
||||
<kbd>MOD</kbd>+<kbd>→</kbd>.
|
||||
- `--orientation` is applied on the client side, and affects display and
|
||||
recording. For the display, it can be changed dynamically using
|
||||
[shortcuts](shortcuts.md).
|
||||
|
||||
To lock the mirroring orientation:
|
||||
To lock the mirroring orientation (on the capture side):
|
||||
|
||||
```bash
|
||||
scrcpy --lock-video-orientation # initial (current) orientation
|
||||
scrcpy --lock-video-orientation=0 # natural orientation
|
||||
scrcpy --lock-video-orientation=1 # 90° counterclockwise
|
||||
scrcpy --lock-video-orientation=2 # 180°
|
||||
scrcpy --lock-video-orientation=3 # 90° clockwise
|
||||
scrcpy --lock-video-orientation # initial (current) orientation
|
||||
scrcpy --lock-video-orientation=0 # natural orientation
|
||||
scrcpy --lock-video-orientation=90 # 90° clockwise
|
||||
scrcpy --lock-video-orientation=180 # 180°
|
||||
scrcpy --lock-video-orientation=270 # 270° clockwise
|
||||
```
|
||||
|
||||
To set an initial window rotation:
|
||||
To orient the video (on the rendering side):
|
||||
|
||||
```bash
|
||||
scrcpy --rotation=0 # no rotation
|
||||
scrcpy --rotation=1 # 90 degrees counterclockwise
|
||||
scrcpy --rotation=2 # 180 degrees
|
||||
scrcpy --rotation=3 # 90 degrees clockwise
|
||||
scrcpy --orientation=0
|
||||
scrcpy --orientation=90 # 90° clockwise
|
||||
scrcpy --orientation=180 # 180°
|
||||
scrcpy --orientation=270 # 270° clockwise
|
||||
scrcpy --orientation=flip0 # hflip
|
||||
scrcpy --orientation=flip90 # hflip + 90° clockwise
|
||||
scrcpy --orientation=flip180 # vflip (hflip + 180°)
|
||||
scrcpy --orientation=flip270 # hflip + 270° clockwise
|
||||
```
|
||||
|
||||
The orientation can be set separately for display and record if necessary, via
|
||||
`--display-orientation` and `--record-orientation`.
|
||||
|
||||
The rotation is applied to a recorded file by writing a display transformation
|
||||
to the MP4 or MKV target file. Flipping is not supported, so only the 4 first
|
||||
values are allowed when recording.
|
||||
|
||||
|
||||
## Crop
|
||||
|
||||
The device screen may be cropped to mirror only part of the screen.
|
||||
|
@ -4,14 +4,14 @@
|
||||
|
||||
Download the [latest release]:
|
||||
|
||||
- [`scrcpy-win64-v2.2.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `9f9da88ac4c8319dcb9bf852f2d9bba942bac663413383419cddf64eaa5685bd`</sub>
|
||||
- [`scrcpy-win32-v2.2.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `cb84269fc847b8b880e320879492a1ae6c017b42175f03e199530f7a53be9d74`</sub>
|
||||
- [`scrcpy-win64-v2.3.1.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `f1f78ac98214078425804e524a1bed515b9d4b8a05b78d210a4ced2b910b262d`</sub>
|
||||
- [`scrcpy-win32-v2.3.1.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `5dffc2d432e9b8b5b0e16f12e71428c37c70d9124cfbe7620df0b41b7efe91ff`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-win64-v2.2.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-win32-v2.2.zip
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win64-v2.3.1.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win32-v2.3.1.zip
|
||||
|
||||
and extract it.
|
||||
|
||||
|
@ -2,8 +2,8 @@
|
||||
set -e
|
||||
|
||||
BUILDDIR=build-auto
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-server-v2.2
|
||||
PREBUILT_SERVER_SHA256=c85c4aa84305efb69115cd497a120ebdd10258993b4cf123a8245b3d99d49874
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1
|
||||
PREBUILT_SERVER_SHA256=f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b
|
||||
|
||||
echo "[scrcpy] Downloading prebuilt server..."
|
||||
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
||||
|
@ -1,5 +1,5 @@
|
||||
project('scrcpy', 'c',
|
||||
version: 'v2.2',
|
||||
version: '2.3.1',
|
||||
meson_version: '>= 0.48',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
@ -16,5 +16,3 @@ endif
|
||||
if get_option('compile_server')
|
||||
subdir('server')
|
||||
endif
|
||||
|
||||
run_target('run', command: ['scripts/run-scrcpy.sh'])
|
||||
|
68
release.mk
68
release.mk
@ -69,58 +69,62 @@ prepare-deps:
|
||||
@app/prebuilt-deps/prepare-libusb.sh
|
||||
|
||||
build-win32: prepare-deps
|
||||
[ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \
|
||||
meson setup "$(WIN32_BUILD_DIR)" \
|
||||
--cross-file cross_win32.txt \
|
||||
--buildtype release --strip -Db_lto=true \
|
||||
-Dcompile_server=false \
|
||||
-Dportable=true )
|
||||
rm -rf "$(WIN32_BUILD_DIR)"
|
||||
mkdir -p "$(WIN32_BUILD_DIR)/local"
|
||||
cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win32/. "$(WIN32_BUILD_DIR)/local/"
|
||||
cp -r app/prebuilt-deps/data/SDL2-2.28.5/i686-w64-mingw32/. "$(WIN32_BUILD_DIR)/local/"
|
||||
cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/. "$(WIN32_BUILD_DIR)/local/"
|
||||
meson setup "$(WIN32_BUILD_DIR)" \
|
||||
--pkg-config-path="$(WIN32_BUILD_DIR)/local/lib/pkgconfig" \
|
||||
-Dc_args="-I$(PWD)/$(WIN32_BUILD_DIR)/local/include" \
|
||||
-Dc_link_args="-L$(PWD)/$(WIN32_BUILD_DIR)/local/lib" \
|
||||
--cross-file=cross_win32.txt \
|
||||
--buildtype=release --strip -Db_lto=true \
|
||||
-Dcompile_server=false \
|
||||
-Dportable=true
|
||||
ninja -C "$(WIN32_BUILD_DIR)"
|
||||
|
||||
build-win64: prepare-deps
|
||||
[ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \
|
||||
meson setup "$(WIN64_BUILD_DIR)" \
|
||||
--cross-file cross_win64.txt \
|
||||
--buildtype release --strip -Db_lto=true \
|
||||
-Dcompile_server=false \
|
||||
-Dportable=true )
|
||||
rm -rf "$(WIN64_BUILD_DIR)"
|
||||
mkdir -p "$(WIN64_BUILD_DIR)/local"
|
||||
cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win64/. "$(WIN64_BUILD_DIR)/local/"
|
||||
cp -r app/prebuilt-deps/data/SDL2-2.28.5/x86_64-w64-mingw32/. "$(WIN64_BUILD_DIR)/local/"
|
||||
cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/. "$(WIN64_BUILD_DIR)/local/"
|
||||
meson setup "$(WIN64_BUILD_DIR)" \
|
||||
--pkg-config-path="$(WIN64_BUILD_DIR)/local/lib/pkgconfig" \
|
||||
-Dc_args="-I$(PWD)/$(WIN64_BUILD_DIR)/local/include" \
|
||||
-Dc_link_args="-L$(PWD)/$(WIN64_BUILD_DIR)/local/lib" \
|
||||
--cross-file=cross_win64.txt \
|
||||
--buildtype=release --strip -Db_lto=true \
|
||||
-Dcompile_server=false \
|
||||
-Dportable=true
|
||||
ninja -C "$(WIN64_BUILD_DIR)"
|
||||
|
||||
dist-win32: build-server build-win32
|
||||
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/SDL2-2.28.4/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp "$(WIN32_BUILD_DIR)"/local/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
|
||||
dist-win64: build-server build-win64
|
||||
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/SDL2-2.28.4/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp "$(WIN64_BUILD_DIR)"/local/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
|
||||
zip-win32: dist-win32
|
||||
cd "$(DIST)"; \
|
||||
|
@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy"
|
@ -7,8 +7,8 @@ android {
|
||||
applicationId "com.genymobile.scrcpy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
versionCode 200
|
||||
versionName "v2.2"
|
||||
versionCode 20301
|
||||
versionName "2.3.1"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
|
@ -12,7 +12,7 @@
|
||||
set -e
|
||||
|
||||
SCRCPY_DEBUG=false
|
||||
SCRCPY_VERSION_NAME=v2.2
|
||||
SCRCPY_VERSION_NAME=2.3.1
|
||||
|
||||
PLATFORM=${ANDROID_PLATFORM:-34}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0}
|
||||
|
@ -11,6 +11,8 @@ public interface AsyncProcessor {
|
||||
}
|
||||
|
||||
void start(TerminationListener listener);
|
||||
|
||||
void stop();
|
||||
|
||||
void join() throws InterruptedException;
|
||||
}
|
||||
|
@ -153,13 +153,14 @@ public final class AudioCapture {
|
||||
previousRecorderTimestamp = timestamp.nanoTime;
|
||||
} else {
|
||||
if (nextPts == 0) {
|
||||
Ln.w("Could not get any audio timestamp");
|
||||
Ln.w("Could not get initial audio timestamp");
|
||||
nextPts = System.nanoTime() / 1000;
|
||||
}
|
||||
// compute from previous timestamp and packet size
|
||||
pts = nextPts;
|
||||
}
|
||||
|
||||
long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE);
|
||||
long durationUs = r * 1000000L / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE);
|
||||
nextPts = pts + durationUs;
|
||||
|
||||
if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) {
|
||||
|
@ -289,18 +289,17 @@ public class CameraCapture extends SurfaceCapture {
|
||||
List<OutputConfiguration> outputs = Arrays.asList(outputConfig);
|
||||
|
||||
int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR;
|
||||
SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor,
|
||||
new CameraCaptureSession.StateCallback() {
|
||||
@Override
|
||||
public void onConfigured(CameraCaptureSession session) {
|
||||
future.complete(session);
|
||||
}
|
||||
SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() {
|
||||
@Override
|
||||
public void onConfigured(CameraCaptureSession session) {
|
||||
future.complete(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigureFailed(CameraCaptureSession session) {
|
||||
future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR));
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onConfigureFailed(CameraCaptureSession session) {
|
||||
future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR));
|
||||
}
|
||||
});
|
||||
|
||||
camera.createCaptureSession(sessionConfig);
|
||||
|
||||
|
@ -1,11 +1,8 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.Base64;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Handle the cleanup of scrcpy, even if the main process is killed.
|
||||
@ -14,134 +11,64 @@ import java.io.IOException;
|
||||
*/
|
||||
public final class CleanUp {
|
||||
|
||||
public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
|
||||
private static final int MSG_TYPE_MASK = 0b11;
|
||||
private static final int MSG_TYPE_RESTORE_STAY_ON = 0;
|
||||
private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1;
|
||||
private static final int MSG_TYPE_RESTORE_NORMAL_POWER_MODE = 2;
|
||||
private static final int MSG_TYPE_POWER_OFF_SCREEN = 3;
|
||||
|
||||
// A simple struct to be passed from the main process to the cleanup process
|
||||
public static class Config implements Parcelable {
|
||||
private static final int MSG_PARAM_SHIFT = 2;
|
||||
|
||||
public static final Creator<Config> CREATOR = new Creator<Config>() {
|
||||
@Override
|
||||
public Config createFromParcel(Parcel in) {
|
||||
return new Config(in);
|
||||
}
|
||||
private final OutputStream out;
|
||||
|
||||
@Override
|
||||
public Config[] newArray(int size) {
|
||||
return new Config[size];
|
||||
}
|
||||
};
|
||||
|
||||
private static final int FLAG_DISABLE_SHOW_TOUCHES = 1;
|
||||
private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2;
|
||||
private static final int FLAG_POWER_OFF_SCREEN = 4;
|
||||
|
||||
private int displayId;
|
||||
|
||||
// Restore the value (between 0 and 7), -1 to not restore
|
||||
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
|
||||
private int restoreStayOn = -1;
|
||||
|
||||
private boolean disableShowTouches;
|
||||
private boolean restoreNormalPowerMode;
|
||||
private boolean powerOffScreen;
|
||||
|
||||
public Config() {
|
||||
// Default constructor, the fields are initialized by CleanUp.configure()
|
||||
}
|
||||
|
||||
protected Config(Parcel in) {
|
||||
displayId = in.readInt();
|
||||
restoreStayOn = in.readInt();
|
||||
byte options = in.readByte();
|
||||
disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0;
|
||||
restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0;
|
||||
powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(displayId);
|
||||
dest.writeInt(restoreStayOn);
|
||||
byte options = 0;
|
||||
if (disableShowTouches) {
|
||||
options |= FLAG_DISABLE_SHOW_TOUCHES;
|
||||
}
|
||||
if (restoreNormalPowerMode) {
|
||||
options |= FLAG_RESTORE_NORMAL_POWER_MODE;
|
||||
}
|
||||
if (powerOffScreen) {
|
||||
options |= FLAG_POWER_OFF_SCREEN;
|
||||
}
|
||||
dest.writeByte(options);
|
||||
}
|
||||
|
||||
private boolean hasWork() {
|
||||
return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
byte[] serialize() {
|
||||
Parcel parcel = Parcel.obtain();
|
||||
writeToParcel(parcel, 0);
|
||||
byte[] bytes = parcel.marshall();
|
||||
parcel.recycle();
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static Config deserialize(byte[] bytes) {
|
||||
Parcel parcel = Parcel.obtain();
|
||||
parcel.unmarshall(bytes, 0, bytes.length);
|
||||
parcel.setDataPosition(0);
|
||||
return CREATOR.createFromParcel(parcel);
|
||||
}
|
||||
|
||||
static Config fromBase64(String base64) {
|
||||
byte[] bytes = Base64.decode(base64, Base64.NO_WRAP);
|
||||
return deserialize(bytes);
|
||||
}
|
||||
|
||||
String toBase64() {
|
||||
byte[] bytes = serialize();
|
||||
return Base64.encodeToString(bytes, Base64.NO_WRAP);
|
||||
}
|
||||
public CleanUp(OutputStream out) {
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
private CleanUp() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen)
|
||||
throws IOException {
|
||||
Config config = new Config();
|
||||
config.displayId = displayId;
|
||||
config.disableShowTouches = disableShowTouches;
|
||||
config.restoreStayOn = restoreStayOn;
|
||||
config.restoreNormalPowerMode = restoreNormalPowerMode;
|
||||
config.powerOffScreen = powerOffScreen;
|
||||
|
||||
if (config.hasWork()) {
|
||||
startProcess(config);
|
||||
} else {
|
||||
// There is no additional clean up to do when scrcpy dies
|
||||
unlinkSelf();
|
||||
}
|
||||
}
|
||||
|
||||
private static void startProcess(Config config) throws IOException {
|
||||
String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()};
|
||||
public static CleanUp configure(int displayId) throws IOException {
|
||||
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)};
|
||||
|
||||
ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||
builder.environment().put("CLASSPATH", SERVER_PATH);
|
||||
builder.start();
|
||||
builder.environment().put("CLASSPATH", Server.SERVER_PATH);
|
||||
Process process = builder.start();
|
||||
return new CleanUp(process.getOutputStream());
|
||||
}
|
||||
|
||||
private boolean sendMessage(int type, int param) {
|
||||
assert (type & ~MSG_TYPE_MASK) == 0;
|
||||
int msg = type | param << MSG_PARAM_SHIFT;
|
||||
try {
|
||||
out.write(msg);
|
||||
out.flush();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Ln.w("Could not configure cleanup (type=" + type + ", param=" + param + ")", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean setRestoreStayOn(int restoreValue) {
|
||||
// Restore the value (between 0 and 7), -1 to not restore
|
||||
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
|
||||
assert restoreValue >= -1 && restoreValue <= 7;
|
||||
return sendMessage(MSG_TYPE_RESTORE_STAY_ON, restoreValue & 0b1111);
|
||||
}
|
||||
|
||||
public boolean setDisableShowTouches(boolean disableOnExit) {
|
||||
return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0);
|
||||
}
|
||||
|
||||
public boolean setRestoreNormalPowerMode(boolean restoreOnExit) {
|
||||
return sendMessage(MSG_TYPE_RESTORE_NORMAL_POWER_MODE, restoreOnExit ? 1 : 0);
|
||||
}
|
||||
|
||||
public boolean setPowerOffScreen(boolean powerOffScreenOnExit) {
|
||||
return sendMessage(MSG_TYPE_POWER_OFF_SCREEN, powerOffScreenOnExit ? 1 : 0);
|
||||
}
|
||||
|
||||
public static void unlinkSelf() {
|
||||
try {
|
||||
new File(SERVER_PATH).delete();
|
||||
new File(Server.SERVER_PATH).delete();
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not unlink server", e);
|
||||
}
|
||||
@ -150,44 +77,71 @@ public final class CleanUp {
|
||||
public static void main(String... args) {
|
||||
unlinkSelf();
|
||||
|
||||
int displayId = Integer.parseInt(args[0]);
|
||||
|
||||
int restoreStayOn = -1;
|
||||
boolean disableShowTouches = false;
|
||||
boolean restoreNormalPowerMode = false;
|
||||
boolean powerOffScreen = false;
|
||||
|
||||
try {
|
||||
// Wait for the server to die
|
||||
System.in.read();
|
||||
int msg;
|
||||
while ((msg = System.in.read()) != -1) {
|
||||
int type = msg & MSG_TYPE_MASK;
|
||||
int param = msg >> MSG_PARAM_SHIFT;
|
||||
switch (type) {
|
||||
case MSG_TYPE_RESTORE_STAY_ON:
|
||||
restoreStayOn = param > 7 ? -1 : param;
|
||||
break;
|
||||
case MSG_TYPE_DISABLE_SHOW_TOUCHES:
|
||||
disableShowTouches = param != 0;
|
||||
break;
|
||||
case MSG_TYPE_RESTORE_NORMAL_POWER_MODE:
|
||||
restoreNormalPowerMode = param != 0;
|
||||
break;
|
||||
case MSG_TYPE_POWER_OFF_SCREEN:
|
||||
powerOffScreen = param != 0;
|
||||
break;
|
||||
default:
|
||||
Ln.w("Unexpected msg type: " + type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Expected when the server is dead
|
||||
}
|
||||
|
||||
Ln.i("Cleaning up");
|
||||
|
||||
Config config = Config.fromBase64(args[0]);
|
||||
|
||||
if (config.disableShowTouches || config.restoreStayOn != -1) {
|
||||
if (config.disableShowTouches) {
|
||||
Ln.i("Disabling \"show touches\"");
|
||||
try {
|
||||
Settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not restore \"show_touches\"", e);
|
||||
}
|
||||
if (disableShowTouches) {
|
||||
Ln.i("Disabling \"show touches\"");
|
||||
try {
|
||||
Settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not restore \"show_touches\"", e);
|
||||
}
|
||||
if (config.restoreStayOn != -1) {
|
||||
Ln.i("Restoring \"stay awake\"");
|
||||
try {
|
||||
Settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not restore \"stay_on_while_plugged_in\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (restoreStayOn != -1) {
|
||||
Ln.i("Restoring \"stay awake\"");
|
||||
try {
|
||||
Settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn));
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not restore \"stay_on_while_plugged_in\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (Device.isScreenOn()) {
|
||||
if (config.powerOffScreen) {
|
||||
if (powerOffScreen) {
|
||||
Ln.i("Power off screen");
|
||||
Device.powerOffScreen(config.displayId);
|
||||
} else if (config.restoreNormalPowerMode) {
|
||||
Device.powerOffScreen(displayId);
|
||||
} else if (restoreNormalPowerMode) {
|
||||
Ln.i("Restoring normal power mode");
|
||||
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ public class Controller implements AsyncProcessor {
|
||||
|
||||
private final Device device;
|
||||
private final DesktopConnection connection;
|
||||
private final CleanUp cleanUp;
|
||||
private final DeviceMessageSender sender;
|
||||
private final boolean clipboardAutosync;
|
||||
private final boolean powerOn;
|
||||
@ -41,9 +42,10 @@ public class Controller implements AsyncProcessor {
|
||||
|
||||
private boolean keepPowerModeOff;
|
||||
|
||||
public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync, boolean powerOn) {
|
||||
public Controller(Device device, DesktopConnection connection, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) {
|
||||
this.device = device;
|
||||
this.connection = connection;
|
||||
this.cleanUp = cleanUp;
|
||||
this.clipboardAutosync = clipboardAutosync;
|
||||
this.powerOn = powerOn;
|
||||
initPointers();
|
||||
@ -170,6 +172,10 @@ public class Controller implements AsyncProcessor {
|
||||
if (setPowerModeOk) {
|
||||
keepPowerModeOff = mode == Device.POWER_MODE_OFF;
|
||||
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
||||
if (cleanUp != null) {
|
||||
boolean mustRestoreOnExit = mode != Device.POWER_MODE_NORMAL;
|
||||
cleanUp.setRestoreNormalPowerMode(mustRestoreOnExit);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -318,9 +324,8 @@ public class Controller implements AsyncProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent event = MotionEvent
|
||||
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
|
||||
0);
|
||||
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
|
||||
DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
@ -341,9 +346,8 @@ public class Controller implements AsyncProcessor {
|
||||
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
|
||||
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
|
||||
|
||||
MotionEvent event = MotionEvent
|
||||
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0,
|
||||
InputDevice.SOURCE_MOUSE, 0);
|
||||
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
|
||||
DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0);
|
||||
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
||||
import com.genymobile.scrcpy.wrappers.DisplayControl;
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||
@ -11,8 +12,8 @@ import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.SystemClock;
|
||||
import android.view.IRotationWatcher;
|
||||
import android.view.IDisplayFoldListener;
|
||||
import android.view.IRotationWatcher;
|
||||
import android.view.InputDevice;
|
||||
import android.view.InputEvent;
|
||||
import android.view.KeyCharacterMap;
|
||||
@ -44,11 +45,11 @@ public final class Device {
|
||||
void onClipboardTextChanged(String text);
|
||||
}
|
||||
|
||||
private final Size deviceSize;
|
||||
private final Rect crop;
|
||||
private int maxSize;
|
||||
private final int lockVideoOrientation;
|
||||
|
||||
private Size deviceSize;
|
||||
private ScreenInfo screenInfo;
|
||||
private RotationListener rotationListener;
|
||||
private FoldListener foldListener;
|
||||
@ -115,8 +116,8 @@ public final class Device {
|
||||
return;
|
||||
}
|
||||
|
||||
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(),
|
||||
options.getMaxSize(), options.getLockVideoOrientation());
|
||||
deviceSize = displayInfo.getSize();
|
||||
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation);
|
||||
// notify
|
||||
if (foldListener != null) {
|
||||
foldListener.onFoldChanged(displayId, folded);
|
||||
@ -163,6 +164,10 @@ public final class Device {
|
||||
}
|
||||
}
|
||||
|
||||
public int getDisplayId() {
|
||||
return displayId;
|
||||
}
|
||||
|
||||
public synchronized void setMaxSize(int newMaxSize) {
|
||||
maxSize = newMaxSize;
|
||||
screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation);
|
||||
@ -315,8 +320,12 @@ public final class Device {
|
||||
*/
|
||||
public static boolean setScreenPowerMode(int mode) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// On Android 14, these internal methods have been moved to DisplayControl
|
||||
boolean useDisplayControl =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasPhysicalDisplayIdsMethod();
|
||||
|
||||
// Change the power mode for all physical displays
|
||||
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
|
||||
long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds();
|
||||
if (physicalDisplayIds == null) {
|
||||
Ln.e("Could not get physical display ids");
|
||||
return false;
|
||||
@ -324,7 +333,8 @@ public final class Device {
|
||||
|
||||
boolean allOk = true;
|
||||
for (long physicalDisplayId : physicalDisplayIds) {
|
||||
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
|
||||
IBinder binder = useDisplayControl ? DisplayControl.getPhysicalDisplayToken(
|
||||
physicalDisplayId) : SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
|
||||
allOk &= SurfaceControl.setDisplayPowerMode(binder, mode);
|
||||
}
|
||||
return allOk;
|
||||
|
@ -51,6 +51,7 @@ public final class DeviceMessageSender {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
thread = new Thread(() -> {
|
||||
try {
|
||||
|
@ -1,8 +1,10 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.view.Surface;
|
||||
@ -11,6 +13,7 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList
|
||||
|
||||
private final Device device;
|
||||
private IBinder display;
|
||||
private VirtualDisplay virtualDisplay;
|
||||
|
||||
public ScreenCapture(Device device) {
|
||||
this.device = device;
|
||||
@ -34,9 +37,29 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList
|
||||
|
||||
if (display != null) {
|
||||
SurfaceControl.destroyDisplay(display);
|
||||
display = null;
|
||||
}
|
||||
if (virtualDisplay != null) {
|
||||
virtualDisplay.release();
|
||||
virtualDisplay = null;
|
||||
}
|
||||
|
||||
try {
|
||||
display = createDisplay();
|
||||
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
|
||||
Ln.d("Display: using SurfaceControl API");
|
||||
} catch (Exception surfaceControlException) {
|
||||
Rect videoRect = screenInfo.getVideoSize().toRect();
|
||||
try {
|
||||
virtualDisplay = ServiceManager.getDisplayManager()
|
||||
.createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), device.getDisplayId(), surface);
|
||||
Ln.d("Display: using DisplayManager API");
|
||||
} catch (Exception displayManagerException) {
|
||||
Ln.e("Could not create display using SurfaceControl", surfaceControlException);
|
||||
Ln.e("Could not create display using DisplayManager", displayManagerException);
|
||||
throw new AssertionError("Could not create display");
|
||||
}
|
||||
}
|
||||
display = createDisplay();
|
||||
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -69,7 +92,7 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList
|
||||
requestReset();
|
||||
}
|
||||
|
||||
private static IBinder createDisplay() {
|
||||
private static IBinder createDisplay() throws Exception {
|
||||
// Since Android 12 (preview), secure displays could not be created with shell permissions anymore.
|
||||
// On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S".
|
||||
boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S".equals(
|
||||
|
@ -3,12 +3,21 @@ package com.genymobile.scrcpy;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class Server {
|
||||
|
||||
public static final String SERVER_PATH;
|
||||
|
||||
static {
|
||||
String[] classPaths = System.getProperty("java.class.path").split(File.pathSeparator);
|
||||
// By convention, scrcpy is always executed with the absolute path of scrcpy-server.jar as the first item in the classpath
|
||||
SERVER_PATH = classPaths[0];
|
||||
}
|
||||
|
||||
private static class Completion {
|
||||
private int running;
|
||||
private boolean fatalError;
|
||||
@ -42,46 +51,47 @@ public final class Server {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
private static void initAndCleanUp(Options options) {
|
||||
boolean mustDisableShowTouchesOnCleanUp = false;
|
||||
int restoreStayOn = -1;
|
||||
boolean restoreNormalPowerMode = options.getControl(); // only restore power mode if control is enabled
|
||||
if (options.getShowTouches() || options.getStayAwake()) {
|
||||
if (options.getShowTouches()) {
|
||||
try {
|
||||
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
|
||||
// If "show touches" was disabled, it must be disabled back on clean up
|
||||
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"show_touches\"", e);
|
||||
}
|
||||
}
|
||||
private static void initAndCleanUp(Options options, CleanUp cleanUp) {
|
||||
// This method is called from its own thread, so it may only configure cleanup actions which are NOT dynamic (i.e. they are configured once
|
||||
// and for all, they cannot be changed from another thread)
|
||||
|
||||
if (options.getStayAwake()) {
|
||||
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
|
||||
try {
|
||||
String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
|
||||
try {
|
||||
restoreStayOn = Integer.parseInt(oldValue);
|
||||
if (restoreStayOn == stayOn) {
|
||||
// No need to restore
|
||||
restoreStayOn = -1;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
restoreStayOn = 0;
|
||||
if (options.getShowTouches()) {
|
||||
try {
|
||||
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
|
||||
// If "show touches" was disabled, it must be disabled back on clean up
|
||||
if (!"1".equals(oldValue)) {
|
||||
if (!cleanUp.setDisableShowTouches(true)) {
|
||||
Ln.e("Could not disable show touch on exit");
|
||||
}
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
|
||||
}
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"show_touches\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.getCleanup()) {
|
||||
if (options.getStayAwake()) {
|
||||
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
|
||||
try {
|
||||
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, restoreNormalPowerMode,
|
||||
options.getPowerOffScreenOnClose());
|
||||
} catch (IOException e) {
|
||||
Ln.e("Could not configure cleanup", e);
|
||||
String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
|
||||
try {
|
||||
int restoreStayOn = Integer.parseInt(oldValue);
|
||||
if (restoreStayOn != stayOn) {
|
||||
// Restore only if the current value is different
|
||||
if (!cleanUp.setRestoreStayOn(restoreStayOn)) {
|
||||
Ln.e("Could not restore stay on on exit");
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// ignore
|
||||
}
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.getPowerOffScreenOnClose()) {
|
||||
if (!cleanUp.setPowerOffScreen(true)) {
|
||||
Ln.e("Could not power off screen on exit");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -92,7 +102,13 @@ public final class Server {
|
||||
throw new ConfigurationException("Camera mirroring is not supported");
|
||||
}
|
||||
|
||||
Thread initThread = startInitThread(options);
|
||||
CleanUp cleanUp = null;
|
||||
Thread initThread = null;
|
||||
|
||||
if (options.getCleanup()) {
|
||||
cleanUp = CleanUp.configure(options.getDisplayId());
|
||||
initThread = startInitThread(options, cleanUp);
|
||||
}
|
||||
|
||||
int scid = options.getScid();
|
||||
boolean tunnelForward = options.isTunnelForward();
|
||||
@ -115,7 +131,7 @@ public final class Server {
|
||||
}
|
||||
|
||||
if (control) {
|
||||
Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
|
||||
Controller controller = new Controller(device, connection, cleanUp, options.getClipboardAutosync(), options.getPowerOn());
|
||||
device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
|
||||
asyncProcessors.add(controller);
|
||||
}
|
||||
@ -158,7 +174,9 @@ public final class Server {
|
||||
|
||||
completion.await();
|
||||
} finally {
|
||||
initThread.interrupt();
|
||||
if (initThread != null) {
|
||||
initThread.interrupt();
|
||||
}
|
||||
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
||||
asyncProcessor.stop();
|
||||
}
|
||||
@ -166,7 +184,9 @@ public final class Server {
|
||||
connection.shutdown();
|
||||
|
||||
try {
|
||||
initThread.join();
|
||||
if (initThread != null) {
|
||||
initThread.join();
|
||||
}
|
||||
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
||||
asyncProcessor.join();
|
||||
}
|
||||
@ -178,8 +198,8 @@ public final class Server {
|
||||
}
|
||||
}
|
||||
|
||||
private static Thread startInitThread(final Options options) {
|
||||
Thread thread = new Thread(() -> initAndCleanUp(options), "init-cleanup");
|
||||
private static Thread startInitThread(final Options options, final CleanUp cleanUp) {
|
||||
Thread thread = new Thread(() -> initAndCleanUp(options, cleanUp), "init-cleanup");
|
||||
thread.start();
|
||||
return thread;
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ public final class Settings {
|
||||
|
||||
String oldValue = getValue(table, key);
|
||||
if (!value.equals(oldValue)) {
|
||||
putValue(table, key, value);
|
||||
putValue(table, key, value);
|
||||
}
|
||||
return oldValue;
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ public final class Workarounds {
|
||||
}
|
||||
|
||||
public static void apply(boolean audio, boolean camera) {
|
||||
boolean mustFillConfigurationController = false;
|
||||
boolean mustFillAppInfo = false;
|
||||
boolean mustFillAppContext = false;
|
||||
|
||||
@ -85,11 +86,23 @@ public final class Workarounds {
|
||||
mustFillAppContext = true;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(),
|
||||
// which requires a non-null ConfigurationController.
|
||||
// ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions.
|
||||
// <https://github.com/Genymobile/scrcpy/issues/4467>
|
||||
mustFillConfigurationController = true;
|
||||
}
|
||||
|
||||
if (mustFillConfigurationController) {
|
||||
// Must be call before fillAppContext() because it is necessary to get a valid system context
|
||||
fillConfigurationController();
|
||||
}
|
||||
if (mustFillAppInfo) {
|
||||
Workarounds.fillAppInfo();
|
||||
fillAppInfo();
|
||||
}
|
||||
if (mustFillAppContext) {
|
||||
Workarounds.fillAppContext();
|
||||
fillAppContext();
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,6 +162,22 @@ public final class Workarounds {
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillConfigurationController() {
|
||||
try {
|
||||
Class<?> configurationControllerClass = Class.forName("android.app.ConfigurationController");
|
||||
Class<?> activityThreadInternalClass = Class.forName("android.app.ActivityThreadInternal");
|
||||
Constructor<?> configurationControllerConstructor = configurationControllerClass.getDeclaredConstructor(activityThreadInternalClass);
|
||||
configurationControllerConstructor.setAccessible(true);
|
||||
Object configurationController = configurationControllerConstructor.newInstance(ACTIVITY_THREAD);
|
||||
|
||||
Field configurationControllerField = ACTIVITY_THREAD_CLASS.getDeclaredField("mConfigurationController");
|
||||
configurationControllerField.setAccessible(true);
|
||||
configurationControllerField.set(ACTIVITY_THREAD, configurationController);
|
||||
} catch (Throwable throwable) {
|
||||
Ln.d("Could not fill configuration: " + throwable.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
static Context getSystemContext() {
|
||||
try {
|
||||
Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext");
|
||||
@ -256,16 +285,28 @@ public final class Workarounds {
|
||||
Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel");
|
||||
Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState);
|
||||
|
||||
// private native int native_setup(Object audiorecordThis,
|
||||
// Object /*AudioAttributes*/ attributes,
|
||||
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
|
||||
// int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource,
|
||||
// long nativeRecordInJavaObj, int maxSharedAudioHistoryMs);
|
||||
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class,
|
||||
int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class);
|
||||
nativeSetupMethod.setAccessible(true);
|
||||
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes, sampleRateArray,
|
||||
channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, attributionSourceParcel, 0L, 0);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
// private native int native_setup(Object audiorecordThis,
|
||||
// Object /*AudioAttributes*/ attributes,
|
||||
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
|
||||
// int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource,
|
||||
// long nativeRecordInJavaObj, int maxSharedAudioHistoryMs);
|
||||
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class,
|
||||
int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class);
|
||||
nativeSetupMethod.setAccessible(true);
|
||||
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes,
|
||||
sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session,
|
||||
attributionSourceParcel, 0L, 0);
|
||||
} else {
|
||||
// Android 14 added a new int parameter "halInputFlags"
|
||||
// <https://github.com/aosp-mirror/platform_frameworks_base/commit/f6135d75db79b1d48fad3a3b3080d37be20a2313>
|
||||
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class,
|
||||
int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class, int.class);
|
||||
nativeSetupMethod.setAccessible(true);
|
||||
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes,
|
||||
sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session,
|
||||
attributionSourceParcel, 0L, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,6 @@ import android.os.IBinder;
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||
@ -26,7 +25,20 @@ public final class ActivityManager {
|
||||
private Method startActivityAsUserWithFeatureMethod;
|
||||
private Method forceStopPackageMethod;
|
||||
|
||||
public ActivityManager(IInterface manager) {
|
||||
static ActivityManager create() {
|
||||
try {
|
||||
// On old Android versions, the ActivityManager is not exposed via AIDL,
|
||||
// so use ActivityManagerNative.getDefault()
|
||||
Class<?> cls = Class.forName("android.app.ActivityManagerNative");
|
||||
Method getDefaultMethod = cls.getDeclaredMethod("getDefault");
|
||||
IInterface am = (IInterface) getDefaultMethod.invoke(null);
|
||||
return new ActivityManager(am);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private ActivityManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
@ -76,7 +88,7 @@ public final class ActivityManager {
|
||||
return null;
|
||||
}
|
||||
return new ContentProvider(this, provider, name, token);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | NoSuchFieldException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
@ -86,7 +98,7 @@ public final class ActivityManager {
|
||||
try {
|
||||
Method method = getRemoveContentProviderExternalMethod();
|
||||
method.invoke(manager, name, token);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import android.content.IOnPrimaryClipChangedListener;
|
||||
import android.os.Build;
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class ClipboardManager {
|
||||
@ -20,7 +19,18 @@ public final class ClipboardManager {
|
||||
private int setMethodVersion;
|
||||
private int addListenerMethodVersion;
|
||||
|
||||
public ClipboardManager(IInterface manager) {
|
||||
static ClipboardManager create() {
|
||||
IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard");
|
||||
if (clipboard == 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);
|
||||
}
|
||||
|
||||
private ClipboardManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
@ -41,8 +51,21 @@ public final class ClipboardManager {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
|
||||
getMethodVersion = 2;
|
||||
} catch (NoSuchMethodException e3) {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
|
||||
getMethodVersion = 3;
|
||||
try {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
|
||||
getMethodVersion = 3;
|
||||
} catch (NoSuchMethodException e4) {
|
||||
try {
|
||||
getPrimaryClipMethod = manager.getClass()
|
||||
.getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class);
|
||||
getMethodVersion = 4;
|
||||
} catch (NoSuchMethodException e5) {
|
||||
getPrimaryClipMethod = manager.getClass()
|
||||
.getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class,
|
||||
boolean.class);
|
||||
getMethodVersion = 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -74,8 +97,7 @@ public final class ClipboardManager {
|
||||
return setPrimaryClipMethod;
|
||||
}
|
||||
|
||||
private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager)
|
||||
throws InvocationTargetException, IllegalAccessException {
|
||||
private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
|
||||
}
|
||||
@ -87,13 +109,17 @@ public final class ClipboardManager {
|
||||
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);
|
||||
default:
|
||||
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);
|
||||
default:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData)
|
||||
throws InvocationTargetException, IllegalAccessException {
|
||||
private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
|
||||
return;
|
||||
@ -120,7 +146,7 @@ public final class ClipboardManager {
|
||||
return null;
|
||||
}
|
||||
return clipData.getItemAt(0).getText();
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
@ -132,14 +158,14 @@ public final class ClipboardManager {
|
||||
ClipData clipData = ClipData.newPlainText(null, text);
|
||||
setPrimaryClip(method, setMethodVersion, manager, clipData);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager,
|
||||
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
|
||||
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener)
|
||||
throws ReflectiveOperationException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
|
||||
return;
|
||||
@ -191,7 +217,7 @@ public final class ClipboardManager {
|
||||
Method method = getAddPrimaryClipChangedListener();
|
||||
addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class ContentProvider implements Closeable {
|
||||
@ -42,8 +41,6 @@ public final class ContentProvider implements Closeable {
|
||||
private Method callMethod;
|
||||
private int callMethodVersion;
|
||||
|
||||
private Object attributionSource;
|
||||
|
||||
ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) {
|
||||
this.manager = manager;
|
||||
this.provider = provider;
|
||||
@ -77,8 +74,7 @@ public final class ContentProvider implements Closeable {
|
||||
return callMethod;
|
||||
}
|
||||
|
||||
private Bundle call(String callMethod, String arg, Bundle extras)
|
||||
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
|
||||
private Bundle call(String callMethod, String arg, Bundle extras) throws ReflectiveOperationException {
|
||||
try {
|
||||
Method method = getCallMethod();
|
||||
Object[] args;
|
||||
@ -99,7 +95,7 @@ public final class ContentProvider implements Closeable {
|
||||
}
|
||||
}
|
||||
return (Bundle) method.invoke(provider, args);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
throw e;
|
||||
}
|
||||
|
@ -0,0 +1,79 @@
|
||||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@SuppressLint({"PrivateApi", "SoonBlockedPrivateApi", "BlockedPrivateApi"})
|
||||
@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
public final class DisplayControl {
|
||||
|
||||
private static final Class<?> CLASS;
|
||||
|
||||
static {
|
||||
Class<?> displayControlClass = null;
|
||||
try {
|
||||
Class<?> classLoaderFactoryClass = Class.forName("com.android.internal.os.ClassLoaderFactory");
|
||||
Method createClassLoaderMethod = classLoaderFactoryClass.getDeclaredMethod("createClassLoader", String.class, String.class, String.class,
|
||||
ClassLoader.class, int.class, boolean.class, String.class);
|
||||
ClassLoader classLoader = (ClassLoader) createClassLoaderMethod.invoke(null, "/system/framework/services.jar", null, null,
|
||||
ClassLoader.getSystemClassLoader(), 0, true, null);
|
||||
|
||||
displayControlClass = classLoader.loadClass("com.android.server.display.DisplayControl");
|
||||
|
||||
Method loadMethod = Runtime.class.getDeclaredMethod("loadLibrary0", Class.class, String.class);
|
||||
loadMethod.setAccessible(true);
|
||||
loadMethod.invoke(Runtime.getRuntime(), displayControlClass, "android_servers");
|
||||
} catch (Throwable e) {
|
||||
Ln.e("Could not initialize DisplayControl", e);
|
||||
// Do not throw an exception here, the methods will fail when they are called
|
||||
}
|
||||
CLASS = displayControlClass;
|
||||
}
|
||||
|
||||
private static Method getPhysicalDisplayTokenMethod;
|
||||
private static Method getPhysicalDisplayIdsMethod;
|
||||
|
||||
private DisplayControl() {
|
||||
// only static methods
|
||||
}
|
||||
|
||||
private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException {
|
||||
if (getPhysicalDisplayTokenMethod == null) {
|
||||
getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class);
|
||||
}
|
||||
return getPhysicalDisplayTokenMethod;
|
||||
}
|
||||
|
||||
public static IBinder getPhysicalDisplayToken(long physicalDisplayId) {
|
||||
try {
|
||||
Method method = getGetPhysicalDisplayTokenMethod();
|
||||
return (IBinder) method.invoke(null, physicalDisplayId);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException {
|
||||
if (getPhysicalDisplayIdsMethod == null) {
|
||||
getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds");
|
||||
}
|
||||
return getPhysicalDisplayIdsMethod;
|
||||
}
|
||||
|
||||
public static long[] getPhysicalDisplayIds() {
|
||||
try {
|
||||
Method method = getGetPhysicalDisplayIdsMethod();
|
||||
return (long[]) method.invoke(null);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,16 +5,33 @@ import com.genymobile.scrcpy.DisplayInfo;
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
import com.genymobile.scrcpy.Size;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.view.Display;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||
public final class DisplayManager {
|
||||
private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
|
||||
private Method createVirtualDisplayMethod;
|
||||
|
||||
public DisplayManager(Object manager) {
|
||||
static DisplayManager create() {
|
||||
try {
|
||||
Class<?> clazz = Class.forName("android.hardware.display.DisplayManagerGlobal");
|
||||
Method getInstanceMethod = clazz.getDeclaredMethod("getInstance");
|
||||
Object dmg = getInstanceMethod.invoke(null);
|
||||
return new DisplayManager(dmg);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private DisplayManager(Object manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
@ -60,7 +77,7 @@ public final class DisplayManager {
|
||||
try {
|
||||
Field filed = Display.class.getDeclaredField(flagString);
|
||||
flags |= filed.getInt(null);
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
// Silently ignore, some flags reported by "dumpsys display" are @TestApi
|
||||
}
|
||||
}
|
||||
@ -82,7 +99,7 @@ public final class DisplayManager {
|
||||
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
|
||||
int flags = cls.getDeclaredField("flags").getInt(displayInfo);
|
||||
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
|
||||
} catch (Exception e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
@ -90,8 +107,21 @@ public final class DisplayManager {
|
||||
public int[] getDisplayIds() {
|
||||
try {
|
||||
return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager);
|
||||
} catch (Exception e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Method getCreateVirtualDisplayMethod() throws NoSuchMethodException {
|
||||
if (createVirtualDisplayMethod == null) {
|
||||
createVirtualDisplayMethod = android.hardware.display.DisplayManager.class
|
||||
.getMethod("createVirtualDisplay", String.class, int.class, int.class, int.class, Surface.class);
|
||||
}
|
||||
return createVirtualDisplayMethod;
|
||||
}
|
||||
|
||||
public VirtualDisplay createVirtualDisplay(String name, int width, int height, int displayIdToMirror, Surface surface) throws Exception {
|
||||
Method method = getCreateVirtualDisplayMethod();
|
||||
return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface);
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,13 @@ package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.view.InputEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||
public final class InputManager {
|
||||
|
||||
public static final int INJECT_INPUT_EVENT_MODE_ASYNC = 0;
|
||||
@ -20,7 +21,27 @@ public final class InputManager {
|
||||
private static Method setDisplayIdMethod;
|
||||
private static Method setActionButtonMethod;
|
||||
|
||||
public InputManager(Object manager) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
@ -35,7 +56,7 @@ public final class InputManager {
|
||||
try {
|
||||
Method method = getInjectInputEventMethod();
|
||||
return (boolean) method.invoke(manager, inputEvent, mode);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
@ -53,7 +74,7 @@ public final class InputManager {
|
||||
Method method = getSetDisplayIdMethod();
|
||||
method.invoke(inputEvent, displayId);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Cannot associate a display id to the input event", e);
|
||||
return false;
|
||||
}
|
||||
@ -71,7 +92,7 @@ public final class InputManager {
|
||||
Method method = getSetActionButtonMethod();
|
||||
method.invoke(motionEvent, actionButton);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Cannot set action button on MotionEvent", e);
|
||||
return false;
|
||||
}
|
||||
|
@ -6,21 +6,25 @@ import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class PowerManager {
|
||||
private final IInterface manager;
|
||||
private Method isScreenOnMethod;
|
||||
|
||||
public PowerManager(IInterface manager) {
|
||||
static PowerManager create() {
|
||||
IInterface manager = ServiceManager.getService("power", "android.os.IPowerManager");
|
||||
return new PowerManager(manager);
|
||||
}
|
||||
|
||||
private PowerManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
private Method getIsScreenOnMethod() throws NoSuchMethodException {
|
||||
if (isScreenOnMethod == null) {
|
||||
@SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future
|
||||
String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
|
||||
String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
|
||||
isScreenOnMethod = manager.getClass().getMethod(methodName);
|
||||
}
|
||||
return isScreenOnMethod;
|
||||
@ -30,7 +34,7 @@ public final class PowerManager {
|
||||
try {
|
||||
Method method = getIsScreenOnMethod();
|
||||
return (boolean) method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
|
@ -9,13 +9,13 @@ import android.os.IBinder;
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||
public final class ServiceManager {
|
||||
|
||||
private static final Method GET_SERVICE_METHOD;
|
||||
|
||||
static {
|
||||
try {
|
||||
GET_SERVICE_METHOD = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class);
|
||||
@ -37,7 +37,7 @@ public final class ServiceManager {
|
||||
/* not instantiable */
|
||||
}
|
||||
|
||||
private static IInterface getService(String service, String type) {
|
||||
static IInterface getService(String service, String type) {
|
||||
try {
|
||||
IBinder binder = (IBinder) GET_SERVICE_METHOD.invoke(null, service);
|
||||
Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class);
|
||||
@ -49,90 +49,51 @@ public final class ServiceManager {
|
||||
|
||||
public static WindowManager getWindowManager() {
|
||||
if (windowManager == null) {
|
||||
windowManager = new WindowManager(getService("window", "android.view.IWindowManager"));
|
||||
windowManager = WindowManager.create();
|
||||
}
|
||||
return windowManager;
|
||||
}
|
||||
|
||||
public static DisplayManager getDisplayManager() {
|
||||
if (displayManager == null) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName("android.hardware.display.DisplayManagerGlobal");
|
||||
Method getInstanceMethod = clazz.getDeclaredMethod("getInstance");
|
||||
Object dmg = getInstanceMethod.invoke(null);
|
||||
displayManager = new DisplayManager(dmg);
|
||||
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
displayManager = DisplayManager.create();
|
||||
}
|
||||
return displayManager;
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
|
||||
public static InputManager getInputManager() {
|
||||
if (inputManager == null) {
|
||||
try {
|
||||
Class<?> inputManagerClass = getInputManagerClass();
|
||||
Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance");
|
||||
Object im = getInstanceMethod.invoke(null);
|
||||
inputManager = new InputManager(im);
|
||||
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
inputManager = InputManager.create();
|
||||
}
|
||||
return inputManager;
|
||||
}
|
||||
|
||||
public static PowerManager getPowerManager() {
|
||||
if (powerManager == null) {
|
||||
powerManager = new PowerManager(getService("power", "android.os.IPowerManager"));
|
||||
powerManager = PowerManager.create();
|
||||
}
|
||||
return powerManager;
|
||||
}
|
||||
|
||||
public static StatusBarManager getStatusBarManager() {
|
||||
if (statusBarManager == null) {
|
||||
statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService"));
|
||||
statusBarManager = StatusBarManager.create();
|
||||
}
|
||||
return statusBarManager;
|
||||
}
|
||||
|
||||
public static ClipboardManager getClipboardManager() {
|
||||
if (clipboardManager == null) {
|
||||
IInterface clipboard = getService("clipboard", "android.content.IClipboard");
|
||||
if (clipboard == null) {
|
||||
// Some devices have no clipboard manager
|
||||
// <https://github.com/Genymobile/scrcpy/issues/1440>
|
||||
// <https://github.com/Genymobile/scrcpy/issues/1556>
|
||||
return null;
|
||||
}
|
||||
clipboardManager = new ClipboardManager(clipboard);
|
||||
// May be null, some devices have no clipboard manager
|
||||
clipboardManager = ClipboardManager.create();
|
||||
}
|
||||
return clipboardManager;
|
||||
}
|
||||
|
||||
public static ActivityManager getActivityManager() {
|
||||
if (activityManager == null) {
|
||||
try {
|
||||
// On old Android versions, the ActivityManager is not exposed via AIDL,
|
||||
// so use ActivityManagerNative.getDefault()
|
||||
Class<?> cls = Class.forName("android.app.ActivityManagerNative");
|
||||
Method getDefaultMethod = cls.getDeclaredMethod("getDefault");
|
||||
IInterface am = (IInterface) getDefaultMethod.invoke(null);
|
||||
activityManager = new ActivityManager(am);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
activityManager = ActivityManager.create();
|
||||
}
|
||||
|
||||
return activityManager;
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@ import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.os.IInterface;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class StatusBarManager {
|
||||
@ -16,7 +15,12 @@ public final class StatusBarManager {
|
||||
private boolean expandSettingsPanelMethodNewVersion = true;
|
||||
private Method collapsePanelsMethod;
|
||||
|
||||
public StatusBarManager(IInterface manager) {
|
||||
static StatusBarManager create() {
|
||||
IInterface manager = ServiceManager.getService("statusbar", "com.android.internal.statusbar.IStatusBarService");
|
||||
return new StatusBarManager(manager);
|
||||
}
|
||||
|
||||
private StatusBarManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
@ -62,7 +66,7 @@ public final class StatusBarManager {
|
||||
} else {
|
||||
method.invoke(manager);
|
||||
}
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
@ -77,7 +81,7 @@ public final class StatusBarManager {
|
||||
// old version
|
||||
method.invoke(manager);
|
||||
}
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
@ -86,7 +90,7 @@ public final class StatusBarManager {
|
||||
try {
|
||||
Method method = getCollapsePanelsMethod();
|
||||
method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
@ -78,12 +77,8 @@ public final class SurfaceControl {
|
||||
}
|
||||
}
|
||||
|
||||
public static IBinder createDisplay(String name, boolean secure) {
|
||||
try {
|
||||
return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
public static IBinder createDisplay(String name, boolean secure) throws Exception {
|
||||
return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure);
|
||||
}
|
||||
|
||||
private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException {
|
||||
@ -109,7 +104,7 @@ public final class SurfaceControl {
|
||||
|
||||
// call getInternalDisplayToken()
|
||||
return (IBinder) method.invoke(null);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
@ -126,7 +121,7 @@ public final class SurfaceControl {
|
||||
try {
|
||||
Method method = getGetPhysicalDisplayTokenMethod();
|
||||
return (IBinder) method.invoke(null, physicalDisplayId);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
@ -139,11 +134,20 @@ public final class SurfaceControl {
|
||||
return getPhysicalDisplayIdsMethod;
|
||||
}
|
||||
|
||||
public static boolean hasPhysicalDisplayIdsMethod() {
|
||||
try {
|
||||
getGetPhysicalDisplayIdsMethod();
|
||||
return true;
|
||||
} catch (NoSuchMethodException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static long[] getPhysicalDisplayIds() {
|
||||
try {
|
||||
Method method = getGetPhysicalDisplayIdsMethod();
|
||||
return (long[]) method.invoke(null);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
@ -161,7 +165,7 @@ public final class SurfaceControl {
|
||||
Method method = getSetDisplayPowerModeMethod();
|
||||
method.invoke(null, displayToken, mode);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
|
@ -4,10 +4,9 @@ import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.IInterface;
|
||||
import android.view.IRotationWatcher;
|
||||
import android.view.IDisplayFoldListener;
|
||||
import android.view.IRotationWatcher;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class WindowManager {
|
||||
@ -17,7 +16,12 @@ public final class WindowManager {
|
||||
private Method isRotationFrozenMethod;
|
||||
private Method thawRotationMethod;
|
||||
|
||||
public WindowManager(IInterface manager) {
|
||||
static WindowManager create() {
|
||||
IInterface manager = ServiceManager.getService("window", "android.view.IWindowManager");
|
||||
return new WindowManager(manager);
|
||||
}
|
||||
|
||||
private WindowManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
@ -61,7 +65,7 @@ public final class WindowManager {
|
||||
try {
|
||||
Method method = getGetRotationMethod();
|
||||
return (int) method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return 0;
|
||||
}
|
||||
@ -71,7 +75,7 @@ public final class WindowManager {
|
||||
try {
|
||||
Method method = getFreezeRotationMethod();
|
||||
method.invoke(manager, rotation);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
@ -80,7 +84,7 @@ public final class WindowManager {
|
||||
try {
|
||||
Method method = getIsRotationFrozenMethod();
|
||||
return (boolean) method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
@ -90,7 +94,7 @@ public final class WindowManager {
|
||||
try {
|
||||
Method method = getThawRotationMethod();
|
||||
method.invoke(manager);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user