Compare commits

..

4 Commits

Author SHA1 Message Date
Romain Vimont
e811b7710d Attach base context to application 2023-11-03 14:32:19 +01:00
Romain Vimont
3d93a099b7 Add log 2023-11-03 11:51:41 +01:00
Romain Vimont
29c305ca5f Initialize FakeContext first 2023-11-03 11:51:32 +01:00
Romain Vimont
65d6bdb237 Attempt to fix #4392
<https://github.com/Genymobile/scrcpy/issues/4392#issuecomment-1790954884>
2023-11-03 09:58:23 +01:00
69 changed files with 502 additions and 1316 deletions

View File

@@ -1,4 +1,4 @@
# scrcpy (v2.3.1) # scrcpy (v2.2)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" /> <img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />

View File

@@ -19,9 +19,8 @@ _scrcpy() {
--crop= --crop=
-d --select-usb -d --select-usb
--disable-screensaver --disable-screensaver
--display-buffer=
--display-id= --display-id=
--display-orientation= --display-buffer=
-e --select-tcpip -e --select-tcpip
-f --fullscreen -f --fullscreen
--force-adb-forward --force-adb-forward
@@ -51,7 +50,6 @@ _scrcpy() {
--no-power-on --no-power-on
--no-video --no-video
--no-video-playback --no-video-playback
--orientation=
--otg --otg
-p --port= -p --port=
--pause-on-exit --pause-on-exit
@@ -63,7 +61,6 @@ _scrcpy() {
-r --record= -r --record=
--raw-key-events --raw-key-events
--record-format= --record-format=
--record-orientation=
--render-driver= --render-driver=
--require-audio --require-audio
--rotation= --rotation=
@@ -100,7 +97,7 @@ _scrcpy() {
return return
;; ;;
--audio-codec) --audio-codec)
COMPREPLY=($(compgen -W 'opus aac flac raw' -- "$cur")) COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur"))
return return
;; ;;
--video-source) --video-source)
@@ -115,16 +112,8 @@ _scrcpy() {
COMPREPLY=($(compgen -W 'front back external' -- "$cur")) COMPREPLY=($(compgen -W 'front back external' -- "$cur"))
return 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) --lock-video-orientation)
COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur")) COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur"))
return return
;; ;;
--pause-on-exit) --pause-on-exit)
@@ -136,13 +125,17 @@ _scrcpy() {
return return
;; ;;
--record-format) --record-format)
COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac flac wav' -- "$cur")) COMPREPLY=($(compgen -W 'mkv mp4' -- "$cur"))
return return
;; ;;
--render-driver) --render-driver)
COMPREPLY=($(compgen -W 'direct3d opengl opengles2 opengles metal software' -- "$cur")) COMPREPLY=($(compgen -W 'direct3d opengl opengles2 opengles metal software' -- "$cur"))
return return
;; ;;
--rotation)
COMPREPLY=($(compgen -W '0 1 2 3' -- "$cur"))
return
;;
--shortcut-mod) --shortcut-mod)
# Only auto-complete a single key # Only auto-complete a single key
COMPREPLY=($(compgen -W 'lctrl rctrl lalt ralt lsuper rsuper' -- "$cur")) COMPREPLY=($(compgen -W 'lctrl rctrl lalt ralt lsuper rsuper' -- "$cur"))

View File

@@ -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 # 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 # startup file, like .bashrc or .zshrc… Run an interactive shell to get
# environment correctly initialized. # 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 Icon=scrcpy
Terminal=true Terminal=true
Type=Application Type=Application

View File

@@ -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 # 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 # startup file, like .bashrc or .zshrc… Run an interactive shell to get
# environment correctly initialized. # environment correctly initialized.
Exec=/bin/sh -c "\\$SHELL -i -c scrcpy" Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy"
Icon=scrcpy Icon=scrcpy
Terminal=false Terminal=false
Type=Application Type=Application

View File

@@ -11,7 +11,7 @@ arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--always-on-top[Make scrcpy window always on top \(above other windows\)]'
'--audio-bit-rate=[Encode the audio at the given bit-rate]' '--audio-bit-rate=[Encode the audio at the given bit-rate]'
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]' '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' '--audio-codec=[Select the audio codec]:codec:(opus aac raw)'
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
'--audio-encoder=[Use a specific MediaCodec audio encoder]' '--audio-encoder=[Use a specific MediaCodec audio encoder]'
'--audio-source=[Select the audio source]:source:(output mic)' '--audio-source=[Select the audio source]:source:(output mic)'
@@ -26,9 +26,8 @@ arguments=(
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
{-d,--select-usb}'[Use USB device]' {-d,--select-usb}'[Use USB device]'
'--disable-screensaver[Disable screensaver while scrcpy is running]' '--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-id=[Specify the display id to mirror]'
'--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' '--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]'
{-e,--select-tcpip}'[Use TCP/IP device]' {-e,--select-tcpip}'[Use TCP/IP device]'
{-f,--fullscreen}'[Start in fullscreen]' {-f,--fullscreen}'[Start in fullscreen]'
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
@@ -41,7 +40,7 @@ arguments=(
'--list-cameras[List cameras available on the device]' '--list-cameras[List cameras available on the device]'
'--list-displays[List displays available on the device]' '--list-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders 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 90 180 270)' '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)'
{-m,--max-size=}'[Limit both the width and height of the video to value]' {-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]' {-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]'
'--max-fps=[Limit the frame rate of screen capture]' '--max-fps=[Limit the frame rate of screen capture]'
@@ -57,7 +56,6 @@ arguments=(
'--no-power-on[Do not power on the device on start]' '--no-power-on[Do not power on the device on start]'
'--no-video[Disable video forwarding]' '--no-video[Disable video forwarding]'
'--no-video-playback[Disable video playback]' '--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\)]' '--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]' {-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)' '--pause-on-exit=[Make scrcpy pause before exiting]:mode:(true false if-error)'
@@ -67,10 +65,10 @@ arguments=(
'--push-target=[Set the target directory for pushing files to the device by drag and drop]' '--push-target=[Set the target directory for pushing files to the device by drag and drop]'
{-r,--record=}'[Record screen to file]:record file:_files' {-r,--record=}'[Record screen to file]:record file:_files'
'--raw-key-events[Inject key events for all input keys, and ignore text events]' '--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-format=[Force recording format]:format:(mp4 mkv)'
'--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)' '--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]' '--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,--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]' {-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)' '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'

View File

@@ -98,24 +98,77 @@ endif
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
dependencies = [ crossbuild_windows = meson.is_cross_build() and host_machine.system() == 'windows'
dependency('libavformat', version: '>= 57.33'),
dependency('libavcodec', version: '>= 57.37'),
dependency('libavutil'),
dependency('libswresample'),
dependency('sdl2', version: '>= 2.0.5'),
]
if v4l2_support if not crossbuild_windows
dependencies += dependency('libavdevice')
endif # 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 usb_support
dependencies += dependency('libusb-1.0')
endif endif
if host_machine.system() == 'windows' if host_machine.system() == 'windows'
dependencies += cc.find_library('mingw32')
dependencies += cc.find_library('ws2_32') dependencies += cc.find_library('ws2_32')
endif endif
@@ -236,10 +289,6 @@ if get_option('buildtype') == 'debug'
'tests/test_device_msg_deserialize.c', 'tests/test_device_msg_deserialize.c',
'src/device_msg.c', 'src/device_msg.c',
]], ]],
['test_orientation', [
'tests/test_orientation.c',
'src/options.c',
]],
['test_strbuf', [ ['test_strbuf', [
'tests/test_strbuf.c', 'tests/test_strbuf.c',
'src/util/strbuf.c', 'src/util/strbuf.c',

View File

@@ -6,11 +6,11 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR" mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR"
VERSION=6.1-scrcpy-3 VERSION=6.0-scrcpy-4
DEP_DIR="ffmpeg-$VERSION" DEP_DIR="ffmpeg-$VERSION"
FILENAME="$DEP_DIR".7z FILENAME="$DEP_DIR".7z
SHA256SUM=b646d18a3d543a4e4c46881568213499f22e4454a464e1552f03f2ac9cc3a05a SHA256SUM=39274b321491ce83e76cab5d24e7cbe3f402d3ccf382f739b13be5651c146b60
if [[ -d "$DEP_DIR" ]] if [[ -d "$DEP_DIR" ]]
then then

View File

@@ -6,10 +6,9 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR" mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR"
VERSION=1.0.26 DEP_DIR=libusb-1.0.26
DEP_DIR="libusb-$VERSION"
FILENAME="libusb-$VERSION-binaries.7z" FILENAME=libusb-1.0.26-binaries.7z
SHA256SUM=9c242696342dbde9cdc47239391f71833939bf9f7aa2bbb28cdaabe890465ec5 SHA256SUM=9c242696342dbde9cdc47239391f71833939bf9f7aa2bbb28cdaabe890465ec5
if [[ -d "$DEP_DIR" ]] if [[ -d "$DEP_DIR" ]]
@@ -18,22 +17,17 @@ then
exit 0 exit 0
fi fi
get_file "https://github.com/libusb/libusb/releases/download/v$VERSION/$FILENAME" \ get_file "https://github.com/libusb/libusb/releases/download/v1.0.26/$FILENAME" "$FILENAME" "$SHA256SUM"
"$FILENAME" "$SHA256SUM"
mkdir "$DEP_DIR" mkdir "$DEP_DIR"
cd "$DEP_DIR" cd "$DEP_DIR"
7z x "../$FILENAME" \ 7z x "../$FILENAME" \
"libusb-$VERSION-binaries/libusb-MinGW-Win32/" \ libusb-1.0.26-binaries/libusb-MinGW-Win32/bin/msys-usb-1.0.dll \
"libusb-$VERSION-binaries/libusb-MinGW-Win32/" \ libusb-1.0.26-binaries/libusb-MinGW-Win32/include/ \
"libusb-$VERSION-binaries/libusb-MinGW-x64/" \ libusb-1.0.26-binaries/libusb-MinGW-x64/bin/msys-usb-1.0.dll \
"libusb-$VERSION-binaries/libusb-MinGW-x64/" libusb-1.0.26-binaries/libusb-MinGW-x64/include/
mv "libusb-$VERSION-binaries/libusb-MinGW-Win32" . mv libusb-1.0.26-binaries/libusb-MinGW-Win32 .
mv "libusb-$VERSION-binaries/libusb-MinGW-x64" . mv libusb-1.0.26-binaries/libusb-MinGW-x64 .
rm -rf "libusb-$VERSION-binaries" rm -rf libusb-1.0.26-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

View File

@@ -6,11 +6,10 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR" mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR"
VERSION=2.28.5 DEP_DIR=SDL2-2.28.4
DEP_DIR="SDL2-$VERSION"
FILENAME="SDL2-devel-$VERSION-mingw.tar.gz" FILENAME=SDL2-devel-2.28.4-mingw.tar.gz
SHA256SUM=3c0c655c2ebf67cad48fead72761d1601740ded30808952c3274ba223d226c21 SHA256SUM=779d091072cf97291f80030f5232d97aa3d48ab0f2c14fe0b9d9a33c593cdc35
if [[ -d "$DEP_DIR" ]] if [[ -d "$DEP_DIR" ]]
then then
@@ -18,8 +17,7 @@ then
exit 0 exit 0
fi fi
get_file "https://github.com/libsdl-org/SDL/releases/download/release-$VERSION/$FILENAME" \ get_file "https://libsdl.org/release/$FILENAME" "$FILENAME" "$SHA256SUM"
"$FILENAME" "$SHA256SUM"
mkdir "$DEP_DIR" mkdir "$DEP_DIR"
cd "$DEP_DIR" cd "$DEP_DIR"

View File

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

View File

@@ -26,7 +26,7 @@ Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are s
Default is 128K (128000). Default is 128K (128000).
.TP .TP
.BI "\-\-audio\-buffer " ms .BI "\-\-audio\-buffer ms
Configure the audio buffering delay (in milliseconds). Configure the audio buffering delay (in milliseconds).
Lower values decrease the latency, but increase the likelyhood of buffer underrun (causing audio glitches). Lower values decrease the latency, but increase the likelyhood of buffer underrun (causing audio glitches).
@@ -35,7 +35,7 @@ Default is 50.
.TP .TP
.BI "\-\-audio\-codec " name .BI "\-\-audio\-codec " name
Select an audio codec (opus, aac, flac or raw). Select an audio codec (opus, aac or raw).
Default is opus. Default is opus.
@@ -62,7 +62,7 @@ Select the audio source (output or mic).
Default is output. Default is output.
.TP .TP
.BI "\-\-audio\-output\-buffer " ms .BI "\-\-audio\-output\-buffer ms
Configure the size of the SDL audio output buffer (in milliseconds). Configure the size of the SDL audio output buffer (in milliseconds).
If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise. If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise.
@@ -124,15 +124,9 @@ Use USB device (if there is exactly one, like adb -d).
Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
.TP .TP
.BI "\-\-disable\-screensaver" .BI "\-\-disable-screensaver"
Disable screensaver while scrcpy is running. Disable screensaver while scrcpy is running.
.TP
.BI "\-\-display\-buffer " ms
Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter.
Default is 0 (no buffering).
.TP .TP
.BI "\-\-display\-id " id .BI "\-\-display\-id " id
Specify the device display id to mirror. Specify the device display id to mirror.
@@ -142,12 +136,10 @@ The available display ids can be listed by \fB\-\-list\-displays\fR.
Default is 0. Default is 0.
.TP .TP
.BI "\-\-display\-orientation " value .BI "\-\-display\-buffer ms
Set the initial display orientation. Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter.
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 (no buffering).
Default is 0.
.TP .TP
.B \-e, \-\-select\-tcpip .B \-e, \-\-select\-tcpip
@@ -215,9 +207,7 @@ List displays available on the device.
.TP .TP
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR] \fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR]
Lock capture video orientation to \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.
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". Default is "unlocked".
@@ -299,10 +289,6 @@ Disable video forwarding.
.B \-\-no\-video\-playback .B \-\-no\-video\-playback
Disable video playback on the computer. Disable video playback on the computer.
.TP
.BI "\-\-orientation " value
Same as --display-orientation=value --record-orientation=value.
.TP .TP
.B \-\-otg .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. 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.
@@ -361,7 +347,7 @@ Record screen to
The format is determined by the The format is determined by the
.B \-\-record\-format .B \-\-record\-format
option if set, or by the file extension. option if set, or by the file extension (.mp4 or .mkv).
.TP .TP
.B \-\-raw\-key\-events .B \-\-raw\-key\-events
@@ -369,15 +355,7 @@ Inject key events for all input keys, and ignore text events.
.TP .TP
.BI "\-\-record\-format " format .BI "\-\-record\-format " format
Force recording format (mp4, mkv, m4a, mka, opus, aac, flac or wav). Force recording format (either mp4 or mkv).
.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 .TP
.BI "\-\-render\-driver " name .BI "\-\-render\-driver " name
@@ -391,6 +369,10 @@ Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "me
.B \-\-require\-audio .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. 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 .TP
.BI "\-s, \-\-serial " number .BI "\-s, \-\-serial " number
The device serial number. Mandatory only if several devices are connected to adb. The device serial number. Mandatory only if several devices are connected to adb.
@@ -552,14 +534,6 @@ Rotate display left
.B MOD+Right .B MOD+Right
Rotate display 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 .TP
.B MOD+g .B MOD+g
Resize window to 1:1 (pixel\-perfect) Resize window to 1:1 (pixel\-perfect)
@@ -642,11 +616,7 @@ Enable/disable FPS counter (print frames/second in logs)
.TP .TP
.B Ctrl+click-and-move .B Ctrl+click-and-move
Pinch-to-zoom and rotate from the center of the screen Pinch-to-zoom from the center of the screen
.TP
.B Shift+click-and-move
Tilt (slide vertically with two fingers)
.TP .TP
.B Drag & drop APK file .B Drag & drop APK file

View File

@@ -90,9 +90,6 @@ enum {
OPT_CAMERA_AR, OPT_CAMERA_AR,
OPT_CAMERA_FPS, OPT_CAMERA_FPS,
OPT_CAMERA_HIGH_SPEED, OPT_CAMERA_HIGH_SPEED,
OPT_DISPLAY_ORIENTATION,
OPT_RECORD_ORIENTATION,
OPT_ORIENTATION,
}; };
struct sc_option { struct sc_option {
@@ -155,7 +152,7 @@ static const struct sc_option options[] = {
.longopt_id = OPT_AUDIO_CODEC, .longopt_id = OPT_AUDIO_CODEC,
.longopt = "audio-codec", .longopt = "audio-codec",
.argdesc = "name", .argdesc = "name",
.text = "Select an audio codec (opus, aac, flac or raw).\n" .text = "Select an audio codec (opus, aac or raw).\n"
"Default is opus.", "Default is opus.",
}, },
{ {
@@ -295,14 +292,6 @@ static const struct sc_option options[] = {
.longopt = "display", .longopt = "display",
.argdesc = "id", .argdesc = "id",
}, },
{
.longopt_id = OPT_DISPLAY_BUFFER,
.longopt = "display-buffer",
.argdesc = "ms",
.text = "Add a buffering delay (in milliseconds) before displaying. "
"This increases latency to compensate for jitter.\n"
"Default is 0 (no buffering).",
},
{ {
.longopt_id = OPT_DISPLAY_ID, .longopt_id = OPT_DISPLAY_ID,
.longopt = "display-id", .longopt = "display-id",
@@ -313,15 +302,12 @@ static const struct sc_option options[] = {
"Default is 0.", "Default is 0.",
}, },
{ {
.longopt_id = OPT_DISPLAY_ORIENTATION, .longopt_id = OPT_DISPLAY_BUFFER,
.longopt = "display-orientation", .longopt = "display-buffer",
.argdesc = "value", .argdesc = "ms",
.text = "Set the initial display orientation.\n" .text = "Add a buffering delay (in milliseconds) before displaying. "
"Possible values are 0, 90, 180, 270, flip0, flip90, flip180 " "This increases latency to compensate for jitter.\n"
"and flip270. The number represents the clockwise rotation " "Default is 0 (no buffering).",
"in degrees; the \"flip\" keyword applies a horizontal flip "
"before the rotation.\n"
"Default is 0.",
}, },
{ {
.shortopt = 'e', .shortopt = 'e',
@@ -413,11 +399,11 @@ static const struct sc_option options[] = {
.longopt = "lock-video-orientation", .longopt = "lock-video-orientation",
.argdesc = "value", .argdesc = "value",
.optional_arg = true, .optional_arg = true,
.text = "Lock capture video orientation to value.\n" .text = "Lock video orientation to value.\n"
"Possible values are \"unlocked\", \"initial\" (locked to the " "Possible values are \"unlocked\", \"initial\" (locked to the "
"initial orientation), 0, 90, 180 and 270. The values " "initial orientation), 0, 1, 2 and 3. Natural device "
"represent the clockwise rotation from the natural device " "orientation is 0, and each increment adds a 90 degrees "
"orientation, in degrees.\n" "rotation counterclockwise.\n"
"Default is \"unlocked\".\n" "Default is \"unlocked\".\n"
"Passing the option without argument is equivalent to passing " "Passing the option without argument is equivalent to passing "
"\"initial\".", "\"initial\".",
@@ -526,13 +512,6 @@ static const struct sc_option options[] = {
.longopt = "no-video-playback", .longopt = "no-video-playback",
.text = "Disable video playback on the computer.", .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_id = OPT_OTG,
.longopt = "otg", .longopt = "otg",
@@ -604,7 +583,7 @@ static const struct sc_option options[] = {
.argdesc = "file.mp4", .argdesc = "file.mp4",
.text = "Record screen to file.\n" .text = "Record screen to file.\n"
"The format is determined by the --record-format option if " "The format is determined by the --record-format option if "
"set, or by the file extension.", "set, or by the file extension (.mp4 or .mkv).",
}, },
{ {
.longopt_id = OPT_RAW_KEY_EVENTS, .longopt_id = OPT_RAW_KEY_EVENTS,
@@ -615,17 +594,7 @@ static const struct sc_option options[] = {
.longopt_id = OPT_RECORD_FORMAT, .longopt_id = OPT_RECORD_FORMAT,
.longopt = "record-format", .longopt = "record-format",
.argdesc = "format", .argdesc = "format",
.text = "Force recording format (mp4, mkv, m4a, mka, opus, aac, flac " .text = "Force recording format (either mp4 or mkv).",
"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_id = OPT_RENDER_DRIVER,
@@ -645,10 +614,12 @@ static const struct sc_option options[] = {
"is enabled but does not work." "is enabled but does not work."
}, },
{ {
// deprecated
.longopt_id = OPT_ROTATION, .longopt_id = OPT_ROTATION,
.longopt = "rotation", .longopt = "rotation",
.argdesc = "value", .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', .shortopt = 's',
@@ -852,14 +823,6 @@ static const struct sc_shortcut shortcuts[] = {
.shortcuts = { "MOD+Right" }, .shortcuts = { "MOD+Right" },
.text = "Rotate display 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" }, .shortcuts = { "MOD+g" },
.text = "Resize window to 1:1 (pixel-perfect)", .text = "Resize window to 1:1 (pixel-perfect)",
@@ -947,11 +910,7 @@ static const struct sc_shortcut shortcuts[] = {
}, },
{ {
.shortcuts = { "Ctrl+click-and-move" }, .shortcuts = { "Ctrl+click-and-move" },
.text = "Pinch-to-zoom and rotate from the center of the screen", .text = "Pinch-to-zoom from the center of the screen",
},
{
.shortcuts = { "Shift+click-and-move" },
.text = "Tilt (slide vertically with two fingers)",
}, },
{ {
.shortcuts = { "Drag & drop APK file" }, .shortcuts = { "Drag & drop APK file" },
@@ -1422,50 +1381,15 @@ parse_lock_video_orientation(const char *s,
return true; return true;
} }
if (!strcmp(s, "0")) { long value;
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_0; bool ok = parse_integer_arg(s, &value, false, 0, 3,
return true; "lock video orientation");
if (!ok) {
return false;
} }
if (!strcmp(s, "90")) { *lock_mode = (enum sc_lock_video_orientation) value;
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_90; return true;
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 static bool
@@ -1480,45 +1404,6 @@ parse_rotation(const char *s, uint8_t *rotation) {
return true; 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 static bool
parse_window_position(const char *s, int16_t *position) { parse_window_position(const char *s, int16_t *position) {
// special value for "auto" // special value for "auto"
@@ -1741,12 +1626,6 @@ get_record_format(const char *name) {
if (!strcmp(name, "aac")) { if (!strcmp(name, "aac")) {
return SC_RECORD_FORMAT_AAC; return SC_RECORD_FORMAT_AAC;
} }
if (!strcmp(name, "flac")) {
return SC_RECORD_FORMAT_FLAC;
}
if (!strcmp(name, "wav")) {
return SC_RECORD_FORMAT_WAV;
}
return 0; return 0;
} }
@@ -1754,8 +1633,7 @@ static bool
parse_record_format(const char *optarg, enum sc_record_format *format) { parse_record_format(const char *optarg, enum sc_record_format *format) {
enum sc_record_format fmt = get_record_format(optarg); enum sc_record_format fmt = get_record_format(optarg);
if (!fmt) { if (!fmt) {
LOGE("Unsupported record format: %s (expected mp4, mkv, m4a, mka, " LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg);
"opus, aac, flac or wav)", optarg);
return false; return false;
} }
@@ -1817,16 +1695,11 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) {
*codec = SC_CODEC_AAC; *codec = SC_CODEC_AAC;
return true; return true;
} }
if (!strcmp(optarg, "flac")) {
*codec = SC_CODEC_FLAC;
return true;
}
if (!strcmp(optarg, "raw")) { if (!strcmp(optarg, "raw")) {
*codec = SC_CODEC_RAW; *codec = SC_CODEC_RAW;
return true; return true;
} }
LOGE("Unsupported audio codec: %s (expected opus, aac, flac or raw)", LOGE("Unsupported audio codec: %s (expected opus, aac or raw)", optarg);
optarg);
return false; return false;
} }
@@ -2122,51 +1995,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW; opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW;
break; break;
case OPT_ROTATION: case OPT_ROTATION:
LOGW("--rotation is deprecated, use --display-orientation " if (!parse_rotation(optarg, &opts->rotation)) {
"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; return false;
} }
break; 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: case OPT_RENDER_DRIVER:
opts->render_driver = optarg; opts->render_driver = optarg;
break; break;
@@ -2425,19 +2257,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->require_audio = true; opts->require_audio = true;
} }
if (opts->audio_playback && opts->audio_buffer == -1) {
if (opts->audio_codec == SC_CODEC_FLAC) {
// Use 50 ms audio buffer by default, but use a higher value for FLAC,
// which is not low latency (the default encoder produces blocks of
// 4096 samples, which represent ~85.333ms).
LOGI("FLAC audio: audio buffer increased to 120 ms (use "
"--audio-buffer to set a custom value)");
opts->audio_buffer = SC_TICK_FROM_MS(120);
} else {
opts->audio_buffer = SC_TICK_FROM_MS(50);
}
}
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
if (v4l2) { if (v4l2) {
if (opts->lock_video_orientation == if (opts->lock_video_orientation ==
@@ -2533,13 +2352,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
} }
} }
if (opts->record_orientation != SC_ORIENTATION_0) { if (opts->audio_codec == SC_CODEC_RAW) {
if (sc_orientation_is_mirror(opts->record_orientation)) { LOGW("Recording does not support RAW audio codec");
LOGE("Record orientation only supports rotation, not " return false;
"flipping: %s",
sc_orientation_get_name(opts->record_orientation));
return false;
}
} }
if (opts->video if (opts->video
@@ -2561,30 +2376,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
"(try with --audio-codec=aac)"); "(try with --audio-codec=aac)");
return false; return false;
} }
if (opts->record_format == SC_RECORD_FORMAT_FLAC
&& opts->audio_codec != SC_CODEC_FLAC) {
LOGE("Recording to FLAC file requires a FLAC audio stream "
"(try with --audio-codec=flac)");
return false;
}
if (opts->record_format == SC_RECORD_FORMAT_WAV
&& opts->audio_codec != SC_CODEC_RAW) {
LOGE("Recording to WAV file requires a RAW audio stream "
"(try with --audio-codec=raw)");
return false;
}
if ((opts->record_format == SC_RECORD_FORMAT_MP4 ||
opts->record_format == SC_RECORD_FORMAT_M4A)
&& opts->audio_codec == SC_CODEC_RAW) {
LOGE("Recording to MP4 container does not support RAW audio");
return false;
}
}
if (opts->audio_codec == SC_CODEC_FLAC && opts->audio_bit_rate) {
LOGW("--audio-bit-rate is ignored for FLAC audio codec");
} }
if (opts->audio_codec == SC_CODEC_RAW) { if (opts->audio_codec == SC_CODEC_RAW) {

View File

@@ -3,9 +3,7 @@
#include "config.h" #include "config.h"
#include <libavcodec/version.h>
#include <libavformat/version.h> #include <libavformat/version.h>
#include <libavutil/version.h>
#include <SDL2/SDL_version.h> #include <SDL2/SDL_version.h>
#ifndef __WIN32 #ifndef __WIN32
@@ -52,15 +50,6 @@
# define SCRCPY_LAVU_HAS_CHLAYOUT # define SCRCPY_LAVU_HAS_CHLAYOUT
#endif #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) #if SDL_VERSION_ATLEAST(2, 0, 6)
// <https://github.com/libsdl-org/SDL/commit/d7a318de563125e5bb465b1000d6bc9576fbc6fc> // <https://github.com/libsdl-org/SDL/commit/d7a318de563125e5bb465b1000d6bc9576fbc6fc>
# define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS # define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS

View File

@@ -25,8 +25,7 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII #define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII #define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII
#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII #define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII
#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac" in ASCII #define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII"
#define SC_CODEC_ID_FLAC UINT32_C(0x666c6163) // "flac" in ASCII
#define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII #define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII
switch (codec_id) { switch (codec_id) {
case SC_CODEC_ID_H264: case SC_CODEC_ID_H264:
@@ -44,8 +43,6 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
return AV_CODEC_ID_OPUS; return AV_CODEC_ID_OPUS;
case SC_CODEC_ID_AAC: case SC_CODEC_ID_AAC:
return AV_CODEC_ID_AAC; return AV_CODEC_ID_AAC;
case SC_CODEC_ID_FLAC:
return AV_CODEC_ID_FLAC;
case SC_CODEC_ID_RAW: case SC_CODEC_ID_RAW:
return AV_CODEC_ID_PCM_S16LE; return AV_CODEC_ID_PCM_S16LE;
default: default:
@@ -210,11 +207,6 @@ run_demuxer(void *data) {
codec_ctx->channels = 2; codec_ctx->channels = 2;
#endif #endif
codec_ctx->sample_rate = 48000; codec_ctx->sample_rate = 48000;
if (raw_codec_id == SC_CODEC_ID_FLAC) {
// The sample_fmt is not set by the FLAC decoder
codec_ctx->sample_fmt = AV_SAMPLE_FMT_S16;
}
} }
if (avcodec_open2(codec_ctx, codec, NULL) < 0) { if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
@@ -227,9 +219,8 @@ run_demuxer(void *data) {
} }
// Config packets must be merged with the next non-config packet only for // Config packets must be merged with the next non-config packet only for
// H.26x // video streams
bool must_merge_config_packet = raw_codec_id == SC_CODEC_ID_H264 bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO;
|| raw_codec_id == SC_CODEC_ID_H265;
struct sc_packet_merger merger; struct sc_packet_merger merger;

View File

@@ -234,7 +234,7 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame) {
enum sc_display_result enum sc_display_result
sc_display_render(struct sc_display *display, const SDL_Rect *geometry, sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
enum sc_orientation orientation) { unsigned rotation) {
SDL_RenderClear(display->renderer); SDL_RenderClear(display->renderer);
if (display->pending.flags) { 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_Renderer *renderer = display->renderer;
SDL_Texture *texture = display->texture; SDL_Texture *texture = display->texture;
if (orientation == SC_ORIENTATION_0) { if (rotation == 0) {
int ret = SDL_RenderCopy(renderer, texture, NULL, geometry); int ret = SDL_RenderCopy(renderer, texture, NULL, geometry);
if (ret) { if (ret) {
LOGE("Could not render texture: %s", SDL_GetError()); LOGE("Could not render texture: %s", SDL_GetError());
return SC_DISPLAY_RESULT_ERROR; return SC_DISPLAY_RESULT_ERROR;
} }
} else { } else {
unsigned cw_rotation = sc_orientation_get_rotation(orientation); // rotation in RenderCopyEx() is clockwise, while screen->rotation is
// counterclockwise (to be consistent with --lock-video-orientation)
int cw_rotation = (4 - rotation) % 4;
double angle = 90 * cw_rotation; double angle = 90 * cw_rotation;
const SDL_Rect *dstrect = NULL; const SDL_Rect *dstrect = NULL;
SDL_Rect rect; SDL_Rect rect;
if (sc_orientation_is_swap(orientation)) { if (rotation & 1) {
rect.x = geometry->x + (geometry->w - geometry->h) / 2; rect.x = geometry->x + (geometry->w - geometry->h) / 2;
rect.y = geometry->y + (geometry->h - geometry->w) / 2; rect.y = geometry->y + (geometry->h - geometry->w) / 2;
rect.w = geometry->h; rect.w = geometry->h;
rect.h = geometry->w; rect.h = geometry->w;
dstrect = &rect; dstrect = &rect;
} else { } else {
assert(rotation == 2);
dstrect = geometry; dstrect = geometry;
} }
SDL_RendererFlip flip = sc_orientation_is_mirror(orientation)
? SDL_FLIP_HORIZONTAL : 0;
int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle, int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle,
NULL, flip); NULL, 0);
if (ret) { if (ret) {
LOGE("Could not render texture: %s", SDL_GetError()); LOGE("Could not render texture: %s", SDL_GetError());
return SC_DISPLAY_RESULT_ERROR; return SC_DISPLAY_RESULT_ERROR;

View File

@@ -9,7 +9,6 @@
#include "coords.h" #include "coords.h"
#include "opengl.h" #include "opengl.h"
#include "options.h"
#ifdef __APPLE__ #ifdef __APPLE__
# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE # define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
@@ -55,6 +54,6 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame);
enum sc_display_result enum sc_display_result
sc_display_render(struct sc_display *display, const SDL_Rect *geometry, sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
enum sc_orientation orientation); unsigned rotation);
#endif #endif

View File

@@ -76,8 +76,6 @@ sc_input_manager_init(struct sc_input_manager *im,
im->sdl_shortcut_mods.count = shortcut_mods->count; im->sdl_shortcut_mods.count = shortcut_mods->count;
im->vfinger_down = false; im->vfinger_down = false;
im->vfinger_invert_x = false;
im->vfinger_invert_y = false;
im->last_keycode = SDLK_UNKNOWN; im->last_keycode = SDLK_UNKNOWN;
im->last_mod = 0; im->last_mod = 0;
@@ -295,11 +293,15 @@ rotate_device(struct sc_controller *controller) {
} }
static void static void
apply_orientation_transform(struct sc_screen *screen, rotate_client_left(struct sc_screen *screen) {
enum sc_orientation transform) { unsigned new_rotation = (screen->rotation + 1) % 4;
enum sc_orientation new_orientation = sc_screen_set_rotation(screen, new_rotation);
sc_orientation_apply(screen->orientation, transform); }
sc_screen_set_orientation(screen, new_orientation);
static void
rotate_client_right(struct sc_screen *screen) {
unsigned new_rotation = (screen->rotation + 3) % 4;
sc_screen_set_rotation(screen, new_rotation);
} }
static void static void
@@ -349,14 +351,9 @@ simulate_virtual_finger(struct sc_input_manager *im,
} }
static struct sc_point static struct sc_point
inverse_point(struct sc_point point, struct sc_size size, inverse_point(struct sc_point point, struct sc_size size) {
bool invert_x, bool invert_y) { point.x = size.width - point.x;
if (invert_x) { point.y = size.height - point.y;
point.x = size.width - point.x;
}
if (invert_y) {
point.y = size.height - point.y;
}
return point; return point;
} }
@@ -424,47 +421,25 @@ sc_input_manager_process_key(struct sc_input_manager *im,
} }
return; return;
case SDLK_DOWN: case SDLK_DOWN:
if (shift) { if (controller && !shift) {
if (!repeat & down) {
apply_orientation_transform(im->screen,
SC_ORIENTATION_FLIP_180);
}
} else if (controller) {
// forward repeated events // forward repeated events
action_volume_down(controller, action); action_volume_down(controller, action);
} }
return; return;
case SDLK_UP: case SDLK_UP:
if (shift) { if (controller && !shift) {
if (!repeat & down) {
apply_orientation_transform(im->screen,
SC_ORIENTATION_FLIP_180);
}
} else if (controller) {
// forward repeated events // forward repeated events
action_volume_up(controller, action); action_volume_up(controller, action);
} }
return; return;
case SDLK_LEFT: case SDLK_LEFT:
if (!repeat && down) { if (!shift && !repeat && down) {
if (shift) { rotate_client_left(im->screen);
apply_orientation_transform(im->screen,
SC_ORIENTATION_FLIP_0);
} else {
apply_orientation_transform(im->screen,
SC_ORIENTATION_270);
}
} }
return; return;
case SDLK_RIGHT: case SDLK_RIGHT:
if (!repeat && down) { if (!shift && !repeat && down) {
if (shift) { rotate_client_right(im->screen);
apply_orientation_transform(im->screen,
SC_ORIENTATION_FLIP_0);
} else {
apply_orientation_transform(im->screen,
SC_ORIENTATION_90);
}
} }
return; return;
case SDLK_c: case SDLK_c:
@@ -612,9 +587,7 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
struct sc_point mouse = struct sc_point mouse =
sc_screen_convert_window_to_frame_coords(im->screen, event->x, sc_screen_convert_window_to_frame_coords(im->screen, event->x,
event->y); 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); simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger);
} }
} }
@@ -735,7 +708,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
return; return;
} }
// Pinch-to-zoom, rotate and tilt simulation. // Pinch-to-zoom simulation.
// //
// If Ctrl is hold when the left-click button is pressed, then // 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 // pinch-to-zoom mode is enabled: on every mouse event until the left-click
@@ -744,29 +717,14 @@ 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 // In other words, the center of the rotation/scaling is the center of the
// screen. // 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 && if (event->button == SDL_BUTTON_LEFT &&
((down && !im->vfinger_down && ((down && !im->vfinger_down && CTRL_PRESSED) ||
((ctrl_pressed && !shift_pressed) ||
(!ctrl_pressed && shift_pressed))) ||
(!down && im->vfinger_down))) { (!down && im->vfinger_down))) {
struct sc_point mouse = struct sc_point mouse =
sc_screen_convert_window_to_frame_coords(im->screen, event->x, sc_screen_convert_window_to_frame_coords(im->screen, event->x,
event->y); event->y);
if (down) { struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size);
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 enum android_motionevent_action action = down
? AMOTION_EVENT_ACTION_DOWN ? AMOTION_EVENT_ACTION_DOWN
: AMOTION_EVENT_ACTION_UP; : AMOTION_EVENT_ACTION_UP;

View File

@@ -32,8 +32,6 @@ struct sc_input_manager {
} sdl_shortcut_mods; } sdl_shortcut_mods;
bool vfinger_down; bool vfinger_down;
bool vfinger_invert_x;
bool vfinger_invert_y;
// Tracks the number of identical consecutive shortcut key down events. // Tracks the number of identical consecutive shortcut key down events.
// Not to be confused with event->repeat, which counts the number of // Not to be confused with event->repeat, which counts the number of

View File

@@ -39,15 +39,14 @@ const struct scrcpy_options scrcpy_options_default = {
.audio_bit_rate = 0, .audio_bit_rate = 0,
.max_fps = 0, .max_fps = 0,
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
.display_orientation = SC_ORIENTATION_0, .rotation = 0,
.record_orientation = SC_ORIENTATION_0,
.window_x = SC_WINDOW_POSITION_UNDEFINED, .window_x = SC_WINDOW_POSITION_UNDEFINED,
.window_y = SC_WINDOW_POSITION_UNDEFINED, .window_y = SC_WINDOW_POSITION_UNDEFINED,
.window_width = 0, .window_width = 0,
.window_height = 0, .window_height = 0,
.display_id = 0, .display_id = 0,
.display_buffer = 0, .display_buffer = 0,
.audio_buffer = -1, // depends on the audio format, .audio_buffer = SC_TICK_FROM_MS(50),
.audio_output_buffer = SC_TICK_FROM_MS(5), .audio_output_buffer = SC_TICK_FROM_MS(5),
.time_limit = 0, .time_limit = 0,
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
@@ -90,39 +89,3 @@ const struct scrcpy_options scrcpy_options_default = {
.camera_high_speed = false, .camera_high_speed = false,
.list = 0, .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;
}

View File

@@ -3,7 +3,6 @@
#include "common.h" #include "common.h"
#include <assert.h>
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
@@ -26,8 +25,6 @@ enum sc_record_format {
SC_RECORD_FORMAT_MKA, SC_RECORD_FORMAT_MKA,
SC_RECORD_FORMAT_OPUS, SC_RECORD_FORMAT_OPUS,
SC_RECORD_FORMAT_AAC, SC_RECORD_FORMAT_AAC,
SC_RECORD_FORMAT_FLAC,
SC_RECORD_FORMAT_WAV,
}; };
static inline bool static inline bool
@@ -35,9 +32,7 @@ sc_record_format_is_audio_only(enum sc_record_format fmt) {
return fmt == SC_RECORD_FORMAT_M4A return fmt == SC_RECORD_FORMAT_M4A
|| fmt == SC_RECORD_FORMAT_MKA || fmt == SC_RECORD_FORMAT_MKA
|| fmt == SC_RECORD_FORMAT_OPUS || fmt == SC_RECORD_FORMAT_OPUS
|| fmt == SC_RECORD_FORMAT_AAC || fmt == SC_RECORD_FORMAT_AAC;
|| fmt == SC_RECORD_FORMAT_FLAC
|| fmt == SC_RECORD_FORMAT_WAV;
} }
enum sc_codec { enum sc_codec {
@@ -46,7 +41,6 @@ enum sc_codec {
SC_CODEC_AV1, SC_CODEC_AV1,
SC_CODEC_OPUS, SC_CODEC_OPUS,
SC_CODEC_AAC, SC_CODEC_AAC,
SC_CODEC_FLAC,
SC_CODEC_RAW, SC_CODEC_RAW,
}; };
@@ -68,75 +62,14 @@ enum sc_camera_facing {
SC_CAMERA_FACING_EXTERNAL, 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 { enum sc_lock_video_orientation {
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
// lock the current orientation when scrcpy starts // lock the current orientation when scrcpy starts
SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2,
SC_LOCK_VIDEO_ORIENTATION_0 = 0, SC_LOCK_VIDEO_ORIENTATION_0 = 0,
SC_LOCK_VIDEO_ORIENTATION_90 = 3, SC_LOCK_VIDEO_ORIENTATION_1,
SC_LOCK_VIDEO_ORIENTATION_180 = 2, SC_LOCK_VIDEO_ORIENTATION_2,
SC_LOCK_VIDEO_ORIENTATION_270 = 1, SC_LOCK_VIDEO_ORIENTATION_3,
}; };
enum sc_keyboard_input_mode { enum sc_keyboard_input_mode {
@@ -219,8 +152,7 @@ struct scrcpy_options {
uint32_t audio_bit_rate; uint32_t audio_bit_rate;
uint16_t max_fps; uint16_t max_fps;
enum sc_lock_video_orientation lock_video_orientation; enum sc_lock_video_orientation lock_video_orientation;
enum sc_orientation display_orientation; uint8_t rotation;
enum sc_orientation record_orientation;
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
uint16_t window_width; uint16_t window_width;

View File

@@ -4,7 +4,6 @@
#include <libavcodec/avcodec.h> #include <libavcodec/avcodec.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include <libavutil/time.h> #include <libavutil/time.h>
#include <libavutil/display.h>
#include "util/log.h" #include "util/log.h"
#include "util/str.h" #include "util/str.h"
@@ -70,10 +69,6 @@ sc_recorder_get_format_name(enum sc_record_format format) {
return "matroska"; return "matroska";
case SC_RECORD_FORMAT_OPUS: case SC_RECORD_FORMAT_OPUS:
return "opus"; return "opus";
case SC_RECORD_FORMAT_FLAC:
return "flac";
case SC_RECORD_FORMAT_WAV:
return "wav";
default: default:
return NULL; return NULL;
} }
@@ -106,7 +101,7 @@ sc_recorder_write_stream(struct sc_recorder *recorder,
AVStream *stream = recorder->ctx->streams[st->index]; AVStream *stream = recorder->ctx->streams[st->index];
sc_recorder_rescale_packet(stream, packet); sc_recorder_rescale_packet(stream, packet);
if (st->last_pts != AV_NOPTS_VALUE && packet->pts <= st->last_pts) { if (st->last_pts != AV_NOPTS_VALUE && packet->pts <= st->last_pts) {
LOGD("Fixing PTS non monotonically increasing in stream %d " LOGW("Fixing PTS non monotonically increasing in stream %d "
"(%" PRIi64 " >= %" PRIi64 ")", "(%" PRIi64 " >= %" PRIi64 ")",
st->index, st->last_pts, packet->pts); st->index, st->last_pts, packet->pts);
packet->pts = ++st->last_pts; packet->pts = ++st->last_pts;
@@ -171,14 +166,13 @@ sc_recorder_close_output_file(struct sc_recorder *recorder) {
} }
static inline bool static inline bool
sc_recorder_must_wait_for_config_packets(struct sc_recorder *recorder) { sc_recorder_has_empty_queues(struct sc_recorder *recorder) {
if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) { if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) {
// The video queue is empty // The video queue is empty
return true; return true;
} }
if (recorder->audio && recorder->audio_expects_config_packet if (recorder->audio && sc_vecdeque_is_empty(&recorder->audio_queue)) {
&& sc_vecdeque_is_empty(&recorder->audio_queue)) {
// The audio queue is empty (when audio is enabled) // The audio queue is empty (when audio is enabled)
return true; return true;
} }
@@ -194,7 +188,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
while (!recorder->stopped && while (!recorder->stopped &&
((recorder->video && !recorder->video_init) ((recorder->video && !recorder->video_init)
|| (recorder->audio && !recorder->audio_init) || (recorder->audio && !recorder->audio_init)
|| sc_recorder_must_wait_for_config_packets(recorder))) { || sc_recorder_has_empty_queues(recorder))) {
sc_cond_wait(&recorder->cond, &recorder->mutex); sc_cond_wait(&recorder->cond, &recorder->mutex);
} }
@@ -213,8 +207,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
} }
AVPacket *audio_pkt = NULL; AVPacket *audio_pkt = NULL;
if (recorder->audio_expects_config_packet && if (!sc_vecdeque_is_empty(&recorder->audio_queue)) {
!sc_vecdeque_is_empty(&recorder->audio_queue)) {
assert(recorder->audio); assert(recorder->audio);
audio_pkt = sc_vecdeque_pop(&recorder->audio_queue); audio_pkt = sc_vecdeque_pop(&recorder->audio_queue);
} }
@@ -494,42 +487,6 @@ run_recorder(void *data) {
return 0; 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 static bool
sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
AVCodecContext *ctx) { AVCodecContext *ctx) {
@@ -557,16 +514,6 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
recorder->video_stream.index = stream->index; 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; recorder->video_init = true;
sc_cond_signal(&recorder->cond); sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex); sc_mutex_unlock(&recorder->mutex);
@@ -648,10 +595,6 @@ sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
recorder->audio_stream.index = stream->index; recorder->audio_stream.index = stream->index;
// A config packet is provided for all supported formats except raw audio
recorder->audio_expects_config_packet =
ctx->codec_id != AV_CODEC_ID_PCM_S16LE;
recorder->audio_init = true; recorder->audio_init = true;
sc_cond_signal(&recorder->cond); sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex); sc_mutex_unlock(&recorder->mutex);
@@ -736,10 +679,7 @@ sc_recorder_stream_init(struct sc_recorder_stream *stream) {
bool bool
sc_recorder_init(struct sc_recorder *recorder, const char *filename, sc_recorder_init(struct sc_recorder *recorder, const char *filename,
enum sc_record_format format, bool video, bool audio, enum sc_record_format format, bool video, bool audio,
enum sc_orientation orientation,
const struct sc_recorder_callbacks *cbs, void *cbs_userdata) { const struct sc_recorder_callbacks *cbs, void *cbs_userdata) {
assert(!sc_orientation_is_mirror(orientation));
recorder->filename = strdup(filename); recorder->filename = strdup(filename);
if (!recorder->filename) { if (!recorder->filename) {
LOG_OOM(); LOG_OOM();
@@ -760,8 +700,6 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
recorder->video = video; recorder->video = video;
recorder->audio = audio; recorder->audio = audio;
recorder->orientation = orientation;
sc_vecdeque_init(&recorder->video_queue); sc_vecdeque_init(&recorder->video_queue);
sc_vecdeque_init(&recorder->audio_queue); sc_vecdeque_init(&recorder->audio_queue);
recorder->stopped = false; recorder->stopped = false;
@@ -769,8 +707,6 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
recorder->video_init = false; recorder->video_init = false;
recorder->audio_init = false; recorder->audio_init = false;
recorder->audio_expects_config_packet = false;
sc_recorder_stream_init(&recorder->video_stream); sc_recorder_stream_init(&recorder->video_stream);
sc_recorder_stream_init(&recorder->audio_stream); sc_recorder_stream_init(&recorder->audio_stream);

View File

@@ -34,8 +34,6 @@ struct sc_recorder {
bool audio; bool audio;
bool video; bool video;
enum sc_orientation orientation;
char *filename; char *filename;
enum sc_record_format format; enum sc_record_format format;
AVFormatContext *ctx; AVFormatContext *ctx;
@@ -52,8 +50,6 @@ struct sc_recorder {
bool video_init; bool video_init;
bool audio_init; bool audio_init;
bool audio_expects_config_packet;
struct sc_recorder_stream video_stream; struct sc_recorder_stream video_stream;
struct sc_recorder_stream audio_stream; struct sc_recorder_stream audio_stream;
@@ -69,7 +65,6 @@ struct sc_recorder_callbacks {
bool bool
sc_recorder_init(struct sc_recorder *recorder, const char *filename, sc_recorder_init(struct sc_recorder *recorder, const char *filename,
enum sc_record_format format, bool video, bool audio, enum sc_record_format format, bool video, bool audio,
enum sc_orientation orientation,
const struct sc_recorder_callbacks *cbs, void *cbs_userdata); const struct sc_recorder_callbacks *cbs, void *cbs_userdata);
bool bool

View File

@@ -417,22 +417,9 @@ scrcpy(struct scrcpy_options *options) {
if (options->video_playback) { if (options->video_playback) {
sdl_set_hints(options->render_driver); sdl_set_hints(options->render_driver);
}
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 (SDL_Init(SDL_INIT_VIDEO)) {
// If it fails, it is an error only if video playback is enabled LOGE("Could not initialize SDL video: %s", SDL_GetError());
if (options->video_playback) { goto end;
LOGE("Could not initialize SDL video: %s", SDL_GetError());
goto end;
} else {
LOGW("Could not initialize SDL video: %s", SDL_GetError());
}
} }
} }
@@ -516,8 +503,7 @@ scrcpy(struct scrcpy_options *options) {
}; };
if (!sc_recorder_init(&s->recorder, options->record_filename, if (!sc_recorder_init(&s->recorder, options->record_filename,
options->record_format, options->video, options->record_format, options->video,
options->audio, options->record_orientation, options->audio, &recorder_cbs, NULL)) {
&recorder_cbs, NULL)) {
goto end; goto end;
} }
recorder_initialized = true; recorder_initialized = true;
@@ -701,7 +687,7 @@ aoa_hid_end:
.window_width = options->window_width, .window_width = options->window_width,
.window_height = options->window_height, .window_height = options->window_height,
.window_borderless = options->window_borderless, .window_borderless = options->window_borderless,
.orientation = options->display_orientation, .rotation = options->rotation,
.mipmaps = options->mipmaps, .mipmaps = options->mipmaps,
.fullscreen = options->fullscreen, .fullscreen = options->fullscreen,
.start_fps_counter = options->start_fps_counter, .start_fps_counter = options->start_fps_counter,

View File

@@ -14,16 +14,16 @@
#define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink) #define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink)
static inline struct sc_size static inline struct sc_size
get_oriented_size(struct sc_size size, enum sc_orientation orientation) { get_rotated_size(struct sc_size size, int rotation) {
struct sc_size oriented_size; struct sc_size rotated_size;
if (sc_orientation_is_swap(orientation)) { if (rotation & 1) {
oriented_size.width = size.height; rotated_size.width = size.height;
oriented_size.height = size.width; rotated_size.height = size.width;
} else { } else {
oriented_size.width = size.width; rotated_size.width = size.width;
oriented_size.height = size.height; rotated_size.height = size.height;
} }
return oriented_size; return rotated_size;
} }
// get the window size in a struct sc_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 = enum sc_display_result res =
sc_display_render(&screen->display, &screen->rect, screen->orientation); sc_display_render(&screen->display, &screen->rect, screen->rotation);
(void) res; // any error already logged (void) res; // any error already logged
} }
@@ -379,10 +379,9 @@ sc_screen_init(struct sc_screen *screen,
goto error_destroy_frame_buffer; goto error_destroy_frame_buffer;
} }
screen->orientation = params->orientation; screen->rotation = params->rotation;
if (screen->orientation != SC_ORIENTATION_0) { if (screen->rotation) {
LOGI("Initial display orientation set to %s", LOGI("Initial display rotation set to %u", screen->rotation);
sc_orientation_get_name(screen->orientation));
} }
uint32_t window_flags = SDL_WINDOW_HIDDEN uint32_t window_flags = SDL_WINDOW_HIDDEN
@@ -560,19 +559,19 @@ apply_pending_resize(struct sc_screen *screen) {
} }
void void
sc_screen_set_orientation(struct sc_screen *screen, sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation) {
enum sc_orientation orientation) { assert(rotation < 4);
if (orientation == screen->orientation) { if (rotation == screen->rotation) {
return; return;
} }
struct sc_size new_content_size = struct sc_size new_content_size =
get_oriented_size(screen->frame_size, orientation); get_rotated_size(screen->frame_size, rotation);
set_content_size(screen, new_content_size); set_content_size(screen, new_content_size);
screen->orientation = orientation; screen->rotation = rotation;
LOGI("Display orientation set to %s", sc_orientation_get_name(orientation)); LOGI("Display rotation set to %u", rotation);
sc_screen_render(screen, true); sc_screen_render(screen, true);
} }
@@ -585,7 +584,7 @@ sc_screen_init_size(struct sc_screen *screen) {
// The requested size is passed via screen->frame_size // The requested size is passed via screen->frame_size
struct sc_size content_size = struct sc_size content_size =
get_oriented_size(screen->frame_size, screen->orientation); get_rotated_size(screen->frame_size, screen->rotation);
screen->content_size = content_size; screen->content_size = content_size;
enum sc_display_result res = enum sc_display_result res =
@@ -605,7 +604,7 @@ prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
screen->frame_size = new_frame_size; screen->frame_size = new_frame_size;
struct sc_size new_content_size = struct sc_size new_content_size =
get_oriented_size(new_frame_size, screen->orientation); get_rotated_size(new_frame_size, screen->rotation);
set_content_size(screen, new_content_size); set_content_size(screen, new_content_size);
sc_screen_update_content_rect(screen); sc_screen_update_content_rect(screen);
@@ -844,7 +843,8 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
struct sc_point struct sc_point
sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen,
int32_t x, int32_t y) { int32_t x, int32_t y) {
enum sc_orientation orientation = screen->orientation; unsigned rotation = screen->rotation;
assert(rotation < 4);
int32_t w = screen->content_size.width; int32_t w = screen->content_size.width;
int32_t h = screen->content_size.height; int32_t h = screen->content_size.height;
@@ -855,43 +855,27 @@ sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen,
x = (int64_t) (x - screen->rect.x) * w / screen->rect.w; x = (int64_t) (x - screen->rect.x) * w / screen->rect.w;
y = (int64_t) (y - screen->rect.y) * h / screen->rect.h; y = (int64_t) (y - screen->rect.y) * h / screen->rect.h;
// rotate
struct sc_point result; struct sc_point result;
switch (orientation) { switch (rotation) {
case SC_ORIENTATION_0: case 0:
result.x = x; result.x = x;
result.y = y; result.y = y;
break; break;
case SC_ORIENTATION_90: case 1:
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.x = h - y;
result.y = x; result.y = x;
break; break;
case SC_ORIENTATION_FLIP_0: case 2:
result.x = w - x; 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; result.y = h - y;
break; break;
default: default:
assert(orientation == SC_ORIENTATION_FLIP_270); assert(rotation == 3);
result.x = y; result.x = y;
result.y = x; result.y = w - x;
break; break;
} }
return result; return result;
} }

View File

@@ -14,7 +14,6 @@
#include "frame_buffer.h" #include "frame_buffer.h"
#include "input_manager.h" #include "input_manager.h"
#include "opengl.h" #include "opengl.h"
#include "options.h"
#include "trait/key_processor.h" #include "trait/key_processor.h"
#include "trait/frame_sink.h" #include "trait/frame_sink.h"
#include "trait/mouse_processor.h" #include "trait/mouse_processor.h"
@@ -50,8 +49,8 @@ struct sc_screen {
// fullscreen (meaningful only when resize_pending is true) // fullscreen (meaningful only when resize_pending is true)
struct sc_size windowed_content_size; struct sc_size windowed_content_size;
// client orientation // client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise)
enum sc_orientation orientation; unsigned rotation;
// rectangle of the content (excluding black borders) // rectangle of the content (excluding black borders)
struct SDL_Rect rect; struct SDL_Rect rect;
bool has_frame; bool has_frame;
@@ -87,7 +86,7 @@ struct sc_screen_params {
bool window_borderless; bool window_borderless;
enum sc_orientation orientation; uint8_t rotation;
bool mipmaps; bool mipmaps;
bool fullscreen; bool fullscreen;
@@ -130,10 +129,9 @@ sc_screen_resize_to_fit(struct sc_screen *screen);
void void
sc_screen_resize_to_pixel_perfect(struct sc_screen *screen); sc_screen_resize_to_pixel_perfect(struct sc_screen *screen);
// set the display orientation // set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise)
void void
sc_screen_set_orientation(struct sc_screen *screen, sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation);
enum sc_orientation orientation);
// react to SDL events // react to SDL events
// If this function returns false, scrcpy must exit with an error. // If this function returns false, scrcpy must exit with an error.

View File

@@ -178,8 +178,6 @@ sc_server_get_codec_name(enum sc_codec codec) {
return "opus"; return "opus";
case SC_CODEC_AAC: case SC_CODEC_AAC:
return "aac"; return "aac";
case SC_CODEC_FLAC:
return "flac";
case SC_CODEC_RAW: case SC_CODEC_RAW:
return "raw"; return "raw";
default: default:

View File

@@ -1,91 +0,0 @@
#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;
}

View File

@@ -7,7 +7,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.1.3' classpath 'com.android.tools.build:gradle:7.4.0'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
@@ -23,3 +23,7 @@ allprojects {
options.compilerArgs << "-Xlint:deprecation" options.compilerArgs << "-Xlint:deprecation"
} }
} }
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -2,7 +2,7 @@ apply plugin: 'checkstyle'
check.dependsOn 'checkstyle' check.dependsOn 'checkstyle'
checkstyle { checkstyle {
toolVersion = '10.12.5' toolVersion = '9.0.1'
} }
task checkstyle(type: Checkstyle) { task checkstyle(type: Checkstyle) {

View File

@@ -6,7 +6,7 @@ c = 'i686-w64-mingw32-gcc'
cpp = 'i686-w64-mingw32-g++' cpp = 'i686-w64-mingw32-g++'
ar = 'i686-w64-mingw32-ar' ar = 'i686-w64-mingw32-ar'
strip = 'i686-w64-mingw32-strip' strip = 'i686-w64-mingw32-strip'
pkg-config = 'i686-w64-mingw32-pkg-config' pkgconfig = 'i686-w64-mingw32-pkg-config'
windres = 'i686-w64-mingw32-windres' windres = 'i686-w64-mingw32-windres'
[host_machine] [host_machine]
@@ -14,3 +14,8 @@ system = 'windows'
cpu_family = 'x86' cpu_family = 'x86'
cpu = 'i686' cpu = 'i686'
endian = 'little' endian = 'little'
[properties]
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win32'
prebuilt_sdl2 = 'SDL2-2.28.4/i686-w64-mingw32'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'

View File

@@ -6,7 +6,7 @@ c = 'x86_64-w64-mingw32-gcc'
cpp = 'x86_64-w64-mingw32-g++' cpp = 'x86_64-w64-mingw32-g++'
ar = 'x86_64-w64-mingw32-ar' ar = 'x86_64-w64-mingw32-ar'
strip = 'x86_64-w64-mingw32-strip' strip = 'x86_64-w64-mingw32-strip'
pkg-config = 'x86_64-w64-mingw32-pkg-config' pkgconfig = 'x86_64-w64-mingw32-pkg-config'
windres = 'x86_64-w64-mingw32-windres' windres = 'x86_64-w64-mingw32-windres'
[host_machine] [host_machine]
@@ -14,3 +14,8 @@ system = 'windows'
cpu_family = 'x86' cpu_family = 'x86'
cpu = 'x86_64' cpu = 'x86_64'
endian = 'little' endian = 'little'
[properties]
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win64'
prebuilt_sdl2 = 'SDL2-2.28.4/x86_64-w64-mingw32'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'

View File

@@ -62,13 +62,12 @@ scrcpy --audio-source=mic --no-video --no-playback --record=file.opus
## Codec ## Codec
The audio codec can be selected. The possible values are `opus` (default), The audio codec can be selected. The possible values are `opus` (default), `aac`
`aac`, `flac` and `raw` (uncompressed PCM 16-bit LE): and `raw` (uncompressed PCM 16-bit LE):
```bash ```bash
scrcpy --audio-codec=opus # default scrcpy --audio-codec=opus # default
scrcpy --audio-codec=aac scrcpy --audio-codec=aac
scrcpy --audio-codec=flac
scrcpy --audio-codec=raw scrcpy --audio-codec=raw
``` ```
@@ -81,14 +80,7 @@ then your device has no Opus encoder: try `scrcpy --audio-codec=aac`.
For advanced usage, to pass arbitrary parameters to the [`MediaFormat`], For advanced usage, to pass arbitrary parameters to the [`MediaFormat`],
check `--audio-codec-options` in the manpage or in `scrcpy --help`. check `--audio-codec-options` in the manpage or in `scrcpy --help`.
For example, to change the [FLAC compression level]:
```bash
scrcpy --audio-codec=flac --audio-codec-options=flac-compression-level=8
```
[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat [`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat
[FLAC compression level]: https://developer.android.com/reference/android/media/MediaFormat#KEY_FLAC_COMPRESSION_LEVEL
## Encoder ## Encoder

View File

@@ -58,7 +58,7 @@ sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \
libswresample-dev libusb-1.0-0-dev libswresample-dev libusb-1.0-0-dev
# server build dependencies # server build dependencies
sudo apt install openjdk-17-jdk sudo apt install openjdk-11-jdk
``` ```
On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install
@@ -100,7 +100,7 @@ sudo apt install mingw-w64 mingw-w64-tools
You also need the JDK to build the server: You also need the JDK to build the server:
```bash ```bash
sudo apt install openjdk-17-jdk sudo apt install openjdk-11-jdk
``` ```
Then generate the releases: Then generate the releases:
@@ -168,13 +168,13 @@ brew install sdl2 ffmpeg libusb
brew install pkg-config meson brew install pkg-config meson
``` ```
Additionally, if you want to build the server, install Java 17 from Caskroom, and Additionally, if you want to build the server, install Java 8 from Caskroom, and
make it available from the `PATH`: make it available from the `PATH`:
```bash ```bash
brew tap homebrew/cask-versions brew tap homebrew/cask-versions
brew install adoptopenjdk/openjdk/adoptopenjdk17 brew install adoptopenjdk/openjdk/adoptopenjdk11
export JAVA_HOME="$(/usr/libexec/java_home --version 1.17)" export JAVA_HOME="$(/usr/libexec/java_home --version 1.11)"
export PATH="$JAVA_HOME/bin:$PATH" export PATH="$JAVA_HOME/bin:$PATH"
``` ```
@@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server #### Option 2: Use prebuilt server
- [`scrcpy-server-v2.3.1`][direct-scrcpy-server] - [`scrcpy-server-v2.2`][direct-scrcpy-server]
<sub>SHA-256: `f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b`</sub> <sub>SHA-256: `c85c4aa84305efb69115cd497a120ebdd10258993b4cf123a8245b3d99d49874`</sub>
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1 [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-server-v2.2
Download the prebuilt server somewhere, and specify its path during the Meson Download the prebuilt server somewhere, and specify its path during the Meson
configuration: configuration:

View File

@@ -18,17 +18,6 @@ scrcpy --video-source=display --audio-source=mic # force display AND micropho
scrcpy --video-source=camera --audio-source=output # force camera AND device audio output 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 ## List
@@ -112,16 +101,6 @@ 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 ## Frame rate
By default, camera is captured at Android's default frame rate (30 fps). By default, camera is captured at Android's default frame rate (30 fps).

View File

@@ -85,7 +85,7 @@ way as <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>).
To disable automatic clipboard synchronization, use To disable automatic clipboard synchronization, use
`--no-clipboard-autosync`. `--no-clipboard-autosync`.
## Pinch-to-zoom, rotate and tilt simulation ## Pinch-to-zoom
To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_. To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_.
@@ -93,12 +93,8 @@ 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 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. 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" Technically, _scrcpy_ generates additional touch events from a "virtual finger"
at a location inverted through the center of the screen. When pressing at a location inverted through the center of the screen.
<kbd>Ctrl</kbd> the x and y coordinates are inverted. Using <kbd>Shift</kbd>
only inverts x.
## Key repeat ## Key repeat

View File

@@ -18,9 +18,7 @@ To record only the audio:
```bash ```bash
scrcpy --no-video --record=file.opus scrcpy --no-video --record=file.opus
scrcpy --no-video --audio-codec=aac --record=file.aac scrcpy --no-video --audio-codec=aac --record=file.aac
scrcpy --no-video --audio-codec=flac --record=file.flac # .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac
scrcpy --no-video --audio-codec=raw --record=file.wav
# .m4a/.mp4 and .mka/.mkv are also supported for opus, aac and flac
``` ```
Timestamps are captured on the device, so [packet delay variation] does not Timestamps are captured on the device, so [packet delay variation] does not
@@ -33,29 +31,20 @@ course, not if you capture your scrcpy window and audio output on the computer).
## Format ## Format
The video and audio streams are encoded on the device, but are muxed on the The video and audio streams are encoded on the device, but are muxed on the
client side. Several formats (containers) are supported: client side. Two formats (containers) are supported:
- MP4 (`.mp4`, `.m4a`, `.aac`) - Matroska (`.mkv`)
- Matroska (`.mkv`, `.mka`) - MP4 (`.mp4`)
- OPUS (`.opus`)
- FLAC (`.flac`)
- WAV (`.wav`)
The container is automatically selected based on the filename. The container is automatically selected based on the filename.
It is also possible to explicitly select a container (in that case the filename It is also possible to explicitly select a container (in that case the filename
needs not end with a known extension): needs not end with `.mkv` or `.mp4`):
``` ```
scrcpy --record=file --record-format=mkv scrcpy --record=file --record-format=mkv
``` ```
## Rotation
The video can be recorded rotated. See [video
orientation](video.md#orientation).
## No playback ## No playback
To disable playback while recording: To disable playback while recording:

View File

@@ -26,8 +26,6 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Switch fullscreen mode | <kbd>MOD</kbd>+<kbd>f</kbd> | Switch fullscreen mode | <kbd>MOD</kbd>+<kbd>f</kbd>
| Rotate display left | <kbd>MOD</kbd>+<kbd></kbd> _(left)_ | Rotate display left | <kbd>MOD</kbd>+<kbd></kbd> _(left)_
| Rotate display right | <kbd>MOD</kbd>+<kbd></kbd> _(right)_ | 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 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¹_ | 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_ | Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
@@ -49,8 +47,7 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Synchronize clipboards and paste⁵ | <kbd>MOD</kbd>+<kbd>v</kbd> | Synchronize clipboards and paste⁵ | <kbd>MOD</kbd>+<kbd>v</kbd>
| Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</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> | Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
| Pinch-to-zoom/rotate | <kbd>Ctrl</kbd>+_click-and-move_ | Pinch-to-zoom | <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 APK file | Install APK from computer
| Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device) | Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device)

View File

@@ -21,13 +21,6 @@ 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 (more [options](https://github.com/umlaeute/v4l2loopback#options) are available
to create several devices or devices with specific IDs). 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: To list the enabled devices:
```bash ```bash

View File

@@ -97,50 +97,39 @@ scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc'
``` ```
## Orientation ## Rotation
The orientation may be applied at 3 different levels: The rotation may be applied at 3 different levels:
- The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the - The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the
device to switch between portrait and landscape (the current running app may device to switch between portrait and landscape (the current running app may
refuse, if it does not support the requested orientation). refuse, if it does not support the requested orientation).
- `--lock-video-orientation` changes the mirroring orientation (the orientation - `--lock-video-orientation` changes the mirroring orientation (the orientation
of the video sent from the device to the computer). This affects the of the video sent from the device to the computer). This affects the
recording. recording.
- `--orientation` is applied on the client side, and affects display and - `--rotation` rotates only the window content. This only affects the display,
recording. For the display, it can be changed dynamically using not the recording. It may be changed dynamically at any time using the
[shortcuts](shortcuts.md). [shortcuts](shortcuts.md) <kbd>MOD</kbd>+<kbd></kbd> and
<kbd>MOD</kbd>+<kbd></kbd>.
To lock the mirroring orientation (on the capture side): To lock the mirroring orientation:
```bash ```bash
scrcpy --lock-video-orientation # initial (current) orientation scrcpy --lock-video-orientation # initial (current) orientation
scrcpy --lock-video-orientation=0 # natural orientation scrcpy --lock-video-orientation=0 # natural orientation
scrcpy --lock-video-orientation=90 # 90° clockwise scrcpy --lock-video-orientation=1 # 90° counterclockwise
scrcpy --lock-video-orientation=180 # 180° scrcpy --lock-video-orientation=2 # 180°
scrcpy --lock-video-orientation=270 # 270° clockwise scrcpy --lock-video-orientation=3 # 90° clockwise
``` ```
To orient the video (on the rendering side): To set an initial window rotation:
```bash ```bash
scrcpy --orientation=0 scrcpy --rotation=0 # no rotation
scrcpy --orientation=90 # 90° clockwise scrcpy --rotation=1 # 90 degrees counterclockwise
scrcpy --orientation=180 # 180° scrcpy --rotation=2 # 180 degrees
scrcpy --orientation=270 # 270° clockwise scrcpy --rotation=3 # 90 degrees 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 ## Crop
The device screen may be cropped to mirror only part of the screen. The device screen may be cropped to mirror only part of the screen.

View File

@@ -4,14 +4,14 @@
Download the [latest release]: Download the [latest release]:
- [`scrcpy-win64-v2.3.1.zip`][direct-win64] (64-bit) - [`scrcpy-win64-v2.2.zip`][direct-win64] (64-bit)
<sub>SHA-256: `f1f78ac98214078425804e524a1bed515b9d4b8a05b78d210a4ced2b910b262d`</sub> <sub>SHA-256: `9f9da88ac4c8319dcb9bf852f2d9bba942bac663413383419cddf64eaa5685bd`</sub>
- [`scrcpy-win32-v2.3.1.zip`][direct-win32] (32-bit) - [`scrcpy-win32-v2.2.zip`][direct-win32] (32-bit)
<sub>SHA-256: `5dffc2d432e9b8b5b0e16f12e71428c37c70d9124cfbe7620df0b41b7efe91ff`</sub> <sub>SHA-256: `cb84269fc847b8b880e320879492a1ae6c017b42175f03e199530f7a53be9d74`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest [latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win64-v2.3.1.zip [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.3.1/scrcpy-win32-v2.3.1.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-win32-v2.2.zip
and extract it. and extract it.

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

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

View File

@@ -1,5 +1,5 @@
project('scrcpy', 'c', project('scrcpy', 'c',
version: '2.3.1', version: 'v2.2',
meson_version: '>= 0.48', meson_version: '>= 0.48',
default_options: [ default_options: [
'c_std=c11', 'c_std=c11',

View File

@@ -69,62 +69,58 @@ prepare-deps:
@app/prebuilt-deps/prepare-libusb.sh @app/prebuilt-deps/prepare-libusb.sh
build-win32: prepare-deps build-win32: prepare-deps
rm -rf "$(WIN32_BUILD_DIR)" [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \
mkdir -p "$(WIN32_BUILD_DIR)/local" meson setup "$(WIN32_BUILD_DIR)" \
cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win32/. "$(WIN32_BUILD_DIR)/local/" --cross-file cross_win32.txt \
cp -r app/prebuilt-deps/data/SDL2-2.28.5/i686-w64-mingw32/. "$(WIN32_BUILD_DIR)/local/" --buildtype release --strip -Db_lto=true \
cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/. "$(WIN32_BUILD_DIR)/local/" -Dcompile_server=false \
meson setup "$(WIN32_BUILD_DIR)" \ -Dportable=true )
--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)" ninja -C "$(WIN32_BUILD_DIR)"
build-win64: prepare-deps build-win64: prepare-deps
rm -rf "$(WIN64_BUILD_DIR)" [ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \
mkdir -p "$(WIN64_BUILD_DIR)/local" meson setup "$(WIN64_BUILD_DIR)" \
cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win64/. "$(WIN64_BUILD_DIR)/local/" --cross-file cross_win64.txt \
cp -r app/prebuilt-deps/data/SDL2-2.28.5/x86_64-w64-mingw32/. "$(WIN64_BUILD_DIR)/local/" --buildtype release --strip -Db_lto=true \
cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/. "$(WIN64_BUILD_DIR)/local/" -Dcompile_server=false \
meson setup "$(WIN64_BUILD_DIR)" \ -Dportable=true )
--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)" ninja -C "$(WIN64_BUILD_DIR)"
dist-win32: build-server build-win32 dist-win32: build-server build-win32
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(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 "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/scrcpy-console.bat "$(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/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)"
cp app/data/icon.png "$(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/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/swresample-4.dll "$(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/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/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/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/local/bin/*.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)/"
dist-win64: build-server build-win64 dist-win64: build-server build-win64
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(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 "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/scrcpy-console.bat "$(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/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/data/icon.png "$(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/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/swresample-4.dll "$(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/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/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/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_BUILD_DIR)"/local/bin/*.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)/"
zip-win32: dist-win32 zip-win32: dist-win32
cd "$(DIST)"; \ cd "$(DIST)"; \

View File

@@ -2,13 +2,13 @@ apply plugin: 'com.android.application'
android { android {
namespace 'com.genymobile.scrcpy' namespace 'com.genymobile.scrcpy'
compileSdk 34 compileSdkVersion 33
defaultConfig { defaultConfig {
applicationId "com.genymobile.scrcpy" applicationId "com.genymobile.scrcpy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 34 targetSdkVersion 33
versionCode 20301 versionCode 200
versionName "2.3.1" versionName "v2.2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
@@ -17,10 +17,6 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
buildFeatures {
buildConfig true
aidl true
}
} }
dependencies { dependencies {

View File

@@ -12,10 +12,10 @@
set -e set -e
SCRCPY_DEBUG=false SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=2.3.1 SCRCPY_VERSION_NAME=v2.2
PLATFORM=${ANDROID_PLATFORM:-34} PLATFORM=${ANDROID_PLATFORM:-33}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0}
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"

View File

@@ -11,8 +11,6 @@ public interface AsyncProcessor {
} }
void start(TerminationListener listener); void start(TerminationListener listener);
void stop(); void stop();
void join() throws InterruptedException; void join() throws InterruptedException;
} }

View File

@@ -24,19 +24,11 @@ public final class AudioCapture {
public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
public static final int BYTES_PER_SAMPLE = 2; public static final int BYTES_PER_SAMPLE = 2;
// Never read more than 1024 samples, even if the buffer is bigger (that would increase latency).
// A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we
// receive 4 successive blocks without waiting, then we wait for the 4 next ones).
public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE;
private static final long ONE_SAMPLE_US = (1000000 + SAMPLE_RATE - 1) / SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS)
private final int audioSource; private final int audioSource;
private AudioRecord recorder; private AudioRecord recorder;
private final AudioTimestamp timestamp = new AudioTimestamp(); private final AudioTimestamp timestamp = new AudioTimestamp();
private long previousRecorderTimestamp = -1;
private long previousPts = 0; private long previousPts = 0;
private long nextPts = 0; private long nextPts = 0;
@@ -44,6 +36,10 @@ public final class AudioCapture {
this.audioSource = audioSource.value(); this.audioSource = audioSource.value();
} }
public static int millisToBytes(int millis) {
return SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * millis / 1000;
}
private static AudioFormat createAudioFormat() { private static AudioFormat createAudioFormat() {
AudioFormat.Builder builder = new AudioFormat.Builder(); AudioFormat.Builder builder = new AudioFormat.Builder();
builder.setEncoding(ENCODING); builder.setEncoding(ENCODING);
@@ -139,8 +135,8 @@ public final class AudioCapture {
} }
@TargetApi(Build.VERSION_CODES.N) @TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer directBuffer, MediaCodec.BufferInfo outBufferInfo) { public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) {
int r = recorder.read(directBuffer, MAX_READ_SIZE); int r = recorder.read(directBuffer, size);
if (r <= 0) { if (r <= 0) {
return r; return r;
} }
@@ -148,28 +144,26 @@ public final class AudioCapture {
long pts; long pts;
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC); int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) { if (ret == AudioRecord.SUCCESS) {
pts = timestamp.nanoTime / 1000; pts = timestamp.nanoTime / 1000;
previousRecorderTimestamp = timestamp.nanoTime;
} else { } else {
if (nextPts == 0) { if (nextPts == 0) {
Ln.w("Could not get initial audio timestamp"); Ln.w("Could not get any audio timestamp");
nextPts = System.nanoTime() / 1000;
} }
// compute from previous timestamp and packet size // compute from previous timestamp and packet size
pts = nextPts; pts = nextPts;
} }
long durationUs = r * 1000000L / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE); long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE);
nextPts = pts + durationUs; nextPts = pts + durationUs;
if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) { if (previousPts != 0 && pts < previousPts) {
// Audio PTS may come from two sources: // Audio PTS may come from two sources:
// - recorder.getTimestamp() if the call works; // - recorder.getTimestamp() if the call works;
// - an estimation from the previous PTS and the packet size as a fallback. // - an estimation from the previous PTS and the packet size as a fallback.
// //
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it. // Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
pts = previousPts + ONE_SAMPLE_US; pts = previousPts + 1;
} }
previousPts = pts; previousPts = pts;

View File

@@ -5,7 +5,6 @@ import android.media.MediaFormat;
public enum AudioCodec implements Codec { public enum AudioCodec implements Codec {
OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS), OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS),
AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC), AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC),
FLAC(0x66_6c_61_63, "flac", MediaFormat.MIMETYPE_AUDIO_FLAC),
RAW(0x00_72_61_77, "raw", MediaFormat.MIMETYPE_AUDIO_RAW); RAW(0x00_72_61_77, "raw", MediaFormat.MIMETYPE_AUDIO_RAW);
private final int id; // 4-byte ASCII representation of the name private final int id; // 4-byte ASCII representation of the name

View File

@@ -37,6 +37,9 @@ public final class AudioEncoder implements AsyncProcessor {
private static final int SAMPLE_RATE = AudioCapture.SAMPLE_RATE; private static final int SAMPLE_RATE = AudioCapture.SAMPLE_RATE;
private static final int CHANNELS = AudioCapture.CHANNELS; private static final int CHANNELS = AudioCapture.CHANNELS;
private static final int READ_MS = 5; // milliseconds
private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS);
private final AudioCapture capture; private final AudioCapture capture;
private final Streamer streamer; private final Streamer streamer;
private final int bitRate; private final int bitRate;
@@ -90,7 +93,7 @@ public final class AudioEncoder implements AsyncProcessor {
while (!Thread.currentThread().isInterrupted()) { while (!Thread.currentThread().isInterrupted()) {
InputTask task = inputTasks.take(); InputTask task = inputTasks.take();
ByteBuffer buffer = mediaCodec.getInputBuffer(task.index); ByteBuffer buffer = mediaCodec.getInputBuffer(task.index);
int r = capture.read(buffer, bufferInfo); int r = capture.read(buffer, READ_SIZE, bufferInfo);
if (r <= 0) { if (r <= 0) {
throw new IOException("Could not read audio: " + r); throw new IOException("Could not read audio: " + r);
} }
@@ -295,7 +298,7 @@ public final class AudioEncoder implements AsyncProcessor {
} }
} }
private final class EncoderCallback extends MediaCodec.Callback { private class EncoderCallback extends MediaCodec.Callback {
@TargetApi(Build.VERSION_CODES.N) @TargetApi(Build.VERSION_CODES.N)
@Override @Override
public void onInputBufferAvailable(MediaCodec codec, int index) { public void onInputBufferAvailable(MediaCodec codec, int index) {

View File

@@ -13,6 +13,9 @@ public final class AudioRawRecorder implements AsyncProcessor {
private Thread thread; private Thread thread;
private static final int READ_MS = 5; // milliseconds
private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS);
public AudioRawRecorder(AudioCapture capture, Streamer streamer) { public AudioRawRecorder(AudioCapture capture, Streamer streamer) {
this.capture = capture; this.capture = capture;
this.streamer = streamer; this.streamer = streamer;
@@ -25,22 +28,16 @@ public final class AudioRawRecorder implements AsyncProcessor {
return; return;
} }
final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioCapture.MAX_READ_SIZE); final ByteBuffer buffer = ByteBuffer.allocateDirect(READ_SIZE);
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
try { try {
try { capture.start();
capture.start();
} catch (Throwable t) {
// Notify the client that the audio could not be captured
streamer.writeDisableStream(false);
throw t;
}
streamer.writeAudioHeader(); streamer.writeAudioHeader();
while (!Thread.currentThread().isInterrupted()) { while (!Thread.currentThread().isInterrupted()) {
buffer.position(0); buffer.position(0);
int r = capture.read(buffer, bufferInfo); int r = capture.read(buffer, READ_SIZE, bufferInfo);
if (r < 0) { if (r < 0) {
throw new IOException("Could not read audio: " + r); throw new IOException("Could not read audio: " + r);
} }
@@ -48,11 +45,10 @@ public final class AudioRawRecorder implements AsyncProcessor {
streamer.writePacket(buffer, bufferInfo); streamer.writePacket(buffer, bufferInfo);
} }
} catch (IOException e) { } catch (Throwable e) {
// Broken pipe is expected on close, because the socket is closed by the client // Notify the client that the audio could not be captured
if (!IO.isBrokenPipe(e)) { streamer.writeDisableStream(false);
Ln.e("Audio capture error", e); throw e;
}
} finally { } finally {
capture.stop(); capture.stop();
} }
@@ -66,8 +62,8 @@ public final class AudioRawRecorder implements AsyncProcessor {
record(); record();
} catch (AudioCaptureForegroundException e) { } catch (AudioCaptureForegroundException e) {
// Do not print stack trace, a user-friendly error-message has already been logged // Do not print stack trace, a user-friendly error-message has already been logged
} catch (Throwable t) { } catch (IOException e) {
Ln.e("Audio recording error", t); Ln.e("Audio recording error", e);
fatalError = true; fatalError = true;
} finally { } finally {
Ln.d("Audio recorder stopped"); Ln.d("Audio recorder stopped");

View File

@@ -289,17 +289,18 @@ public class CameraCapture extends SurfaceCapture {
List<OutputConfiguration> outputs = Arrays.asList(outputConfig); List<OutputConfiguration> outputs = Arrays.asList(outputConfig);
int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR; int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR;
SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() { SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor,
@Override new CameraCaptureSession.StateCallback() {
public void onConfigured(CameraCaptureSession session) { @Override
future.complete(session); public void onConfigured(CameraCaptureSession session) {
} future.complete(session);
}
@Override @Override
public void onConfigureFailed(CameraCaptureSession session) { public void onConfigureFailed(CameraCaptureSession session) {
future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR)); future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR));
} }
}); });
camera.createCaptureSession(sessionConfig); camera.createCaptureSession(sessionConfig);

View File

@@ -14,6 +14,8 @@ import java.io.IOException;
*/ */
public final class CleanUp { public final class CleanUp {
public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
// A simple struct to be passed from the main process to the cleanup process // A simple struct to be passed from the main process to the cleanup process
public static class Config implements Parcelable { public static class Config implements Parcelable {
@@ -133,13 +135,13 @@ public final class CleanUp {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()}; String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()};
ProcessBuilder builder = new ProcessBuilder(cmd); ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", Server.SERVER_PATH); builder.environment().put("CLASSPATH", SERVER_PATH);
builder.start(); builder.start();
} }
public static void unlinkSelf() { public static void unlinkSelf() {
try { try {
new File(Server.SERVER_PATH).delete(); new File(SERVER_PATH).delete();
} catch (Exception e) { } catch (Exception e) {
Ln.e("Could not unlink server", e); Ln.e("Could not unlink server", e);
} }
@@ -187,7 +189,5 @@ public final class CleanUp {
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
} }
} }
System.exit(0);
} }
} }

View File

@@ -318,8 +318,9 @@ public class Controller implements AsyncProcessor {
} }
} }
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, MotionEvent event = MotionEvent
DEFAULT_DEVICE_ID, 0, source, 0); .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); return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
} }
@@ -340,8 +341,9 @@ public class Controller implements AsyncProcessor {
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, MotionEvent event = MotionEvent
DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0); .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); return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
} }

View File

@@ -1,7 +1,6 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.DisplayControl;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.SurfaceControl;
@@ -12,8 +11,8 @@ import android.graphics.Rect;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.IDisplayFoldListener;
import android.view.IRotationWatcher; import android.view.IRotationWatcher;
import android.view.IDisplayFoldListener;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.InputEvent; import android.view.InputEvent;
import android.view.KeyCharacterMap; import android.view.KeyCharacterMap;
@@ -45,11 +44,11 @@ public final class Device {
void onClipboardTextChanged(String text); void onClipboardTextChanged(String text);
} }
private final Size deviceSize;
private final Rect crop; private final Rect crop;
private int maxSize; private int maxSize;
private final int lockVideoOrientation; private final int lockVideoOrientation;
private Size deviceSize;
private ScreenInfo screenInfo; private ScreenInfo screenInfo;
private RotationListener rotationListener; private RotationListener rotationListener;
private FoldListener foldListener; private FoldListener foldListener;
@@ -116,8 +115,8 @@ public final class Device {
return; return;
} }
deviceSize = displayInfo.getSize(); screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(),
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); options.getMaxSize(), options.getLockVideoOrientation());
// notify // notify
if (foldListener != null) { if (foldListener != null) {
foldListener.onFoldChanged(displayId, folded); foldListener.onFoldChanged(displayId, folded);
@@ -316,12 +315,8 @@ public final class Device {
*/ */
public static boolean setScreenPowerMode(int mode) { public static boolean setScreenPowerMode(int mode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 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 // Change the power mode for all physical displays
long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds(); long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
if (physicalDisplayIds == null) { if (physicalDisplayIds == null) {
Ln.e("Could not get physical display ids"); Ln.e("Could not get physical display ids");
return false; return false;
@@ -329,8 +324,7 @@ public final class Device {
boolean allOk = true; boolean allOk = true;
for (long physicalDisplayId : physicalDisplayIds) { for (long physicalDisplayId : physicalDisplayIds) {
IBinder binder = useDisplayControl ? DisplayControl.getPhysicalDisplayToken( IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
physicalDisplayId) : SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
allOk &= SurfaceControl.setDisplayPowerMode(binder, mode); allOk &= SurfaceControl.setDisplayPowerMode(binder, mode);
} }
return allOk; return allOk;

View File

@@ -51,7 +51,6 @@ public final class DeviceMessageSender {
} }
} }
} }
public void start() { public void start() {
thread = new Thread(() -> { thread = new Thread(() -> {
try { try {

View File

@@ -2,12 +2,11 @@ package com.genymobile.scrcpy;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.AttributionSource; import android.content.AttributionSource;
import android.content.Context; import android.content.MutableContextWrapper;
import android.content.ContextWrapper;
import android.os.Build; import android.os.Build;
import android.os.Process; import android.os.Process;
public final class FakeContext extends ContextWrapper { public final class FakeContext extends MutableContextWrapper {
public static final String PACKAGE_NAME = "com.android.shell"; public static final String PACKAGE_NAME = "com.android.shell";
public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29 public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29
@@ -19,7 +18,7 @@ public final class FakeContext extends ContextWrapper {
} }
private FakeContext() { private FakeContext() {
super(Workarounds.getSystemContext()); super(Workarounds.retrieveSystemContext());
} }
@Override @Override
@@ -45,9 +44,4 @@ public final class FakeContext extends ContextWrapper {
public int getDeviceId() { public int getDeviceId() {
return 0; return 0;
} }
@Override
public Context getApplicationContext() {
return this;
}
} }

View File

@@ -93,26 +93,19 @@ public final class LogUtils {
builder.append("\n (none)"); builder.append("\n (none)");
} else { } else {
for (String id : cameraIds) { for (String id : cameraIds) {
builder.append("\n --camera-id=").append(id); builder.append("\n --video-source=camera --camera-id=").append(id);
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
int facing = characteristics.get(CameraCharacteristics.LENS_FACING); int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
builder.append(" (").append(getCameraFacingName(facing)).append(", "); builder.append(" (").append(getCameraFacingName(facing)).append(", ");
Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
builder.append(activeSize.width()).append("x").append(activeSize.height()); builder.append(activeSize.width()).append("x").append(activeSize.height()).append(", ");
try { // Capture frame rates for low-FPS mode are the same for every resolution
// Capture frame rates for low-FPS mode are the same for every resolution Range<Integer>[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
Range<Integer>[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges);
SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges); builder.append("fps=").append(uniqueLowFps).append(')');
builder.append(", fps=").append(uniqueLowFps);
} catch (Exception e) {
// Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper"
Ln.w("Could not get available frame rates for camera " + id, e);
}
builder.append(')');
if (includeSizes) { if (includeSizes) {
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

View File

@@ -18,6 +18,7 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList
@Override @Override
public void init() { public void init() {
display = createDisplay();
device.setRotationListener(this); device.setRotationListener(this);
device.setFoldListener(this); device.setFoldListener(this);
} }
@@ -31,11 +32,6 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation(); int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack(); int layerStack = device.getLayerStack();
if (display != null) {
SurfaceControl.destroyDisplay(display);
}
display = createDisplay();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
} }
@@ -43,9 +39,7 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList
public void release() { public void release() {
device.setRotationListener(null); device.setRotationListener(null);
device.setFoldListener(null); device.setFoldListener(null);
if (display != null) { SurfaceControl.destroyDisplay(display);
SurfaceControl.destroyDisplay(display);
}
} }
@Override @Override

View File

@@ -3,21 +3,12 @@ package com.genymobile.scrcpy;
import android.os.BatteryManager; import android.os.BatteryManager;
import android.os.Build; import android.os.Build;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public final class Server { 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 static class Completion {
private int running; private int running;
private boolean fatalError; private boolean fatalError;
@@ -101,6 +92,8 @@ public final class Server {
throw new ConfigurationException("Camera mirroring is not supported"); throw new ConfigurationException("Camera mirroring is not supported");
} }
final Device device = new Device(options);
Thread initThread = startInitThread(options); Thread initThread = startInitThread(options);
int scid = options.getScid(); int scid = options.getScid();
@@ -109,9 +102,7 @@ public final class Server {
boolean video = options.getVideo(); boolean video = options.getVideo();
boolean audio = options.getAudio(); boolean audio = options.getAudio();
boolean sendDummyByte = options.getSendDummyByte(); boolean sendDummyByte = options.getSendDummyByte();
boolean camera = video && options.getVideoSource() == VideoSource.CAMERA; boolean camera = options.getVideoSource() == VideoSource.CAMERA;
final Device device = camera ? null : new Device(options);
Workarounds.apply(audio, camera); Workarounds.apply(audio, camera);

View File

@@ -75,7 +75,7 @@ public final class Settings {
String oldValue = getValue(table, key); String oldValue = getValue(table, key);
if (!value.equals(oldValue)) { if (!value.equals(oldValue)) {
putValue(table, key, value); putValue(table, key, value);
} }
return oldValue; return oldValue;
} }

View File

@@ -5,14 +5,14 @@ import android.media.MediaCodec;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
public final class Streamer { public final class Streamer {
private static final long PACKET_FLAG_CONFIG = 1L << 63; private static final long PACKET_FLAG_CONFIG = 1L << 63;
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62; private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
private static final long AOPUSHDR = 0x5244485355504F41L; // "AOPUSHDR" in ASCII (little-endian)
private final FileDescriptor fd; private final FileDescriptor fd;
private final Codec codec; private final Codec codec;
private final boolean sendCodecMeta; private final boolean sendCodecMeta;
@@ -30,7 +30,6 @@ public final class Streamer {
public Codec getCodec() { public Codec getCodec() {
return codec; return codec;
} }
public void writeAudioHeader() throws IOException { public void writeAudioHeader() throws IOException {
if (sendCodecMeta) { if (sendCodecMeta) {
ByteBuffer buffer = ByteBuffer.allocate(4); ByteBuffer buffer = ByteBuffer.allocate(4);
@@ -63,12 +62,8 @@ public final class Streamer {
} }
public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException { public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException {
if (config) { if (config && codec == AudioCodec.OPUS) {
if (codec == AudioCodec.OPUS) { fixOpusConfigPacket(buffer);
fixOpusConfigPacket(buffer);
} else if (codec == AudioCodec.FLAC) {
fixFlacConfigPacket(buffer);
}
} }
if (sendFrameMeta) { if (sendFrameMeta) {
@@ -125,14 +120,11 @@ public final class Streamer {
throw new IOException("Not enough data in OPUS config packet"); throw new IOException("Not enough data in OPUS config packet");
} }
final byte[] opusHeaderId = {'A', 'O', 'P', 'U', 'S', 'H', 'D', 'R'}; long id = buffer.getLong();
byte[] idBuffer = new byte[8]; if (id != AOPUSHDR) {
buffer.get(idBuffer);
if (!Arrays.equals(idBuffer, opusHeaderId)) {
throw new IOException("OPUS header not found"); throw new IOException("OPUS header not found");
} }
// The size is in native byte-order
long sizeLong = buffer.getLong(); long sizeLong = buffer.getLong();
if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) { if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) {
throw new IOException("Invalid block size in OPUS header: " + sizeLong); throw new IOException("Invalid block size in OPUS header: " + sizeLong);
@@ -146,41 +138,4 @@ public final class Streamer {
// Set the buffer to point to the OPUS header slice // Set the buffer to point to the OPUS header slice
buffer.limit(buffer.position() + size); buffer.limit(buffer.position() + size);
} }
private static void fixFlacConfigPacket(ByteBuffer buffer) throws IOException {
// 00000000 66 4c 61 43 00 00 00 22 |fLaC..." |
// -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA -------------------
// 00000000 10 00 10 00 00 00 00 00 | ........|
// 00000010 00 00 0b b8 02 f0 00 00 00 00 00 00 00 00 00 00 |................|
// 00000020 00 00 00 00 00 00 00 00 00 00 |.......... |
// ------------------------------------------------------------------------------
// 00000020 84 00 00 28 20 00 | ...( .|
// 00000030 00 00 72 65 66 65 72 65 6e 63 65 20 6c 69 62 46 |..reference libF|
// 00000040 4c 41 43 20 31 2e 33 2e 32 20 32 30 32 32 31 30 |LAC 1.3.2 202210|
// 00000050 32 32 00 00 00 00 |22....|
//
// <https://developer.android.com/reference/android/media/MediaCodec#CSD>
if (buffer.remaining() < 8) {
throw new IOException("Not enough data in FLAC config packet");
}
final byte[] flacHeaderId = {'f', 'L', 'a', 'C'};
byte[] idBuffer = new byte[4];
buffer.get(idBuffer);
if (!Arrays.equals(idBuffer, flacHeaderId)) {
throw new IOException("FLAC header not found");
}
// The size is in big-endian
buffer.order(ByteOrder.BIG_ENDIAN);
int size = buffer.getInt();
if (buffer.remaining() < size) {
throw new IOException("Not enough data in FLAC header (invalid size: " + size + ")");
}
// Set the buffer to point to the FLAC header slice
buffer.limit(buffer.position() + size);
}
} }

View File

@@ -7,6 +7,7 @@ import android.content.AttributionSource;
import android.content.Context; import android.content.Context;
import android.content.ContextWrapper; import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.media.AudioAttributes; import android.media.AudioAttributes;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.AudioRecord; import android.media.AudioRecord;
@@ -19,38 +20,26 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@SuppressLint("PrivateApi,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi")
public final class Workarounds { public final class Workarounds {
private static final Class<?> ACTIVITY_THREAD_CLASS; private static Class<?> activityThreadClass;
private static final Object ACTIVITY_THREAD; private static Object activityThread;
static {
prepareMainLooper();
try {
// ActivityThread activityThread = new ActivityThread();
ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread");
Constructor<?> activityThreadConstructor = ACTIVITY_THREAD_CLASS.getDeclaredConstructor();
activityThreadConstructor.setAccessible(true);
ACTIVITY_THREAD = activityThreadConstructor.newInstance();
// ActivityThread.sCurrentActivityThread = activityThread;
Field sCurrentActivityThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
sCurrentActivityThreadField.set(null, ACTIVITY_THREAD);
} catch (Exception e) {
throw new AssertionError(e);
}
}
private Workarounds() { private Workarounds() {
// not instantiable // not instantiable
} }
public static void apply(boolean audio, boolean camera) { public static void apply(boolean audio, boolean camera) {
boolean mustFillConfigurationController = false; Workarounds.prepareMainLooper();
try {
fillActivityThread();
FakeContext.get();
} catch (Exception e) {
throw new AssertionError(e);
}
boolean mustFillAppInfo = false; boolean mustFillAppInfo = false;
boolean mustFillBaseContext = false;
boolean mustFillAppContext = false; boolean mustFillAppContext = false;
if (Build.BRAND.equalsIgnoreCase("meizu")) { if (Build.BRAND.equalsIgnoreCase("meizu")) {
@@ -71,6 +60,7 @@ public final class Workarounds {
// - <https://github.com/Genymobile/scrcpy/issues/4015#issuecomment-1595382142> // - <https://github.com/Genymobile/scrcpy/issues/4015#issuecomment-1595382142>
// - <https://github.com/Genymobile/scrcpy/issues/3805#issuecomment-1596148031> // - <https://github.com/Genymobile/scrcpy/issues/3805#issuecomment-1596148031>
mustFillAppInfo = true; mustFillAppInfo = true;
mustFillBaseContext = true;
mustFillAppContext = true; mustFillAppContext = true;
} }
@@ -83,26 +73,17 @@ public final class Workarounds {
if (camera) { if (camera) {
mustFillAppInfo = true; mustFillAppInfo = true;
mustFillAppContext = true; mustFillBaseContext = 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) { if (mustFillAppInfo) {
fillAppInfo(); Workarounds.fillAppInfo();
}
if (mustFillBaseContext) {
Workarounds.fillBaseContext();
} }
if (mustFillAppContext) { if (mustFillAppContext) {
fillAppContext(); Workarounds.fillAppContext();
} }
} }
@@ -119,8 +100,27 @@ public final class Workarounds {
Looper.prepareMainLooper(); Looper.prepareMainLooper();
} }
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
private static void fillActivityThread() throws Exception {
if (activityThread == null) {
// ActivityThread activityThread = new ActivityThread();
activityThreadClass = Class.forName("android.app.ActivityThread");
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
activityThreadConstructor.setAccessible(true);
activityThread = activityThreadConstructor.newInstance();
// ActivityThread.sCurrentActivityThread = activityThread;
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
sCurrentActivityThreadField.set(null, activityThread);
}
}
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
private static void fillAppInfo() { private static void fillAppInfo() {
try { try {
fillActivityThread();
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData"); Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
Constructor<?> appBindDataConstructor = appBindDataClass.getDeclaredConstructor(); Constructor<?> appBindDataConstructor = appBindDataClass.getDeclaredConstructor();
@@ -130,67 +130,69 @@ public final class Workarounds {
ApplicationInfo applicationInfo = new ApplicationInfo(); ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.packageName = FakeContext.PACKAGE_NAME; applicationInfo.packageName = FakeContext.PACKAGE_NAME;
Application application = new Application() {
@Override
public String getOpPackageName() {
return FakeContext.PACKAGE_NAME;
}
};
Method method = ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class);
method.invoke(application, retrieveSystemContext());
Field initialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
initialApplicationField.setAccessible(true);
initialApplicationField.set(activityThread, application);
// appBindData.appInfo = applicationInfo; // appBindData.appInfo = applicationInfo;
Field appInfoField = appBindDataClass.getDeclaredField("appInfo"); Field appInfoField = appBindDataClass.getDeclaredField("appInfo");
appInfoField.setAccessible(true); appInfoField.setAccessible(true);
appInfoField.set(appBindData, applicationInfo); appInfoField.set(appBindData, applicationInfo);
// activityThread.mBoundApplication = appBindData; // activityThread.mBoundApplication = appBindData;
Field mBoundApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mBoundApplication"); Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
mBoundApplicationField.setAccessible(true); mBoundApplicationField.setAccessible(true);
mBoundApplicationField.set(ACTIVITY_THREAD, appBindData); mBoundApplicationField.set(activityThread, appBindData);
} catch (Throwable throwable) { } catch (Throwable throwable) {
// this is a workaround, so failing is not an error // this is a workaround, so failing is not an error
Ln.d("Could not fill app info: " + throwable.getMessage()); Ln.d("Could not fill app info: " + throwable.getMessage());
} }
} }
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
private static void fillAppContext() { private static void fillAppContext() {
try { try {
Application app = new Application(); fillActivityThread();
Application app = Application.class.newInstance();
Field baseField = ContextWrapper.class.getDeclaredField("mBase"); Field baseField = ContextWrapper.class.getDeclaredField("mBase");
baseField.setAccessible(true); baseField.setAccessible(true);
baseField.set(app, FakeContext.get()); baseField.set(app, FakeContext.get());
// activityThread.mInitialApplication = app; // activityThread.mInitialApplication = app;
Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication"); Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
mInitialApplicationField.setAccessible(true); mInitialApplicationField.setAccessible(true);
mInitialApplicationField.set(ACTIVITY_THREAD, app); mInitialApplicationField.set(activityThread, app);
} catch (Throwable throwable) { } catch (Throwable throwable) {
// this is a workaround, so failing is not an error // this is a workaround, so failing is not an error
Ln.d("Could not fill app context: " + throwable.getMessage()); Ln.d("Could not fill app context: " + throwable.getMessage());
} }
} }
private static void fillConfigurationController() { private static void fillBaseContext() {
try { try {
Class<?> configurationControllerClass = Class.forName("android.app.ConfigurationController"); fillActivityThread();
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"); Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
configurationControllerField.setAccessible(true); Context context = (Context) getSystemContextMethod.invoke(activityThread);
configurationControllerField.set(ACTIVITY_THREAD, configurationController); FakeContext.get().setBaseContext(context);
} catch (Throwable throwable) {
Ln.d("Could not fill configuration: " + throwable.getMessage());
}
}
static Context getSystemContext() {
try {
Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext");
return (Context) getSystemContextMethod.invoke(ACTIVITY_THREAD);
} catch (Throwable throwable) { } catch (Throwable throwable) {
// this is a workaround, so failing is not an error // this is a workaround, so failing is not an error
Ln.d("Could not get system context: " + throwable.getMessage()); Ln.d("Could not fill base context: " + throwable.getMessage());
return null;
} }
} }
@TargetApi(Build.VERSION_CODES.R) @TargetApi(Build.VERSION_CODES.R)
@SuppressLint("WrongConstant,MissingPermission") @SuppressLint("WrongConstant,MissingPermission,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi")
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) { public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) {
// Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment. // Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment.
// //
@@ -285,28 +287,16 @@ public final class Workarounds {
Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel"); Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel");
Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState); Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // private native int native_setup(Object audiorecordThis,
// private native int native_setup(Object audiorecordThis, // Object /*AudioAttributes*/ attributes,
// Object /*AudioAttributes*/ attributes, // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, // int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource,
// int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource, // long nativeRecordInJavaObj, int maxSharedAudioHistoryMs);
// long nativeRecordInJavaObj, int maxSharedAudioHistoryMs); Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class,
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class);
int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class); nativeSetupMethod.setAccessible(true);
nativeSetupMethod.setAccessible(true); initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes, sampleRateArray,
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, attributionSourceParcel, 0L, 0);
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);
}
} }
} }
@@ -336,4 +326,16 @@ public final class Workarounds {
throw new RuntimeException("Cannot create AudioRecord"); throw new RuntimeException("Cannot create AudioRecord");
} }
} }
static Context retrieveSystemContext() {
try {
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
Context ctx = (Context) getSystemContextMethod.invoke(activityThread);
Ln.i("===== " + ctx);
return ctx;
} catch (Exception e) {
Ln.e("Cannot retrieve system context", e);
return null;
}
}
} }

View File

@@ -41,14 +41,8 @@ public final class ClipboardManager {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
getMethodVersion = 2; getMethodVersion = 2;
} catch (NoSuchMethodException e3) { } catch (NoSuchMethodException e3) {
try { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); getMethodVersion = 3;
getMethodVersion = 3;
} catch (NoSuchMethodException e4) {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class);
getMethodVersion = 4;
}
} }
} }
} }
@@ -93,11 +87,8 @@ public final class ClipboardManager {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
case 2: case 2:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
case 3:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null);
default: default:
// The last boolean parameter is "userOperate" return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null);
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
} }
} }
@@ -147,8 +138,8 @@ public final class ClipboardManager {
} }
} }
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener) private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager,
throws InvocationTargetException, IllegalAccessException { IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME); method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
return; return;

View File

@@ -1,80 +0,0 @@
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.InvocationTargetException;
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 (InvocationTargetException | IllegalAccessException | NoSuchMethodException 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 (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
}

View File

@@ -20,7 +20,7 @@ public final class PowerManager {
private Method getIsScreenOnMethod() throws NoSuchMethodException { private Method getIsScreenOnMethod() throws NoSuchMethodException {
if (isScreenOnMethod == null) { if (isScreenOnMethod == null) {
@SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future @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); isScreenOnMethod = manager.getClass().getMethod(methodName);
} }
return isScreenOnMethod; return isScreenOnMethod;

View File

@@ -16,7 +16,6 @@ import java.lang.reflect.Method;
public final class ServiceManager { public final class ServiceManager {
private static final Method GET_SERVICE_METHOD; private static final Method GET_SERVICE_METHOD;
static { static {
try { try {
GET_SERVICE_METHOD = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); GET_SERVICE_METHOD = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class);

View File

@@ -139,15 +139,6 @@ public final class SurfaceControl {
return getPhysicalDisplayIdsMethod; return getPhysicalDisplayIdsMethod;
} }
public static boolean hasPhysicalDisplayIdsMethod() {
try {
getGetPhysicalDisplayIdsMethod();
return true;
} catch (NoSuchMethodException e) {
return false;
}
}
public static long[] getPhysicalDisplayIds() { public static long[] getPhysicalDisplayIds() {
try { try {
Method method = getGetPhysicalDisplayIdsMethod(); Method method = getGetPhysicalDisplayIdsMethod();

View File

@@ -4,8 +4,8 @@ import com.genymobile.scrcpy.Ln;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.os.IInterface; import android.os.IInterface;
import android.view.IDisplayFoldListener;
import android.view.IRotationWatcher; import android.view.IRotationWatcher;
import android.view.IDisplayFoldListener;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;