Compare commits

..

4 Commits

Author SHA1 Message Date
Romain Vimont
73c0f3ee05 Add compilation flag for HIDPI workaround
The HIDPI workaround is enabled by default. Expose a flag to be able to
disable it.
2019-11-06 19:27:42 +01:00
Louis Kruger
78d9d82091 Reset renderer on hidpi scaling change
Moving a window between hidpi and non-hidpi monitors is not correctly
handled by SDL.

Detect when this happens, and recreate the renderer.

Fixes <https://github.com/Genymobile/scrcpy/issues/15>.

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-11-06 19:27:38 +01:00
Romain Vimont
c8c921caf5 Remove HIDPI compilation flag
The flag had been added to avoid hidpi bugs.

A workaround will be implemented for these bugs, so the flag is not
needed anymore.
2019-11-06 19:15:30 +01:00
Romain Vimont
39ff8d9900 Handle window resizing in screen
Only the screen knows what to do when the window is resized.

This paves the way to do other actions on window resizing.
2019-11-06 18:25:37 +01:00
83 changed files with 913 additions and 1553 deletions

View File

@@ -233,10 +233,10 @@ You can then [run](README.md#run) _scrcpy_.
## Prebuilt server ## Prebuilt server
- [`scrcpy-server-v1.11`][direct-scrcpy-server] - [`scrcpy-server-v1.10.jar`][direct-scrcpy-server]
_(SHA-256: ff3a454012e91d9185cfe8ca7691cea16c43a7dcc08e92fa47ab9f0ea675abd1)_ _(SHA-256: cbeb1a4e046f1392c1dc73c3ccffd7f86dec4636b505556ea20929687a119390)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-server-v1.11 [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-server-v1.10.jar
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

@@ -100,11 +100,11 @@ dist-win32: build-server build-win32 build-win32-noconsole
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 "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
@@ -115,11 +115,11 @@ dist-win64: build-server build-win64 build-win64-noconsole
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 "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"

264
README.md
View File

@@ -1,4 +1,4 @@
# scrcpy (v1.11) # scrcpy (v1.10)
This application provides display and control of Android devices connected on This application provides display and control of Android devices connected on
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
@@ -62,13 +62,13 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
For Windows, for simplicity, prebuilt archives with all the dependencies For Windows, for simplicity, prebuilt archives with all the dependencies
(including `adb`) are available: (including `adb`) are available:
- [`scrcpy-win32-v1.11.zip`][direct-win32] - [`scrcpy-win32-v1.10.zip`][direct-win32]
_(SHA-256: f25ed46e6f3e81e0ff9b9b4df7fe1a4bbd13f8396b7391be0a488b64c675b41e)_ _(SHA-256: f98b400b3764404b33b212e9762dd6f1593ddb766c1480fc2609c94768e4a8e1)_
- [`scrcpy-win64-v1.11.zip`][direct-win64] - [`scrcpy-win64-v1.10.zip`][direct-win64]
_(SHA-256: 3802c9ea0307d437947ff150ec65e53990b0beaacd0c8d0bed19c7650ce141bd)_ _(SHA-256: 95de34575d873c7e95dfcfb5e74d0f6af4f70b2a5bc6fde0f48d1a05480e3a44)_
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-win32-v1.11.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-win32-v1.10.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-win64-v1.11.zip [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-win64-v1.10.zip
You can also [build the app manually][BUILD]. You can also [build the app manually][BUILD].
@@ -108,9 +108,8 @@ scrcpy --help
## Features ## Features
### Capture configuration
#### Reduce size ### Reduce size
Sometimes, it is useful to mirror an Android device at a lower definition to Sometimes, it is useful to mirror an Android device at a lower definition to
increase performance. increase performance.
@@ -126,7 +125,7 @@ The other dimension is computed to that the device aspect ratio is preserved.
That way, a device in 1920×1080 will be mirrored at 1024×576. That way, a device in 1920×1080 will be mirrored at 1024×576.
#### Change bit-rate ### Change bit-rate
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps): The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
@@ -135,15 +134,8 @@ scrcpy --bit-rate 2M
scrcpy -b 2M # short version scrcpy -b 2M # short version
``` ```
#### Limit frame rate
On devices with Android >= 10, the capture frame rate can be limited: ### Crop
```bash
scrcpy --max-fps 15
```
#### 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.
@@ -151,12 +143,35 @@ This is useful for example to mirror only one eye of the Oculus Go:
```bash ```bash
scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
scrcpy -c 1224:1440:0:0 # short version
``` ```
If `--max-size` is also specified, resizing is applied after cropping. If `--max-size` is also specified, resizing is applied after cropping.
### Recording ### Wireless
_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
device over TCP/IP:
1. Connect the device to the same Wi-Fi as your computer.
2. Get your device IP address (in Settings → About phone → Status).
3. Enable adb over TCP/IP on your device: `adb tcpip 5555`.
4. Unplug your device.
5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_.
6. Run `scrcpy` as usual.
It may be useful to decrease the bit-rate and the definition:
```bash
scrcpy --bit-rate 2M --max-size 800
scrcpy -b2M -m800 # short version
```
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
### Record screen
It is possible to record the screen while mirroring: It is possible to record the screen while mirroring:
@@ -181,31 +196,7 @@ variation] does not impact the recorded file.
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
### Connection ### Multi-devices
#### Wireless
_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
device over TCP/IP:
1. Connect the device to the same Wi-Fi as your computer.
2. Get your device IP address (in Settings → About phone → Status).
3. Enable adb over TCP/IP on your device: `adb tcpip 5555`.
4. Unplug your device.
5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_.
6. Run `scrcpy` as usual.
It may be useful to decrease the bit-rate and the definition:
```bash
scrcpy --bit-rate 2M --max-size 800
scrcpy -b2M -m800 # short version
```
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
#### Multi-devices
If several devices are listed in `adb devices`, you must specify the _serial_: If several devices are listed in `adb devices`, you must specify the _serial_:
@@ -216,65 +207,8 @@ scrcpy -s 0123456789abcdef # short version
You can start several instances of _scrcpy_ for several devices. You can start several instances of _scrcpy_ for several devices.
#### SSH tunnel
To connect to a remote device, it is possible to connect a local `adb` client to ### Fullscreen
a remote `adb` server (provided they use the same version of the _adb_
protocol):
```bash
adb kill-server # kill the local adb server on 5037
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
# keep this open
```
From another terminal:
```bash
scrcpy
```
Like for wireless connections, it may be useful to reduce quality:
```
scrcpy -b2M -m800 --max-fps 15
```
### Window configuration
#### Title
By default, the window title is the device model. It can be changed:
```bash
scrcpy --window-title 'My device'
```
#### Position and size
The initial window position and size may be specified:
```bash
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
```
#### Borderless
To disable window decorations:
```bash
scrcpy --window-borderless
```
#### Always on top
To keep the scrcpy window always on top:
```bash
scrcpy --always-on-top
```
#### Fullscreen
The app may be started directly in fullscreen: The app may be started directly in fullscreen:
@@ -286,45 +220,17 @@ scrcpy -f # short version
Fullscreen can then be toggled dynamically with `Ctrl`+`f`. Fullscreen can then be toggled dynamically with `Ctrl`+`f`.
### Other mirroring options ### Always on top
#### Read-only The window of app can always be above others by:
To disable controls (everything which can interact with the device: input keys,
mouse events, drag&drop files):
```bash ```bash
scrcpy --no-control scrcpy --always-on-top
scrcpy -n scrcpy -T # short version
``` ```
#### Turn screen off
It is possible to turn the device screen off while mirroring on start with a ### Show touches
command-line option:
```bash
scrcpy --turn-screen-off
scrcpy -S
```
Or by pressing `Ctrl`+`o` at any time.
To turn it back on, press `POWER` (or `Ctrl`+`p`).
#### Render expired frames
By default, to minimize latency, _scrcpy_ always renders the last decoded frame
available, and drops any previous one.
To force the rendering of all frames (at a cost of a possible increased
latency), use:
```bash
scrcpy --render-expired-frames
```
#### Show touches
For presentations, it may be useful to show physical touches (on the physical For presentations, it may be useful to show physical touches (on the physical
device). device).
@@ -341,43 +247,7 @@ scrcpy -t
Note that it only shows _physical_ touches (with the finger on the device). Note that it only shows _physical_ touches (with the finger on the device).
### Input control ### Install APK
#### Copy-paste
It is possible to synchronize clipboards between the computer and the device, in
both directions:
- `Ctrl`+`c` copies the device clipboard to the computer clipboard;
- `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard;
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but
breaks non-ASCII characters).
#### Text injection preference
There are two kinds of [events][textevents] generated when typing text:
- _key events_, signaling that a key is pressed or released;
- _text events_, signaling that a text has been entered.
By default, letters are injected using key events, so that the keyboard behaves
as expected in games (typically for WASD keys).
But this may [cause issues][prefertext]. If you encounter such a problem, you
can avoid it by:
```bash
scrcpy --prefer-text
```
(but this will break keyboard behavior in games)
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
### File drop
#### Install APK
To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_ To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_
window. window.
@@ -385,7 +255,7 @@ window.
There is no visual feedback, a log is printed to the console. There is no visual feedback, a log is printed to the console.
#### Push file to device ### Push file to device
To push a file to `/sdcard/` on the device, drag & drop a (non-APK) file to the To push a file to `/sdcard/` on the device, drag & drop a (non-APK) file to the
_scrcpy_ window. _scrcpy_ window.
@@ -398,8 +268,53 @@ The target directory can be changed on start:
scrcpy --push-target /sdcard/foo/bar/ scrcpy --push-target /sdcard/foo/bar/
``` ```
### Read-only
### Audio forwarding To disable controls (everything which can interact with the device: input keys,
mouse events, drag&drop files):
```bash
scrcpy --no-control
scrcpy -n
```
### Turn screen off
It is possible to turn the device screen off while mirroring on start with a
command-line option:
```bash
scrcpy --turn-screen-off
scrcpy -S
```
Or by pressing `Ctrl`+`o` at any time.
To turn it back on, press `POWER` (or `Ctrl`+`p`).
### Render expired frames
By default, to minimize latency, _scrcpy_ always renders the last decoded frame
available, and drops any previous one.
To force the rendering of all frames (at a cost of a possible increased
latency), use:
```bash
scrcpy --render-expired-frames
```
### Custom window title
By default, the window title is the device model. It can be changed:
```bash
scrcpy --window-title 'My device'
```
### Forward audio
Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only). Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only).
@@ -425,7 +340,6 @@ Also see [issue #14].
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p` | Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
| Power on | _Right-click²_ | _Right-click²_ | Power on | _Right-click²_ | _Right-click²_
| Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o` | Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o`
| Rotate device screen | `Ctrl`+`r` | `Cmd`+`r`
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n` | Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` | Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c` | Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`

View File

@@ -10,16 +10,16 @@ src = [
'src/file_handler.c', 'src/file_handler.c',
'src/fps_counter.c', 'src/fps_counter.c',
'src/input_manager.c', 'src/input_manager.c',
'src/net.c',
'src/receiver.c', 'src/receiver.c',
'src/recorder.c', 'src/recorder.c',
'src/scrcpy.c', 'src/scrcpy.c',
'src/screen.c', 'src/screen.c',
'src/server.c', 'src/server.c',
'src/stream.c', 'src/str_util.c',
'src/tiny_xpm.c', 'src/tiny_xpm.c',
'src/stream.c',
'src/video_buffer.c', 'src/video_buffer.c',
'src/util/net.c',
'src/util/str_util.c'
] ]
if not get_option('crossbuild_windows') if not get_option('crossbuild_windows')
@@ -85,7 +85,7 @@ endif
conf = configuration_data() conf = configuration_data()
# expose the build type # expose the build type
conf.set('NDEBUG', get_option('buildtype') != 'debug') conf.set('BUILD_DEBUG', get_option('buildtype') == 'debug')
# the version, updated on release # the version, updated on release
conf.set_quoted('SCRCPY_VERSION', meson.project_version()) conf.set_quoted('SCRCPY_VERSION', meson.project_version())
@@ -109,22 +109,24 @@ conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited
# overridden by option --bit-rate # overridden by option --bit-rate
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
# enable High DPI support
conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))
# disable console on Windows # disable console on Windows
conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole')) conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole'))
# run a server debugger and wait for a client to be attached # run a server debugger and wait for a client to be attached
conf.set('SERVER_DEBUGGER', get_option('server_debugger')) conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
# enable a workaround for bug #15
conf.set('HIDPI_WORKAROUND', get_option('hidpi_workaround'))
configure_file(configuration: conf, output: 'config.h') configure_file(configuration: conf, output: 'config.h')
src_dir = include_directories('src') src_dir = include_directories('src')
if get_option('windows_noconsole') if get_option('windows_noconsole')
link_args = [ '-Wl,--subsystem,windows' ] c_args = [ '-mwindows' ]
link_args = [ '-mwindows' ]
else else
c_args = []
link_args = [] link_args = []
endif endif
@@ -132,7 +134,7 @@ executable('scrcpy', src,
dependencies: dependencies, dependencies: dependencies,
include_directories: src_dir, include_directories: src_dir,
install: true, install: true,
c_args: [], c_args: c_args,
link_args: link_args) link_args: link_args)
install_man('scrcpy.1') install_man('scrcpy.1')
@@ -141,16 +143,13 @@ install_man('scrcpy.1')
### TESTS ### TESTS
tests = [ tests = [
['test_buffer_util', [
'tests/test_buffer_util.c'
]],
['test_cbuf', [ ['test_cbuf', [
'tests/test_cbuf.c', 'tests/test_cbuf.c',
]], ]],
['test_control_event_serialize', [ ['test_control_event_serialize', [
'tests/test_control_msg_serialize.c', 'tests/test_control_msg_serialize.c',
'src/control_msg.c', 'src/control_msg.c',
'src/util/str_util.c' 'src/str_util.c'
]], ]],
['test_device_event_deserialize', [ ['test_device_event_deserialize', [
'tests/test_device_msg_deserialize.c', 'tests/test_device_msg_deserialize.c',
@@ -161,7 +160,7 @@ tests = [
]], ]],
['test_strutil', [ ['test_strutil', [
'tests/test_strutil.c', 'tests/test_strutil.c',
'src/util/str_util.c' 'src/str_util.c'
]], ]],
] ]

View File

@@ -15,10 +15,6 @@ provides display and control of Android devices connected on USB (or over TCP/IP
.SH OPTIONS .SH OPTIONS
.TP
.B \-\-always\-on\-top
Make scrcpy window always on top (above other windows).
.TP .TP
.BI "\-b, \-\-bit\-rate " value .BI "\-b, \-\-bit\-rate " value
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
@@ -26,7 +22,7 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are
Default is 8000000. Default is 8000000.
.TP .TP
.BI \-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy .BI "\-c, \-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
Crop the device screen on the server. Crop the device screen on the server.
The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any
@@ -38,12 +34,12 @@ value is computed on the cropped size.
Start in fullscreen. Start in fullscreen.
.TP .TP
.B \-h, \-\-help .BI "\-F, \-\-record\-format " format
Print this help. Force recording format (either mp4 or mkv).
.TP .TP
.BI \-\-max\-fps " value .B \-h, \-\-help
Limit the framerate of screen capture (only supported on devices with Android >= 10). Print this help.
.TP .TP
.BI "\-m, \-\-max\-size " value .BI "\-m, \-\-max\-size " value
@@ -65,13 +61,6 @@ Set the TCP port the client listens on.
Default is 27183. Default is 27183.
.TP
.B \-\-prefer\-text
Inject alpha characters and space as text events instead of key events.
This avoids issues when combining multiple keys to enter special characters,
but breaks the expected behavior of alpha keys in games (typically WASD).
.TP .TP
.BI "\-\-push\-target " path .BI "\-\-push\-target " path
Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push". Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push".
@@ -84,13 +73,9 @@ Record screen to
.IR file . .IR file .
The format is determined by the The format is determined by the
.B \-\-record\-format .B \-F/\-\-record\-format
option if set, or by the file extension (.mp4 or .mkv). option if set, or by the file extension (.mp4 or .mkv).
.TP
.BI \-\-record\-format " format
Force recording format (either mp4 or mkv).
.TP .TP
.B \-\-render\-expired\-frames .B \-\-render\-expired\-frames
By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency. By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency.
@@ -110,40 +95,17 @@ Enable "show touches" on start, disable on quit.
It only shows physical touches (not clicks from scrcpy). It only shows physical touches (not clicks from scrcpy).
.TP .TP
.B \-v, \-\-version .B \-T, \-\-always\-on\-top
Print the version of scrcpy. Make scrcpy window always on top (above other windows).
.TP .TP
.B \-\-window\-borderless .B \-v, \-\-version
Disable window decorations (display borderless window). Print the version of scrcpy.
.TP .TP
.BI \-\-window\-title " text .BI \-\-window\-title " text
Set a custom window title. Set a custom window title.
.TP
.BI \-\-window\-x " value
Set the initial window horizontal position.
Default is -1 (automatic).\n
.TP
.BI \-\-window\-y " value
Set the initial window vertical position.
Default is -1 (automatic).\n
.TP
.BI \-\-window\-width " value
Set the initial window width.
Default is 0 (automatic).\n
.TP
.BI \-\-window\-height " value
Set the initial window height.
Default is 0 (automatic).\n
.SH SHORTCUTS .SH SHORTCUTS
@@ -195,10 +157,6 @@ turn screen on
.B Ctrl+o .B Ctrl+o
turn device screen off (keep mirroring) turn device screen off (keep mirroring)
.TP
.B Ctrl+r
rotate device screen
.TP .TP
.B Ctrl+n .B Ctrl+n
expand notification panel expand notification panel

View File

@@ -36,8 +36,8 @@ buffer_read32be(const uint8_t *buf) {
return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
} }
static inline uint64_t static inline
buffer_read64be(const uint8_t *buf) { uint64_t buffer_read64be(const uint8_t *buf) {
uint32_t msb = buffer_read32be(buf); uint32_t msb = buffer_read32be(buf);
uint32_t lsb = buffer_read32be(&buf[4]); uint32_t lsb = buffer_read32be(&buf[4]);
return ((uint64_t) msb << 32) | lsb; return ((uint64_t) msb << 32) | lsb;

View File

@@ -7,8 +7,8 @@
#include "config.h" #include "config.h"
#include "common.h" #include "common.h"
#include "util/log.h" #include "log.h"
#include "util/str_util.h" #include "str_util.h"
static const char *adb_command; static const char *adb_command;
@@ -91,7 +91,7 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
memcpy(&cmd[i], adb_cmd, len * sizeof(const char *)); memcpy(&cmd[i], adb_cmd, len * sizeof(const char *));
cmd[len + i] = NULL; cmd[len + i] = NULL;
enum process_result r = cmd_execute(cmd, &process); enum process_result r = cmd_execute(cmd[0], cmd, &process);
if (r != PROCESS_SUCCESS) { if (r != PROCESS_SUCCESS) {
show_adb_err_msg(r, cmd); show_adb_err_msg(r, cmd);
return PROCESS_NONE; return PROCESS_NONE;

View File

@@ -18,7 +18,6 @@
# define PRIsizet PRIu32 # define PRIsizet PRIu32
# endif # endif
# define PROCESS_NONE NULL # define PROCESS_NONE NULL
# define NO_EXIT_CODE -1u // max value as unsigned
typedef HANDLE process_t; typedef HANDLE process_t;
typedef DWORD exit_code_t; typedef DWORD exit_code_t;
@@ -29,7 +28,6 @@
# define PRIsizet "zu" # define PRIsizet "zu"
# define PRIexitcode "d" # define PRIexitcode "d"
# define PROCESS_NONE -1 # define PROCESS_NONE -1
# define NO_EXIT_CODE -1
typedef pid_t process_t; typedef pid_t process_t;
typedef int exit_code_t; typedef int exit_code_t;
@@ -37,6 +35,8 @@
#include "config.h" #include "config.h"
# define NO_EXIT_CODE -1
enum process_result { enum process_result {
PROCESS_SUCCESS, PROCESS_SUCCESS,
PROCESS_ERROR_GENERIC, PROCESS_ERROR_GENERIC,
@@ -44,7 +44,7 @@ enum process_result {
}; };
enum process_result enum process_result
cmd_execute(const char *const argv[], process_t *process); cmd_execute(const char *path, const char *const argv[], process_t *process);
bool bool
cmd_terminate(process_t pid); cmd_terminate(process_t pid);

View File

@@ -1,12 +1,12 @@
#include "control_msg.h" #include "control_msg.h"
#include <assert.h>
#include <string.h> #include <string.h>
#include <SDL2/SDL_assert.h>
#include "config.h" #include "config.h"
#include "util/buffer_util.h" #include "buffer_util.h"
#include "util/log.h" #include "log.h"
#include "util/str_util.h" #include "str_util.h"
static void static void
write_position(uint8_t *buf, const struct position *position) { write_position(uint8_t *buf, const struct position *position) {
@@ -27,7 +27,7 @@ write_string(const char *utf8, size_t max_len, unsigned char *buf) {
static uint16_t static uint16_t
to_fixed_point_16(float f) { to_fixed_point_16(float f) {
assert(f >= 0.0f && f <= 1.0f); SDL_assert(f >= 0.0f && f <= 1.0f);
uint32_t u = f * 0x1p16f; // 2^16 uint32_t u = f * 0x1p16f; // 2^16
if (u >= 0xffff) { if (u >= 0xffff) {
u = 0xffff; u = 0xffff;
@@ -78,7 +78,6 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_GET_CLIPBOARD: case CONTROL_MSG_TYPE_GET_CLIPBOARD:
case CONTROL_MSG_TYPE_ROTATE_DEVICE:
// no additional data // no additional data
return 1; return 1;
default: default:

View File

@@ -28,7 +28,6 @@ enum control_msg_type {
CONTROL_MSG_TYPE_GET_CLIPBOARD, CONTROL_MSG_TYPE_GET_CLIPBOARD,
CONTROL_MSG_TYPE_SET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD,
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
CONTROL_MSG_TYPE_ROTATE_DEVICE,
}; };
enum screen_power_mode { enum screen_power_mode {

View File

@@ -1,10 +1,10 @@
#include "controller.h" #include "controller.h"
#include <assert.h> #include <SDL2/SDL_assert.h>
#include "config.h" #include "config.h"
#include "util/lock.h" #include "lock_util.h"
#include "util/log.h" #include "log.h"
bool bool
controller_init(struct controller *controller, socket_t control_socket) { controller_init(struct controller *controller, socket_t control_socket) {
@@ -85,8 +85,7 @@ run_controller(void *data) {
} }
struct control_msg msg; struct control_msg msg;
bool non_empty = cbuf_take(&controller->queue, &msg); bool non_empty = cbuf_take(&controller->queue, &msg);
assert(non_empty); SDL_assert(non_empty);
(void) non_empty;
mutex_unlock(controller->mutex); mutex_unlock(controller->mutex);
bool ok = process_msg(controller, &msg); bool ok = process_msg(controller, &msg);

View File

@@ -6,10 +6,10 @@
#include <SDL2/SDL_thread.h> #include <SDL2/SDL_thread.h>
#include "config.h" #include "config.h"
#include "cbuf.h"
#include "control_msg.h" #include "control_msg.h"
#include "net.h"
#include "receiver.h" #include "receiver.h"
#include "util/cbuf.h"
#include "util/net.h"
struct control_msg_queue CBUF(struct control_msg, 64); struct control_msg_queue CBUF(struct control_msg, 64);

View File

@@ -2,6 +2,7 @@
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include <libavutil/time.h> #include <libavutil/time.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_events.h> #include <SDL2/SDL_events.h>
#include <SDL2/SDL_mutex.h> #include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h> #include <SDL2/SDL_thread.h>
@@ -9,11 +10,12 @@
#include "config.h" #include "config.h"
#include "compat.h" #include "compat.h"
#include "buffer_util.h"
#include "events.h" #include "events.h"
#include "lock_util.h"
#include "log.h"
#include "recorder.h" #include "recorder.h"
#include "video_buffer.h" #include "video_buffer.h"
#include "util/buffer_util.h"
#include "util/log.h"
// set the decoded frame as ready for rendering, and notify // set the decoded frame as ready for rendering, and notify
static void static void

View File

@@ -1,7 +1,7 @@
#include "device.h" #include "device.h"
#include "config.h" #include "config.h"
#include "util/log.h" #include "log.h"
bool bool
device_read_info(socket_t device_socket, char *device_name, struct size *size) { device_read_info(socket_t device_socket, char *device_name, struct size *size) {

View File

@@ -5,7 +5,7 @@
#include "config.h" #include "config.h"
#include "common.h" #include "common.h"
#include "util/net.h" #include "net.h"
#define DEVICE_NAME_FIELD_LENGTH 64 #define DEVICE_NAME_FIELD_LENGTH 64

View File

@@ -1,10 +1,11 @@
#include "device_msg.h" #include "device_msg.h"
#include <string.h> #include <string.h>
#include <SDL2/SDL_assert.h>
#include "config.h" #include "config.h"
#include "util/buffer_util.h" #include "buffer_util.h"
#include "util/log.h" #include "log.h"
ssize_t ssize_t
device_msg_deserialize(const unsigned char *buf, size_t len, device_msg_deserialize(const unsigned char *buf, size_t len,

View File

@@ -5,7 +5,7 @@
#define MAP(FROM, TO) case FROM: *to = TO; return true #define MAP(FROM, TO) case FROM: *to = TO; return true
#define FAIL default: return false #define FAIL default: return false
bool static bool
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
switch (from) { switch (from) {
MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN); MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN);
@@ -33,7 +33,7 @@ autocomplete_metastate(enum android_metastate metastate) {
return metastate; return metastate;
} }
enum android_metastate static enum android_metastate
convert_meta_state(SDL_Keymod mod) { convert_meta_state(SDL_Keymod mod) {
enum android_metastate metastate = 0; enum android_metastate metastate = 0;
if (mod & KMOD_LSHIFT) { if (mod & KMOD_LSHIFT) {
@@ -74,9 +74,8 @@ convert_meta_state(SDL_Keymod mod) {
return autocomplete_metastate(metastate); return autocomplete_metastate(metastate);
} }
bool static bool
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
bool prefer_text) {
switch (from) { switch (from) {
MAP(SDLK_RETURN, AKEYCODE_ENTER); MAP(SDLK_RETURN, AKEYCODE_ENTER);
MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER); MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER);
@@ -93,12 +92,6 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN); MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN);
MAP(SDLK_UP, AKEYCODE_DPAD_UP); MAP(SDLK_UP, AKEYCODE_DPAD_UP);
} }
if (prefer_text) {
// do not forward alpha and space key events
return false;
}
if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) { if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) {
return false; return false;
} }
@@ -135,7 +128,7 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
} }
} }
enum android_motionevent_buttons static enum android_motionevent_buttons
convert_mouse_buttons(uint32_t state) { convert_mouse_buttons(uint32_t state) {
enum android_motionevent_buttons buttons = 0; enum android_motionevent_buttons buttons = 0;
if (state & SDL_BUTTON_LMASK) { if (state & SDL_BUTTON_LMASK) {
@@ -157,6 +150,24 @@ convert_mouse_buttons(uint32_t state) {
} }
bool bool
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
return false;
}
uint16_t mod = from->keysym.mod;
if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) {
return false;
}
to->inject_keycode.metastate = convert_meta_state(mod);
return true;
}
static bool
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
switch (from) { switch (from) {
MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN);
@@ -166,6 +177,41 @@ convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
} }
bool bool
convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) {
return false;
}
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
to->inject_touch_event.position.screen_size = screen_size;
to->inject_touch_event.position.point.x = from->x;
to->inject_touch_event.position.point.y = from->y;
to->inject_touch_event.pressure = 1.f;
to->inject_touch_event.buttons =
convert_mouse_buttons(SDL_BUTTON(from->button));
return true;
}
bool
convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE;
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
to->inject_touch_event.position.screen_size = screen_size;
to->inject_touch_event.position.point.x = from->x;
to->inject_touch_event.position.point.y = from->y;
to->inject_touch_event.pressure = 1.f;
to->inject_touch_event.buttons = convert_mouse_buttons(from->state);
return true;
}
static bool
convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) {
switch (from) { switch (from) {
MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE); MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE);
@@ -174,3 +220,39 @@ convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) {
FAIL; FAIL;
} }
} }
bool
convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
if (!convert_touch_action(from->type, &to->inject_touch_event.action)) {
return false;
}
to->inject_touch_event.pointer_id = from->fingerId;
to->inject_touch_event.position.screen_size = screen_size;
// SDL touch event coordinates are normalized in the range [0; 1]
to->inject_touch_event.position.point.x = from->x * screen_size.width;
to->inject_touch_event.position.point.y = from->y * screen_size.height;
to->inject_touch_event.pressure = from->pressure;
to->inject_touch_event.buttons = 0;
return true;
}
bool
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
to->inject_scroll_event.position = position;
int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
// SDL behavior seems inconsistent between horizontal and vertical scrolling
// so reverse the horizontal
// <https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>
to->inject_scroll_event.hscroll = -mul * from->x;
to->inject_scroll_event.vscroll = mul * from->y;
return true;
}

View File

@@ -7,23 +7,36 @@
#include "config.h" #include "config.h"
#include "control_msg.h" #include "control_msg.h"
bool struct complete_mouse_motion_event {
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to); SDL_MouseMotionEvent *mouse_motion_event;
struct size screen_size;
};
enum android_metastate struct complete_mouse_wheel_event {
convert_meta_state(SDL_Keymod mod); SDL_MouseWheelEvent *mouse_wheel_event;
struct point position;
};
bool bool
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to);
bool prefer_text);
enum android_motionevent_buttons
convert_mouse_buttons(uint32_t state);
bool bool
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to); convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size,
struct control_msg *to);
// the video size may be different from the real device size, so we need the
// size to which the absolute position apply, to scale it accordingly
bool
convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size,
struct control_msg *to);
bool bool
convert_touch_action(SDL_EventType from, enum android_motionevent_action *to); convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size,
struct control_msg *to);
// on Android, a scroll event requires the current mouse position
bool
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position,
struct control_msg *to);
#endif #endif

View File

@@ -1,12 +1,12 @@
#include "file_handler.h" #include "file_handler.h"
#include <assert.h>
#include <string.h> #include <string.h>
#include <SDL2/SDL_assert.h>
#include "config.h" #include "config.h"
#include "command.h" #include "command.h"
#include "util/lock.h" #include "lock_util.h"
#include "util/log.h" #include "log.h"
#define DEFAULT_PUSH_TARGET "/sdcard/" #define DEFAULT_PUSH_TARGET "/sdcard/"
@@ -120,8 +120,7 @@ run_file_handler(void *data) {
} }
struct file_handler_request req; struct file_handler_request req;
bool non_empty = cbuf_take(&file_handler->queue, &req); bool non_empty = cbuf_take(&file_handler->queue, &req);
assert(non_empty); SDL_assert(non_empty);
(void) non_empty;
process_t process; process_t process;
if (req.action == ACTION_INSTALL_APK) { if (req.action == ACTION_INSTALL_APK) {

View File

@@ -6,8 +6,8 @@
#include <SDL2/SDL_thread.h> #include <SDL2/SDL_thread.h>
#include "config.h" #include "config.h"
#include "cbuf.h"
#include "command.h" #include "command.h"
#include "util/cbuf.h"
typedef enum { typedef enum {
ACTION_INSTALL_APK, ACTION_INSTALL_APK,

View File

@@ -1,11 +1,11 @@
#include "fps_counter.h" #include "fps_counter.h"
#include <assert.h> #include <SDL2/SDL_assert.h>
#include <SDL2/SDL_timer.h> #include <SDL2/SDL_timer.h>
#include "config.h" #include "config.h"
#include "util/lock.h" #include "lock_util.h"
#include "util/log.h" #include "log.h"
#define FPS_COUNTER_INTERVAL_MS 1000 #define FPS_COUNTER_INTERVAL_MS 1000
@@ -77,7 +77,7 @@ run_fps_counter(void *data) {
uint32_t now = SDL_GetTicks(); uint32_t now = SDL_GetTicks();
check_interval_expired(counter, now); check_interval_expired(counter, now);
assert(counter->next_timestamp > now); SDL_assert(counter->next_timestamp > now);
uint32_t remaining = counter->next_timestamp - now; uint32_t remaining = counter->next_timestamp - now;
// ignore the reason (timeout or signaled), we just loop anyway // ignore the reason (timeout or signaled), we just loop anyway

View File

@@ -1,11 +1,11 @@
#include "input_manager.h" #include "input_manager.h"
#include <assert.h> #include <SDL2/SDL_assert.h>
#include "config.h" #include "config.h"
#include "event_converter.h" #include "event_converter.h"
#include "util/lock.h" #include "lock_util.h"
#include "util/log.h" #include "log.h"
// Convert window coordinates (as provided by SDL_GetMouseState() to renderer // Convert window coordinates (as provided by SDL_GetMouseState() to renderer
// coordinates (as provided in SDL mouse events) // coordinates (as provided in SDL mouse events)
@@ -211,28 +211,15 @@ clipboard_paste(struct controller *controller) {
} }
} }
static void
rotate_device(struct controller *controller) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_ROTATE_DEVICE;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request device rotation");
}
}
void void
input_manager_process_text_input(struct input_manager *im, input_manager_process_text_input(struct input_manager *input_manager,
const SDL_TextInputEvent *event) { const SDL_TextInputEvent *event) {
if (!im->prefer_text) {
char c = event->text[0]; char c = event->text[0];
if (isalpha(c) || c == ' ') { if (isalpha(c) || c == ' ') {
assert(event->text[1] == '\0'); SDL_assert(event->text[1] == '\0');
// letters and space are handled as raw key event // letters and space are handled as raw key event
return; return;
} }
}
struct control_msg msg; struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
msg.inject_text.text = SDL_strdup(event->text); msg.inject_text.text = SDL_strdup(event->text);
@@ -240,34 +227,14 @@ input_manager_process_text_input(struct input_manager *im,
LOGW("Could not strdup input text"); LOGW("Could not strdup input text");
return; return;
} }
if (!controller_push_msg(im->controller, &msg)) { if (!controller_push_msg(input_manager->controller, &msg)) {
SDL_free(msg.inject_text.text); SDL_free(msg.inject_text.text);
LOGW("Could not request 'inject text'"); LOGW("Could not request 'inject text'");
} }
} }
static bool
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
bool prefer_text) {
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
return false;
}
uint16_t mod = from->keysym.mod;
if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod,
prefer_text)) {
return false;
}
to->inject_keycode.metastate = convert_meta_state(mod);
return true;
}
void void
input_manager_process_key(struct input_manager *im, input_manager_process_key(struct input_manager *input_manager,
const SDL_KeyboardEvent *event, const SDL_KeyboardEvent *event,
bool control) { bool control) {
// control: indicates the state of the command-line option --no-control // control: indicates the state of the command-line option --no-control
@@ -294,7 +261,7 @@ input_manager_process_key(struct input_manager *im,
return; return;
} }
struct controller *controller = im->controller; struct controller *controller = input_manager->controller;
// capture all Ctrl events // capture all Ctrl events
if (ctrl || cmd) { if (ctrl || cmd) {
@@ -369,23 +336,23 @@ input_manager_process_key(struct input_manager *im,
return; return;
case SDLK_f: case SDLK_f:
if (!shift && cmd && !repeat && down) { if (!shift && cmd && !repeat && down) {
screen_switch_fullscreen(im->screen); screen_switch_fullscreen(input_manager->screen);
} }
return; return;
case SDLK_x: case SDLK_x:
if (!shift && cmd && !repeat && down) { if (!shift && cmd && !repeat && down) {
screen_resize_to_fit(im->screen); screen_resize_to_fit(input_manager->screen);
} }
return; return;
case SDLK_g: case SDLK_g:
if (!shift && cmd && !repeat && down) { if (!shift && cmd && !repeat && down) {
screen_resize_to_pixel_perfect(im->screen); screen_resize_to_pixel_perfect(input_manager->screen);
} }
return; return;
case SDLK_i: case SDLK_i:
if (!shift && cmd && !repeat && down) { if (!shift && cmd && !repeat && down) {
struct fps_counter *fps_counter = struct fps_counter *fps_counter =
im->video_buffer->fps_counter; input_manager->video_buffer->fps_counter;
switch_fps_counter_state(fps_counter); switch_fps_counter_state(fps_counter);
} }
return; return;
@@ -398,11 +365,6 @@ input_manager_process_key(struct input_manager *im,
} }
} }
return; return;
case SDLK_r:
if (control && cmd && !shift && !repeat && down) {
rotate_device(controller);
}
return;
} }
return; return;
@@ -413,30 +375,15 @@ input_manager_process_key(struct input_manager *im,
} }
struct control_msg msg; struct control_msg msg;
if (convert_input_key(event, &msg, im->prefer_text)) { if (convert_input_key(event, &msg)) {
if (!controller_push_msg(controller, &msg)) { if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'inject keycode'"); LOGW("Could not request 'inject keycode'");
} }
} }
} }
static bool
convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE;
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
to->inject_touch_event.position.screen_size = screen->frame_size;
to->inject_touch_event.position.point.x = from->x;
to->inject_touch_event.position.point.y = from->y;
to->inject_touch_event.pressure = 1.f;
to->inject_touch_event.buttons = convert_mouse_buttons(from->state);
return true;
}
void void
input_manager_process_mouse_motion(struct input_manager *im, input_manager_process_mouse_motion(struct input_manager *input_manager,
const SDL_MouseMotionEvent *event) { const SDL_MouseMotionEvent *event) {
if (!event->state) { if (!event->state) {
// do not send motion events when no button is pressed // do not send motion events when no button is pressed
@@ -447,74 +394,33 @@ input_manager_process_mouse_motion(struct input_manager *im,
return; return;
} }
struct control_msg msg; struct control_msg msg;
if (convert_mouse_motion(event, im->screen, &msg)) { if (convert_mouse_motion(event, input_manager->screen->frame_size, &msg)) {
if (!controller_push_msg(im->controller, &msg)) { if (!controller_push_msg(input_manager->controller, &msg)) {
LOGW("Could not request 'inject mouse motion event'"); LOGW("Could not request 'inject mouse motion event'");
} }
} }
} }
static bool
convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
if (!convert_touch_action(from->type, &to->inject_touch_event.action)) {
return false;
}
struct size frame_size = screen->frame_size;
to->inject_touch_event.pointer_id = from->fingerId;
to->inject_touch_event.position.screen_size = frame_size;
// SDL touch event coordinates are normalized in the range [0; 1]
to->inject_touch_event.position.point.x = from->x * frame_size.width;
to->inject_touch_event.position.point.y = from->y * frame_size.height;
to->inject_touch_event.pressure = from->pressure;
to->inject_touch_event.buttons = 0;
return true;
}
void void
input_manager_process_touch(struct input_manager *im, input_manager_process_touch(struct input_manager *input_manager,
const SDL_TouchFingerEvent *event) { const SDL_TouchFingerEvent *event) {
struct control_msg msg; struct control_msg msg;
if (convert_touch(event, im->screen, &msg)) { if (convert_touch(event, input_manager->screen->frame_size, &msg)) {
if (!controller_push_msg(im->controller, &msg)) { if (!controller_push_msg(input_manager->controller, &msg)) {
LOGW("Could not request 'inject touch event'"); LOGW("Could not request 'inject touch event'");
} }
} }
} }
static bool static bool
is_outside_device_screen(struct input_manager *im, int x, int y) is_outside_device_screen(struct input_manager *input_manager, int x, int y)
{ {
return x < 0 || x >= im->screen->frame_size.width || return x < 0 || x >= input_manager->screen->frame_size.width ||
y < 0 || y >= im->screen->frame_size.height; y < 0 || y >= input_manager->screen->frame_size.height;
}
static bool
convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) {
return false;
}
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
to->inject_touch_event.position.screen_size = screen->frame_size;
to->inject_touch_event.position.point.x = from->x;
to->inject_touch_event.position.point.y = from->y;
to->inject_touch_event.pressure = 1.f;
to->inject_touch_event.buttons =
convert_mouse_buttons(SDL_BUTTON(from->button));
return true;
} }
void void
input_manager_process_mouse_button(struct input_manager *im, input_manager_process_mouse_button(struct input_manager *input_manager,
const SDL_MouseButtonEvent *event, const SDL_MouseButtonEvent *event,
bool control) { bool control) {
if (event->which == SDL_TOUCH_MOUSEID) { if (event->which == SDL_TOUCH_MOUSEID) {
@@ -523,19 +429,19 @@ input_manager_process_mouse_button(struct input_manager *im,
} }
if (event->type == SDL_MOUSEBUTTONDOWN) { if (event->type == SDL_MOUSEBUTTONDOWN) {
if (control && event->button == SDL_BUTTON_RIGHT) { if (control && event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(im->controller); press_back_or_turn_screen_on(input_manager->controller);
return; return;
} }
if (control && event->button == SDL_BUTTON_MIDDLE) { if (control && event->button == SDL_BUTTON_MIDDLE) {
action_home(im->controller, ACTION_DOWN | ACTION_UP); action_home(input_manager->controller, ACTION_DOWN | ACTION_UP);
return; return;
} }
// double-click on black borders resize to fit the device screen // double-click on black borders resize to fit the device screen
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
bool outside = bool outside =
is_outside_device_screen(im, event->x, event->y); is_outside_device_screen(input_manager, event->x, event->y);
if (outside) { if (outside) {
screen_resize_to_fit(im->screen); screen_resize_to_fit(input_manager->screen);
return; return;
} }
} }
@@ -547,36 +453,23 @@ input_manager_process_mouse_button(struct input_manager *im,
} }
struct control_msg msg; struct control_msg msg;
if (convert_mouse_button(event, im->screen, &msg)) { if (convert_mouse_button(event, input_manager->screen->frame_size, &msg)) {
if (!controller_push_msg(im->controller, &msg)) { if (!controller_push_msg(input_manager->controller, &msg)) {
LOGW("Could not request 'inject mouse button event'"); LOGW("Could not request 'inject mouse button event'");
} }
} }
} }
static bool
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
struct control_msg *to) {
struct position position = {
.screen_size = screen->frame_size,
.point = get_mouse_point(screen),
};
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
to->inject_scroll_event.position = position;
to->inject_scroll_event.hscroll = from->x;
to->inject_scroll_event.vscroll = from->y;
return true;
}
void void
input_manager_process_mouse_wheel(struct input_manager *im, input_manager_process_mouse_wheel(struct input_manager *input_manager,
const SDL_MouseWheelEvent *event) { const SDL_MouseWheelEvent *event) {
struct position position = {
.screen_size = input_manager->screen->frame_size,
.point = get_mouse_point(input_manager->screen),
};
struct control_msg msg; struct control_msg msg;
if (convert_mouse_wheel(event, im->screen, &msg)) { if (convert_mouse_wheel(event, position, &msg)) {
if (!controller_push_msg(im->controller, &msg)) { if (!controller_push_msg(input_manager->controller, &msg)) {
LOGW("Could not request 'inject mouse wheel event'"); LOGW("Could not request 'inject mouse wheel event'");
} }
} }

View File

@@ -14,33 +14,32 @@ struct input_manager {
struct controller *controller; struct controller *controller;
struct video_buffer *video_buffer; struct video_buffer *video_buffer;
struct screen *screen; struct screen *screen;
bool prefer_text;
}; };
void void
input_manager_process_text_input(struct input_manager *im, input_manager_process_text_input(struct input_manager *input_manager,
const SDL_TextInputEvent *event); const SDL_TextInputEvent *event);
void void
input_manager_process_key(struct input_manager *im, input_manager_process_key(struct input_manager *input_manager,
const SDL_KeyboardEvent *event, const SDL_KeyboardEvent *event,
bool control); bool control);
void void
input_manager_process_mouse_motion(struct input_manager *im, input_manager_process_mouse_motion(struct input_manager *input_manager,
const SDL_MouseMotionEvent *event); const SDL_MouseMotionEvent *event);
void void
input_manager_process_touch(struct input_manager *im, input_manager_process_touch(struct input_manager *input_manager,
const SDL_TouchFingerEvent *event); const SDL_TouchFingerEvent *event);
void void
input_manager_process_mouse_button(struct input_manager *im, input_manager_process_mouse_button(struct input_manager *input_manager,
const SDL_MouseButtonEvent *event, const SDL_MouseButtonEvent *event,
bool control); bool control);
void void
input_manager_process_mouse_wheel(struct input_manager *im, input_manager_process_mouse_wheel(struct input_manager *input_manager,
const SDL_MouseWheelEvent *event); const SDL_MouseWheelEvent *event);
#endif #endif

52
app/src/lock_util.h Normal file
View File

@@ -0,0 +1,52 @@
#ifndef LOCKUTIL_H
#define LOCKUTIL_H
#include <stdint.h>
#include <SDL2/SDL_mutex.h>
#include "config.h"
#include "log.h"
static inline void
mutex_lock(SDL_mutex *mutex) {
if (SDL_LockMutex(mutex)) {
LOGC("Could not lock mutex");
abort();
}
}
static inline void
mutex_unlock(SDL_mutex *mutex) {
if (SDL_UnlockMutex(mutex)) {
LOGC("Could not unlock mutex");
abort();
}
}
static inline void
cond_wait(SDL_cond *cond, SDL_mutex *mutex) {
if (SDL_CondWait(cond, mutex)) {
LOGC("Could not wait on condition");
abort();
}
}
static inline int
cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) {
int r = SDL_CondWaitTimeout(cond, mutex, ms);
if (r < 0) {
LOGC("Could not wait on condition with timeout");
abort();
}
return r;
}
static inline void
cond_signal(SDL_cond *cond) {
if (SDL_CondSignal(cond)) {
LOGC("Could not signal a condition");
abort();
}
}
#endif

View File

@@ -10,13 +10,28 @@
#include "config.h" #include "config.h"
#include "compat.h" #include "compat.h"
#include "log.h"
#include "recorder.h" #include "recorder.h"
#include "util/log.h"
struct args { struct args {
struct scrcpy_options opts; const char *serial;
const char *crop;
const char *record_filename;
const char *window_title;
const char *push_target;
enum recorder_format record_format;
bool fullscreen;
bool no_control;
bool no_display;
bool help; bool help;
bool version; bool version;
bool show_touches;
uint16_t port;
uint16_t max_size;
uint32_t bit_rate;
bool always_on_top;
bool turn_screen_off;
bool render_expired_frames;
}; };
static void usage(const char *arg0) { static void usage(const char *arg0) {
@@ -30,15 +45,12 @@ static void usage(const char *arg0) {
"\n" "\n"
"Options:\n" "Options:\n"
"\n" "\n"
" --always-on-top\n"
" Make scrcpy window always on top (above other windows).\n"
"\n"
" -b, --bit-rate value\n" " -b, --bit-rate value\n"
" Encode the video at the given bit-rate, expressed in bits/s.\n" " Encode the video at the given bit-rate, expressed in bits/s.\n"
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
" Default is %d.\n" " Default is %d.\n"
"\n" "\n"
" --crop width:height:x:y\n" " -c, --crop width:height:x:y\n"
" Crop the device screen on the server.\n" " Crop the device screen on the server.\n"
" The values are expressed in the device natural orientation\n" " The values are expressed in the device natural orientation\n"
" (typically, portrait for a phone, landscape for a tablet).\n" " (typically, portrait for a phone, landscape for a tablet).\n"
@@ -47,13 +59,12 @@ static void usage(const char *arg0) {
" -f, --fullscreen\n" " -f, --fullscreen\n"
" Start in fullscreen.\n" " Start in fullscreen.\n"
"\n" "\n"
" -F, --record-format format\n"
" Force recording format (either mp4 or mkv).\n"
"\n"
" -h, --help\n" " -h, --help\n"
" Print this help.\n" " Print this help.\n"
"\n" "\n"
" --max-fps value\n"
" Limit the frame rate of screen capture (only supported on\n"
" devices with Android >= 10).\n"
"\n"
" -m, --max-size value\n" " -m, --max-size value\n"
" Limit both the width and height of the video to value. The\n" " Limit both the width and height of the video to value. The\n"
" other dimension is computed so that the device aspect-ratio\n" " other dimension is computed so that the device aspect-ratio\n"
@@ -71,13 +82,6 @@ static void usage(const char *arg0) {
" Set the TCP port the client listens on.\n" " Set the TCP port the client listens on.\n"
" Default is %d.\n" " Default is %d.\n"
"\n" "\n"
" --prefer-text\n"
" Inject alpha characters and space as text events instead of\n"
" key events.\n"
" This avoids issues when combining multiple keys to enter a\n"
" special character, but breaks the expected behavior of alpha\n"
" keys in games (typically WASD).\n"
"\n"
" --push-target path\n" " --push-target path\n"
" Set the target directory for pushing files to the device by\n" " Set the target directory for pushing files to the device by\n"
" drag & drop. It is passed as-is to \"adb push\".\n" " drag & drop. It is passed as-is to \"adb push\".\n"
@@ -85,12 +89,9 @@ static void usage(const char *arg0) {
"\n" "\n"
" -r, --record file.mp4\n" " -r, --record file.mp4\n"
" Record screen to file.\n" " Record screen to file.\n"
" The format is determined by the --record-format option if\n" " The format is determined by the -F/--record-format option if\n"
" set, or by the file extension (.mp4 or .mkv).\n" " set, or by the file extension (.mp4 or .mkv).\n"
"\n" "\n"
" --record-format format\n"
" Force recording format (either mp4 or mkv).\n"
"\n"
" --render-expired-frames\n" " --render-expired-frames\n"
" By default, to minimize latency, scrcpy always renders the\n" " By default, to minimize latency, scrcpy always renders the\n"
" last available decoded frame, and drops any previous ones.\n" " last available decoded frame, and drops any previous ones.\n"
@@ -108,31 +109,15 @@ static void usage(const char *arg0) {
" Enable \"show touches\" on start, disable on quit.\n" " Enable \"show touches\" on start, disable on quit.\n"
" It only shows physical touches (not clicks from scrcpy).\n" " It only shows physical touches (not clicks from scrcpy).\n"
"\n" "\n"
" -T, --always-on-top\n"
" Make scrcpy window always on top (above other windows).\n"
"\n"
" -v, --version\n" " -v, --version\n"
" Print the version of scrcpy.\n" " Print the version of scrcpy.\n"
"\n" "\n"
" --window-borderless\n"
" Disable window decorations (display borderless window).\n"
"\n"
" --window-title text\n" " --window-title text\n"
" Set a custom window title.\n" " Set a custom window title.\n"
"\n" "\n"
" --window-x value\n"
" Set the initial window horizontal position.\n"
" Default is -1 (automatic).\n"
"\n"
" --window-y value\n"
" Set the initial window vertical position.\n"
" Default is -1 (automatic).\n"
"\n"
" --window-width value\n"
" Set the initial window width.\n"
" Default is 0 (automatic).\n"
"\n"
" --window-height value\n"
" Set the initial window width.\n"
" Default is 0 (automatic).\n"
"\n"
"Shortcuts:\n" "Shortcuts:\n"
"\n" "\n"
" " CTRL_OR_CMD "+f\n" " " CTRL_OR_CMD "+f\n"
@@ -175,9 +160,6 @@ static void usage(const char *arg0) {
" " CTRL_OR_CMD "+o\n" " " CTRL_OR_CMD "+o\n"
" turn device screen off (keep mirroring)\n" " turn device screen off (keep mirroring)\n"
"\n" "\n"
" " CTRL_OR_CMD "+r\n"
" rotate device screen\n"
"\n"
" " CTRL_OR_CMD "+n\n" " " CTRL_OR_CMD "+n\n"
" expand notification panel\n" " expand notification panel\n"
"\n" "\n"
@@ -246,7 +228,7 @@ parse_bit_rate(char *optarg, uint32_t *bit_rate) {
return false; return false;
} }
} }
if (value < 0 || ((uint32_t) -1) / mul < (unsigned long) value) { if (value < 0 || ((uint32_t) -1) / mul < value) {
LOGE("Bitrate must be positive and less than 2^32: %s", optarg); LOGE("Bitrate must be positive and less than 2^32: %s", optarg);
return false; return false;
} }
@@ -276,75 +258,11 @@ parse_max_size(char *optarg, uint16_t *max_size) {
return true; return true;
} }
static bool
parse_max_fps(const char *optarg, uint16_t *max_fps) {
char *endptr;
if (*optarg == '\0') {
LOGE("Max FPS parameter is empty");
return false;
}
long value = strtol(optarg, &endptr, 0);
if (*endptr != '\0') {
LOGE("Invalid max FPS: %s", optarg);
return false;
}
if (value & ~0xffff) {
// in practice, it should not be higher than 60
LOGE("Max FPS value is invalid: %ld", value);
return false;
}
*max_fps = (uint16_t) value;
return true;
}
static bool
parse_window_position(char *optarg, int16_t *position) {
char *endptr;
if (*optarg == '\0') {
LOGE("Window position parameter is empty");
return false;
}
long value = strtol(optarg, &endptr, 0);
if (*endptr != '\0') {
LOGE("Invalid window position: %s", optarg);
return false;
}
if (value < -1 || value > 0x7fff) {
LOGE("Window position must be between -1 and 32767: %ld", value);
return false;
}
*position = (int16_t) value;
return true;
}
static bool
parse_window_dimension(char *optarg, uint16_t *dimension) {
char *endptr;
if (*optarg == '\0') {
LOGE("Window dimension parameter is empty");
return false;
}
long value = strtol(optarg, &endptr, 0);
if (*endptr != '\0') {
LOGE("Invalid window dimension: %s", optarg);
return false;
}
if (value & ~0xffff) {
LOGE("Window position must be between 0 and 65535: %ld", value);
return false;
}
*dimension = (uint16_t) value;
return true;
}
static bool static bool
parse_port(char *optarg, uint16_t *port) { parse_port(char *optarg, uint16_t *port) {
char *endptr; char *endptr;
if (*optarg == '\0') { if (*optarg == '\0') {
LOGE("Port parameter is empty"); LOGE("Invalid port parameter is empty");
return false; return false;
} }
long value = strtol(optarg, &endptr, 0); long value = strtol(optarg, &endptr, 0);
@@ -394,157 +312,98 @@ guess_record_format(const char *filename) {
#define OPT_RENDER_EXPIRED_FRAMES 1000 #define OPT_RENDER_EXPIRED_FRAMES 1000
#define OPT_WINDOW_TITLE 1001 #define OPT_WINDOW_TITLE 1001
#define OPT_PUSH_TARGET 1002 #define OPT_PUSH_TARGET 1002
#define OPT_ALWAYS_ON_TOP 1003
#define OPT_CROP 1004
#define OPT_RECORD_FORMAT 1005
#define OPT_PREFER_TEXT 1006
#define OPT_WINDOW_X 1007
#define OPT_WINDOW_Y 1008
#define OPT_WINDOW_WIDTH 1009
#define OPT_WINDOW_HEIGHT 1010
#define OPT_WINDOW_BORDERLESS 1011
#define OPT_MAX_FPS 1012
static bool static bool
parse_args(struct args *args, int argc, char *argv[]) { parse_args(struct args *args, int argc, char *argv[]) {
static const struct option long_options[] = { static const struct option long_options[] = {
{"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, {"always-on-top", no_argument, NULL, 'T'},
{"bit-rate", required_argument, NULL, 'b'}, {"bit-rate", required_argument, NULL, 'b'},
{"crop", required_argument, NULL, OPT_CROP}, {"crop", required_argument, NULL, 'c'},
{"fullscreen", no_argument, NULL, 'f'}, {"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{"max-fps", required_argument, NULL, OPT_MAX_FPS},
{"max-size", required_argument, NULL, 'm'}, {"max-size", required_argument, NULL, 'm'},
{"no-control", no_argument, NULL, 'n'}, {"no-control", no_argument, NULL, 'n'},
{"no-display", no_argument, NULL, 'N'}, {"no-display", no_argument, NULL, 'N'},
{"port", required_argument, NULL, 'p'}, {"port", required_argument, NULL, 'p'},
{"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"push-target", required_argument, NULL,
OPT_PUSH_TARGET},
{"record", required_argument, NULL, 'r'}, {"record", required_argument, NULL, 'r'},
{"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, {"record-format", required_argument, NULL, 'F'},
{"render-expired-frames", no_argument, NULL, {"render-expired-frames", no_argument, NULL,
OPT_RENDER_EXPIRED_FRAMES}, OPT_RENDER_EXPIRED_FRAMES},
{"serial", required_argument, NULL, 's'}, {"serial", required_argument, NULL, 's'},
{"show-touches", no_argument, NULL, 't'}, {"show-touches", no_argument, NULL, 't'},
{"turn-screen-off", no_argument, NULL, 'S'}, {"turn-screen-off", no_argument, NULL, 'S'},
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
{"version", no_argument, NULL, 'v'}, {"version", no_argument, NULL, 'v'},
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, {"window-title", required_argument, NULL,
{"window-x", required_argument, NULL, OPT_WINDOW_X}, OPT_WINDOW_TITLE},
{"window-y", required_argument, NULL, OPT_WINDOW_Y},
{"window-width", required_argument, NULL, OPT_WINDOW_WIDTH},
{"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT},
{"window-borderless", no_argument, NULL,
OPT_WINDOW_BORDERLESS},
{NULL, 0, NULL, 0 }, {NULL, 0, NULL, 0 },
}; };
struct scrcpy_options *opts = &args->opts;
int c; int c;
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options,
NULL)) != -1) { NULL)) != -1) {
switch (c) { switch (c) {
case 'b': case 'b':
if (!parse_bit_rate(optarg, &opts->bit_rate)) { if (!parse_bit_rate(optarg, &args->bit_rate)) {
return false; return false;
} }
break; break;
case 'c': case 'c':
LOGW("Deprecated option -c. Use --crop instead."); args->crop = optarg;
// fall through
case OPT_CROP:
opts->crop = optarg;
break; break;
case 'f': case 'f':
opts->fullscreen = true; args->fullscreen = true;
break; break;
case 'F': case 'F':
LOGW("Deprecated option -F. Use --record-format instead."); if (!parse_record_format(optarg, &args->record_format)) {
// fall through
case OPT_RECORD_FORMAT:
if (!parse_record_format(optarg, &opts->record_format)) {
return false; return false;
} }
break; break;
case 'h': case 'h':
args->help = true; args->help = true;
break; break;
case OPT_MAX_FPS:
if (!parse_max_fps(optarg, &opts->max_fps)) {
return false;
}
break;
case 'm': case 'm':
if (!parse_max_size(optarg, &opts->max_size)) { if (!parse_max_size(optarg, &args->max_size)) {
return false; return false;
} }
break; break;
case 'n': case 'n':
opts->control = false; args->no_control = true;
break; break;
case 'N': case 'N':
opts->display = false; args->no_display = true;
break; break;
case 'p': case 'p':
if (!parse_port(optarg, &opts->port)) { if (!parse_port(optarg, &args->port)) {
return false; return false;
} }
break; break;
case 'r': case 'r':
opts->record_filename = optarg; args->record_filename = optarg;
break; break;
case 's': case 's':
opts->serial = optarg; args->serial = optarg;
break; break;
case 'S': case 'S':
opts->turn_screen_off = true; args->turn_screen_off = true;
break; break;
case 't': case 't':
opts->show_touches = true; args->show_touches = true;
break; break;
case 'T': case 'T':
LOGW("Deprecated option -T. Use --always-on-top instead."); args->always_on_top = true;
// fall through
case OPT_ALWAYS_ON_TOP:
opts->always_on_top = true;
break; break;
case 'v': case 'v':
args->version = true; args->version = true;
break; break;
case OPT_RENDER_EXPIRED_FRAMES: case OPT_RENDER_EXPIRED_FRAMES:
opts->render_expired_frames = true; args->render_expired_frames = true;
break; break;
case OPT_WINDOW_TITLE: case OPT_WINDOW_TITLE:
opts->window_title = optarg; args->window_title = optarg;
break;
case OPT_WINDOW_X:
if (!parse_window_position(optarg, &opts->window_x)) {
return false;
}
break;
case OPT_WINDOW_Y:
if (!parse_window_position(optarg, &opts->window_y)) {
return false;
}
break;
case OPT_WINDOW_WIDTH:
if (!parse_window_dimension(optarg, &opts->window_width)) {
return false;
}
break;
case OPT_WINDOW_HEIGHT:
if (!parse_window_dimension(optarg, &opts->window_height)) {
return false;
}
break;
case OPT_WINDOW_BORDERLESS:
opts->window_borderless = true;
break; break;
case OPT_PUSH_TARGET: case OPT_PUSH_TARGET:
opts->push_target = optarg; args->push_target = optarg;
break;
case OPT_PREFER_TEXT:
opts->prefer_text = true;
break; break;
default: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
@@ -552,12 +411,12 @@ parse_args(struct args *args, int argc, char *argv[]) {
} }
} }
if (!opts->display && !opts->record_filename) { if (args->no_display && !args->record_filename) {
LOGE("-N/--no-display requires screen recording (-r/--record)"); LOGE("-N/--no-display requires screen recording (-r/--record)");
return false; return false;
} }
if (!opts->display && opts->fullscreen) { if (args->no_display && args->fullscreen) {
LOGE("-f/--fullscreen-window is incompatible with -N/--no-display"); LOGE("-f/--fullscreen-window is incompatible with -N/--no-display");
return false; return false;
} }
@@ -568,21 +427,21 @@ parse_args(struct args *args, int argc, char *argv[]) {
return false; return false;
} }
if (opts->record_format && !opts->record_filename) { if (args->record_format && !args->record_filename) {
LOGE("Record format specified without recording"); LOGE("Record format specified without recording");
return false; return false;
} }
if (opts->record_filename && !opts->record_format) { if (args->record_filename && !args->record_format) {
opts->record_format = guess_record_format(opts->record_filename); args->record_format = guess_record_format(args->record_filename);
if (!opts->record_format) { if (!args->record_format) {
LOGE("No format specified for \"%s\" (try with -F mkv)", LOGE("No format specified for \"%s\" (try with -F mkv)",
opts->record_filename); args->record_filename);
return false; return false;
} }
} }
if (!opts->control && opts->turn_screen_off) { if (args->no_control && args->turn_screen_off) {
LOGE("Could not request to turn screen off if control is disabled"); LOGE("Could not request to turn screen off if control is disabled");
return false; return false;
} }
@@ -599,11 +458,24 @@ main(int argc, char *argv[]) {
setbuf(stderr, NULL); setbuf(stderr, NULL);
#endif #endif
struct args args = { struct args args = {
.opts = SCRCPY_OPTIONS_DEFAULT, .serial = NULL,
.crop = NULL,
.record_filename = NULL,
.window_title = NULL,
.push_target = NULL,
.record_format = 0,
.help = false, .help = false,
.version = false, .version = false,
.show_touches = false,
.port = DEFAULT_LOCAL_PORT,
.max_size = DEFAULT_MAX_SIZE,
.bit_rate = DEFAULT_BIT_RATE,
.always_on_top = false,
.no_control = false,
.no_display = false,
.turn_screen_off = false,
.render_expired_frames = false,
}; };
if (!parse_args(&args, argc, argv)) { if (!parse_args(&args, argc, argv)) {
return 1; return 1;
} }
@@ -628,11 +500,29 @@ main(int argc, char *argv[]) {
return 1; return 1;
} }
#ifndef NDEBUG #ifdef BUILD_DEBUG
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
#endif #endif
int res = scrcpy(&args.opts) ? 0 : 1; struct scrcpy_options options = {
.serial = args.serial,
.crop = args.crop,
.port = args.port,
.record_filename = args.record_filename,
.window_title = args.window_title,
.push_target = args.push_target,
.record_format = args.record_format,
.max_size = args.max_size,
.bit_rate = args.bit_rate,
.show_touches = args.show_touches,
.fullscreen = args.fullscreen,
.always_on_top = args.always_on_top,
.control = !args.no_control,
.display = !args.no_display,
.turn_screen_off = args.turn_screen_off,
.render_expired_frames = args.render_expired_frames,
};
int res = scrcpy(&options) ? 0 : 1;
avformat_network_deinit(); // ignore failure avformat_network_deinit(); // ignore failure

View File

@@ -2,9 +2,9 @@
#ifndef QUEUE_H #ifndef QUEUE_H
#define QUEUE_H #define QUEUE_H
#include <assert.h>
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include <SDL2/SDL_assert.h>
#include "config.h" #include "config.h"
@@ -67,7 +67,7 @@
// type so that we can "return" it) // type so that we can "return" it)
#define queue_take(PQ, NEXTFIELD, PITEM) \ #define queue_take(PQ, NEXTFIELD, PITEM) \
(void) ({ \ (void) ({ \
assert(!queue_is_empty(PQ)); \ SDL_assert(!queue_is_empty(PQ)); \
*(PITEM) = (PQ)->first; \ *(PITEM) = (PQ)->first; \
(PQ)->first = (PQ)->first->NEXTFIELD; \ (PQ)->first = (PQ)->first->NEXTFIELD; \
}) })

View File

@@ -1,12 +1,12 @@
#include "receiver.h" #include "receiver.h"
#include <assert.h> #include <SDL2/SDL_assert.h>
#include <SDL2/SDL_clipboard.h> #include <SDL2/SDL_clipboard.h>
#include "config.h" #include "config.h"
#include "device_msg.h" #include "device_msg.h"
#include "util/lock.h" #include "lock_util.h"
#include "util/log.h" #include "log.h"
bool bool
receiver_init(struct receiver *receiver, socket_t control_socket) { receiver_init(struct receiver *receiver, socket_t control_socket) {
@@ -23,7 +23,7 @@ receiver_destroy(struct receiver *receiver) {
} }
static void static void
process_msg(struct device_msg *msg) { process_msg(struct receiver *receiver, struct device_msg *msg) {
switch (msg->type) { switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD: case DEVICE_MSG_TYPE_CLIPBOARD:
LOGI("Device clipboard copied"); LOGI("Device clipboard copied");
@@ -33,7 +33,7 @@ process_msg(struct device_msg *msg) {
} }
static ssize_t static ssize_t
process_msgs(const unsigned char *buf, size_t len) { process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) {
size_t head = 0; size_t head = 0;
for (;;) { for (;;) {
struct device_msg msg; struct device_msg msg;
@@ -45,11 +45,11 @@ process_msgs(const unsigned char *buf, size_t len) {
return head; return head;
} }
process_msg(&msg); process_msg(receiver, &msg);
device_msg_destroy(&msg); device_msg_destroy(&msg);
head += r; head += r;
assert(head <= len); SDL_assert(head <= len);
if (head == len) { if (head == len) {
return head; return head;
} }
@@ -64,7 +64,7 @@ run_receiver(void *data) {
size_t head = 0; size_t head = 0;
for (;;) { for (;;) {
assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE); SDL_assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE);
ssize_t r = net_recv(receiver->control_socket, buf, ssize_t r = net_recv(receiver->control_socket, buf,
DEVICE_MSG_SERIALIZED_MAX_SIZE - head); DEVICE_MSG_SERIALIZED_MAX_SIZE - head);
if (r <= 0) { if (r <= 0) {
@@ -72,7 +72,7 @@ run_receiver(void *data) {
break; break;
} }
ssize_t consumed = process_msgs(buf, r); ssize_t consumed = process_msgs(receiver, buf, r);
if (consumed == -1) { if (consumed == -1) {
// an error occurred // an error occurred
break; break;

View File

@@ -6,7 +6,7 @@
#include <SDL2/SDL_thread.h> #include <SDL2/SDL_thread.h>
#include "config.h" #include "config.h"
#include "util/net.h" #include "net.h"
// receive events from the device // receive events from the device
// managed by the controller // managed by the controller

View File

@@ -1,12 +1,12 @@
#include "recorder.h" #include "recorder.h"
#include <assert.h>
#include <libavutil/time.h> #include <libavutil/time.h>
#include <SDL2/SDL_assert.h>
#include "config.h" #include "config.h"
#include "compat.h" #include "compat.h"
#include "util/lock.h" #include "lock_util.h"
#include "util/log.h" #include "log.h"
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
@@ -116,7 +116,7 @@ recorder_get_format_name(enum recorder_format format) {
bool bool
recorder_open(struct recorder *recorder, const AVCodec *input_codec) { recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
const char *format_name = recorder_get_format_name(recorder->format); const char *format_name = recorder_get_format_name(recorder->format);
assert(format_name); SDL_assert(format_name);
const AVOutputFormat *format = find_muxer(format_name); const AVOutputFormat *format = find_muxer(format_name);
if (!format) { if (!format) {
LOGE("Could not find muxer"); LOGE("Could not find muxer");
@@ -174,16 +174,11 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
void void
recorder_close(struct recorder *recorder) { recorder_close(struct recorder *recorder) {
if (recorder->header_written) {
int ret = av_write_trailer(recorder->ctx); int ret = av_write_trailer(recorder->ctx);
if (ret < 0) { if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename); LOGE("Failed to write trailer to %s", recorder->filename);
recorder->failed = true; recorder->failed = true;
} }
} else {
// the recorded file is empty
recorder->failed = true;
}
avio_close(recorder->ctx->pb); avio_close(recorder->ctx->pb);
avformat_free_context(recorder->ctx); avformat_free_context(recorder->ctx);
@@ -301,12 +296,8 @@ run_recorder(void *data) {
continue; continue;
} }
// config packets have no PTS, we must ignore them
if (rec->packet.pts != AV_NOPTS_VALUE
&& previous->packet.pts != AV_NOPTS_VALUE) {
// we now know the duration of the previous packet // we now know the duration of the previous packet
previous->packet.duration = rec->packet.pts - previous->packet.pts; previous->packet.duration = rec->packet.pts - previous->packet.pts;
}
bool ok = recorder_write(recorder, &previous->packet); bool ok = recorder_write(recorder, &previous->packet);
record_packet_delete(previous); record_packet_delete(previous);
@@ -357,7 +348,7 @@ recorder_join(struct recorder *recorder) {
bool bool
recorder_push(struct recorder *recorder, const AVPacket *packet) { recorder_push(struct recorder *recorder, const AVPacket *packet) {
mutex_lock(recorder->mutex); mutex_lock(recorder->mutex);
assert(!recorder->stopped); SDL_assert(!recorder->stopped);
if (recorder->failed) { if (recorder->failed) {
// reject any new packet (this will stop the stream) // reject any new packet (this will stop the stream)

View File

@@ -8,11 +8,10 @@
#include "config.h" #include "config.h"
#include "common.h" #include "common.h"
#include "util/queue.h" #include "queue.h"
enum recorder_format { enum recorder_format {
RECORDER_FORMAT_AUTO, RECORDER_FORMAT_MP4 = 1,
RECORDER_FORMAT_MP4,
RECORDER_FORMAT_MKV, RECORDER_FORMAT_MKV,
}; };

View File

@@ -18,15 +18,15 @@
#include "file_handler.h" #include "file_handler.h"
#include "fps_counter.h" #include "fps_counter.h"
#include "input_manager.h" #include "input_manager.h"
#include "log.h"
#include "lock_util.h"
#include "net.h"
#include "recorder.h" #include "recorder.h"
#include "screen.h" #include "screen.h"
#include "server.h" #include "server.h"
#include "stream.h" #include "stream.h"
#include "tiny_xpm.h" #include "tiny_xpm.h"
#include "video_buffer.h" #include "video_buffer.h"
#include "util/lock.h"
#include "util/log.h"
#include "util/net.h"
static struct server server = SERVER_INITIALIZER; static struct server server = SERVER_INITIALIZER;
static struct screen screen = SCREEN_INITIALIZER; static struct screen screen = SCREEN_INITIALIZER;
@@ -42,7 +42,6 @@ static struct input_manager input_manager = {
.controller = &controller, .controller = &controller,
.video_buffer = &video_buffer, .video_buffer = &video_buffer,
.screen = &screen, .screen = &screen,
.prefer_text = false, // initialized later
}; };
// init SDL and set appropriate hints // init SDL and set appropriate hints
@@ -103,7 +102,6 @@ sdl_init_and_configure(bool display) {
// <https://stackoverflow.com/a/40693139/1987178> // <https://stackoverflow.com/a/40693139/1987178>
static int static int
event_watcher(void *data, SDL_Event *event) { event_watcher(void *data, SDL_Event *event) {
(void) data;
if (event->type == SDL_WINDOWEVENT if (event->type == SDL_WINDOWEVENT
&& event->window.event == SDL_WINDOWEVENT_RESIZED) { && event->window.event == SDL_WINDOWEVENT_RESIZED) {
// called from another thread, not very safe, but it's a workaround! // called from another thread, not very safe, but it's a workaround!
@@ -145,7 +143,14 @@ handle_event(SDL_Event *event, bool control) {
} }
break; break;
case SDL_WINDOWEVENT: case SDL_WINDOWEVENT:
screen_handle_window_event(&screen, &event->window); switch (event->window.event) {
case SDL_WINDOWEVENT_EXPOSED:
screen_render(&screen);
break;
case SDL_WINDOWEVENT_SIZE_CHANGED:
screen_window_resized(&screen);
break;
}
break; break;
case SDL_TEXTINPUT: case SDL_TEXTINPUT:
if (!control) { if (!control) {
@@ -202,7 +207,6 @@ handle_event(SDL_Event *event, bool control) {
static bool static bool
event_loop(bool display, bool control) { event_loop(bool display, bool control) {
(void) display;
#ifdef CONTINUOUS_RESIZING_WORKAROUND #ifdef CONTINUOUS_RESIZING_WORKAROUND
if (display) { if (display) {
SDL_AddEventWatch(event_watcher, NULL); SDL_AddEventWatch(event_watcher, NULL);
@@ -215,7 +219,6 @@ event_loop(bool display, bool control) {
case EVENT_RESULT_STOPPED_BY_USER: case EVENT_RESULT_STOPPED_BY_USER:
return true; return true;
case EVENT_RESULT_STOPPED_BY_EOS: case EVENT_RESULT_STOPPED_BY_EOS:
LOGW("Device disconnected");
return false; return false;
case EVENT_RESULT_CONTINUE: case EVENT_RESULT_CONTINUE:
break; break;
@@ -258,7 +261,6 @@ sdl_priority_from_av_level(int level) {
static void static void
av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
(void) avcl;
SDL_LogPriority priority = sdl_priority_from_av_level(level); SDL_LogPriority priority = sdl_priority_from_av_level(level);
if (priority == 0) { if (priority == 0) {
return; return;
@@ -283,7 +285,6 @@ scrcpy(const struct scrcpy_options *options) {
.local_port = options->port, .local_port = options->port,
.max_size = options->max_size, .max_size = options->max_size,
.bit_rate = options->bit_rate, .bit_rate = options->bit_rate,
.max_fps = options->max_fps,
.control = options->control, .control = options->control,
}; };
if (!server_start(&server, options->serial, &params)) { if (!server_start(&server, options->serial, &params)) {
@@ -391,10 +392,7 @@ scrcpy(const struct scrcpy_options *options) {
options->window_title ? options->window_title : device_name; options->window_title ? options->window_title : device_name;
if (!screen_init_rendering(&screen, window_title, frame_size, if (!screen_init_rendering(&screen, window_title, frame_size,
options->always_on_top, options->window_x, options->always_on_top)) {
options->window_y, options->window_width,
options->window_height,
options->window_borderless)) {
goto end; goto end;
} }
@@ -418,8 +416,6 @@ scrcpy(const struct scrcpy_options *options) {
show_touches_waited = true; show_touches_waited = true;
} }
input_manager.prefer_text = options->prefer_text;
ret = event_loop(options->display, options->control); ret = event_loop(options->display, options->control);
LOGD("quit..."); LOGD("quit...");

View File

@@ -3,10 +3,9 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <recorder.h>
#include "config.h" #include "config.h"
#include "input_manager.h"
#include "recorder.h"
struct scrcpy_options { struct scrcpy_options {
const char *serial; const char *serial;
@@ -18,11 +17,6 @@ struct scrcpy_options {
uint16_t port; uint16_t port;
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps;
int16_t window_x;
int16_t window_y;
uint16_t window_width;
uint16_t window_height;
bool show_touches; bool show_touches;
bool fullscreen; bool fullscreen;
bool always_on_top; bool always_on_top;
@@ -30,36 +24,8 @@ struct scrcpy_options {
bool display; bool display;
bool turn_screen_off; bool turn_screen_off;
bool render_expired_frames; bool render_expired_frames;
bool prefer_text;
bool window_borderless;
}; };
#define SCRCPY_OPTIONS_DEFAULT { \
.serial = NULL, \
.crop = NULL, \
.record_filename = NULL, \
.window_title = NULL, \
.push_target = NULL, \
.record_format = RECORDER_FORMAT_AUTO, \
.port = DEFAULT_LOCAL_PORT, \
.max_size = DEFAULT_LOCAL_PORT, \
.bit_rate = DEFAULT_BIT_RATE, \
.max_fps = 0, \
.window_x = -1, \
.window_y = -1, \
.window_width = 0, \
.window_height = 0, \
.show_touches = false, \
.fullscreen = false, \
.always_on_top = false, \
.control = true, \
.display = true, \
.turn_screen_off = false, \
.render_expired_frames = false, \
.prefer_text = false, \
.window_borderless = false, \
}
bool bool
scrcpy(const struct scrcpy_options *options); scrcpy(const struct scrcpy_options *options);

View File

@@ -1,6 +1,5 @@
#include "screen.h" #include "screen.h"
#include <assert.h>
#include <string.h> #include <string.h>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
@@ -8,10 +7,10 @@
#include "common.h" #include "common.h"
#include "compat.h" #include "compat.h"
#include "icon.xpm" #include "icon.xpm"
#include "lock_util.h"
#include "log.h"
#include "tiny_xpm.h" #include "tiny_xpm.h"
#include "video_buffer.h" #include "video_buffer.h"
#include "util/lock.h"
#include "util/log.h"
#define DISPLAY_MARGINS 96 #define DISPLAY_MARGINS 96
@@ -31,28 +30,23 @@ get_window_size(SDL_Window *window) {
// get the windowed window size // get the windowed window size
static struct size static struct size
get_windowed_window_size(const struct screen *screen) { get_windowed_window_size(const struct screen *screen) {
if (screen->fullscreen || screen->maximized) { if (screen->fullscreen) {
return screen->windowed_window_size; return screen->windowed_window_size;
} }
return get_window_size(screen->window); return get_window_size(screen->window);
} }
// apply the windowed window size if fullscreen and maximized are disabled
static void
apply_windowed_size(struct screen *screen) {
if (!screen->fullscreen && !screen->maximized) {
SDL_SetWindowSize(screen->window, screen->windowed_window_size.width,
screen->windowed_window_size.height);
}
}
// set the window size to be applied when fullscreen is disabled // set the window size to be applied when fullscreen is disabled
static void static void
set_window_size(struct screen *screen, struct size new_size) { set_window_size(struct screen *screen, struct size new_size) {
// setting the window size during fullscreen is implementation defined, // setting the window size during fullscreen is implementation defined,
// so apply the resize only after fullscreen is disabled // so apply the resize only after fullscreen is disabled
if (screen->fullscreen) {
// SDL_SetWindowSize will be called when fullscreen will be disabled
screen->windowed_window_size = new_size; screen->windowed_window_size = new_size;
apply_windowed_size(screen); } else {
SDL_SetWindowSize(screen->window, new_size.width, new_size.height);
}
} }
// get the preferred display bounds (i.e. the screen bounds with some margins) // get the preferred display bounds (i.e. the screen bounds with some margins)
@@ -111,7 +105,7 @@ get_optimal_size(struct size current_size, struct size frame_size) {
} }
// w and h must fit into 16 bits // w and h must fit into 16 bits
assert(w < 0x10000 && h < 0x10000); SDL_assert_release(w < 0x10000 && h < 0x10000);
return (struct size) {w, h}; return (struct size) {w, h};
} }
@@ -123,30 +117,9 @@ get_optimal_window_size(const struct screen *screen, struct size frame_size) {
} }
// initially, there is no current size, so use the frame size as current size // initially, there is no current size, so use the frame size as current size
// req_width and req_height, if not 0, are the sizes requested by the user
static inline struct size static inline struct size
get_initial_optimal_size(struct size frame_size, uint16_t req_width, get_initial_optimal_size(struct size frame_size) {
uint16_t req_height) { return get_optimal_size(frame_size, frame_size);
struct size window_size;
if (!req_width && !req_height) {
window_size = get_optimal_size(frame_size, frame_size);
} else {
if (req_width) {
window_size.width = req_width;
} else {
// compute from the requested height
window_size.width = (uint32_t) req_height * frame_size.width
/ frame_size.height;
}
if (req_height) {
window_size.height = req_height;
} else {
// compute from the requested width
window_size.height = (uint32_t) req_width * frame_size.height
/ frame_size.width;
}
}
return window_size;
} }
void void
@@ -161,19 +134,57 @@ create_texture(SDL_Renderer *renderer, struct size frame_size) {
frame_size.width, frame_size.height); frame_size.width, frame_size.height);
} }
#ifdef HIDPI_WORKAROUND
static void
screen_get_sizes(struct screen *screen, struct screen_sizes *out) {
int ww, wh, dw, dh;
SDL_GetWindowSize(screen->window, &ww, &wh);
SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
out->window.width = ww;
out->window.height = wh;
out->window.width = dw;
out->window.height = dh;
}
#endif
// This may be called more than once to work around SDL bugs
static bool
screen_init_renderer_and_texture(struct screen *screen) {
screen->renderer = SDL_CreateRenderer(screen->window, -1,
SDL_RENDERER_ACCELERATED);
if (!screen->renderer) {
LOGC("Could not create renderer: %s", SDL_GetError());
return false;
}
if (SDL_RenderSetLogicalSize(screen->renderer, screen->frame_size.width,
screen->frame_size.height)) {
LOGE("Could not set renderer logical size: %s", SDL_GetError());
return false;
}
screen->texture = create_texture(screen->renderer, screen->frame_size);
if (!screen->texture) {
LOGC("Could not create texture: %s", SDL_GetError());
return false;
}
#ifdef HIDPI_WORKAROUND
screen_get_sizes(screen, &screen->sizes);
#endif
return true;
}
bool bool
screen_init_rendering(struct screen *screen, const char *window_title, screen_init_rendering(struct screen *screen, const char *window_title,
struct size frame_size, bool always_on_top, struct size frame_size, bool always_on_top) {
int16_t window_x, int16_t window_y, uint16_t window_width,
uint16_t window_height, bool window_borderless) {
screen->frame_size = frame_size; screen->frame_size = frame_size;
struct size window_size = struct size window_size = get_initial_optimal_size(frame_size);
get_initial_optimal_size(frame_size, window_width, window_height); uint32_t window_flags = SDL_WINDOW_HIDDEN
uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; | SDL_WINDOW_RESIZABLE
#ifdef HIDPI_SUPPORT | SDL_WINDOW_ALLOW_HIGHDPI;
window_flags |= SDL_WINDOW_ALLOW_HIGHDPI;
#endif
if (always_on_top) { if (always_on_top) {
#ifdef SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP #ifdef SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
@@ -182,13 +193,9 @@ screen_init_rendering(struct screen *screen, const char *window_title,
"(compile with SDL >= 2.0.5 to enable it)"); "(compile with SDL >= 2.0.5 to enable it)");
#endif #endif
} }
if (window_borderless) {
window_flags |= SDL_WINDOW_BORDERLESS;
}
int x = window_x != -1 ? window_x : (int) SDL_WINDOWPOS_UNDEFINED; screen->window = SDL_CreateWindow(window_title, SDL_WINDOWPOS_UNDEFINED,
int y = window_y != -1 ? window_y : (int) SDL_WINDOWPOS_UNDEFINED; SDL_WINDOWPOS_UNDEFINED,
screen->window = SDL_CreateWindow(window_title, x, y,
window_size.width, window_size.height, window_size.width, window_size.height,
window_flags); window_flags);
if (!screen->window) { if (!screen->window) {
@@ -196,21 +203,6 @@ screen_init_rendering(struct screen *screen, const char *window_title,
return false; return false;
} }
screen->renderer = SDL_CreateRenderer(screen->window, -1,
SDL_RENDERER_ACCELERATED);
if (!screen->renderer) {
LOGC("Could not create renderer: %s", SDL_GetError());
screen_destroy(screen);
return false;
}
if (SDL_RenderSetLogicalSize(screen->renderer, frame_size.width,
frame_size.height)) {
LOGE("Could not set renderer logical size: %s", SDL_GetError());
screen_destroy(screen);
return false;
}
SDL_Surface *icon = read_xpm(icon_xpm); SDL_Surface *icon = read_xpm(icon_xpm);
if (icon) { if (icon) {
SDL_SetWindowIcon(screen->window, icon); SDL_SetWindowIcon(screen->window, icon);
@@ -221,15 +213,11 @@ screen_init_rendering(struct screen *screen, const char *window_title,
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width,
frame_size.height); frame_size.height);
screen->texture = create_texture(screen->renderer, frame_size); if (!screen_init_renderer_and_texture(screen)) {
if (!screen->texture) {
LOGC("Could not create texture: %s", SDL_GetError());
screen_destroy(screen); screen_destroy(screen);
return false; return false;
} }
screen->windowed_window_size = window_size;
return true; return true;
} }
@@ -314,6 +302,43 @@ screen_update_frame(struct screen *screen, struct video_buffer *vb) {
return true; return true;
} }
#ifdef HIDPI_WORKAROUND
// workaround for <https://github.com/Genymobile/scrcpy/issues/15>
static inline bool
screen_fix_hidpi(struct screen *screen) {
struct screen_sizes cur;
screen_get_sizes(screen, &cur);
struct screen_sizes *prev = &screen->sizes;
bool width_ratio_changed = cur.window.width * prev->drawable.width !=
cur.drawable.width * prev->window.width;
bool height_ratio_changed = cur.window.height * prev->drawable.height !=
cur.drawable.height * prev->window.height;
if (width_ratio_changed || height_ratio_changed) {
SDL_DestroyTexture(screen->texture);
SDL_DestroyRenderer(screen->renderer);
if (!screen_init_renderer_and_texture(screen)) {
screen->texture = NULL;
screen->renderer = NULL;
return false;
}
LOGI("Renderer reset after hidpi scaling changed");
}
return true;
}
#endif
void
screen_window_resized(struct screen *screen) {
#ifdef HIDPI_WORKAROUND
screen_fix_hidpi(screen);
#endif
screen_render(screen);
}
void void
screen_render(struct screen *screen) { screen_render(struct screen *screen) {
SDL_RenderClear(screen->renderer); SDL_RenderClear(screen->renderer);
@@ -323,6 +348,10 @@ screen_render(struct screen *screen) {
void void
screen_switch_fullscreen(struct screen *screen) { screen_switch_fullscreen(struct screen *screen) {
if (!screen->fullscreen) {
// going to fullscreen, store the current windowed window size
screen->windowed_window_size = get_window_size(screen->window);
}
uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
if (SDL_SetWindowFullscreen(screen->window, new_mode)) { if (SDL_SetWindowFullscreen(screen->window, new_mode)) {
LOGW("Could not switch fullscreen mode: %s", SDL_GetError()); LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
@@ -330,7 +359,11 @@ screen_switch_fullscreen(struct screen *screen) {
} }
screen->fullscreen = !screen->fullscreen; screen->fullscreen = !screen->fullscreen;
apply_windowed_size(screen); if (!screen->fullscreen) {
// fullscreen disabled, restore expected windowed window size
SDL_SetWindowSize(screen->window, screen->windowed_window_size.width,
screen->windowed_window_size.height);
}
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
screen_render(screen); screen_render(screen);
@@ -338,75 +371,20 @@ screen_switch_fullscreen(struct screen *screen) {
void void
screen_resize_to_fit(struct screen *screen) { screen_resize_to_fit(struct screen *screen) {
if (screen->fullscreen) { if (!screen->fullscreen) {
return; struct size optimal_size = get_optimal_window_size(screen,
} screen->frame_size);
SDL_SetWindowSize(screen->window, optimal_size.width,
if (screen->maximized) { optimal_size.height);
SDL_RestoreWindow(screen->window);
screen->maximized = false;
}
struct size optimal_size =
get_optimal_window_size(screen, screen->frame_size);
SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height);
LOGD("Resized to optimal size"); LOGD("Resized to optimal size");
} }
}
void void
screen_resize_to_pixel_perfect(struct screen *screen) { screen_resize_to_pixel_perfect(struct screen *screen) {
if (screen->fullscreen) { if (!screen->fullscreen) {
return;
}
if (screen->maximized) {
SDL_RestoreWindow(screen->window);
screen->maximized = false;
}
SDL_SetWindowSize(screen->window, screen->frame_size.width, SDL_SetWindowSize(screen->window, screen->frame_size.width,
screen->frame_size.height); screen->frame_size.height);
LOGD("Resized to pixel-perfect"); LOGD("Resized to pixel-perfect");
} }
void
screen_handle_window_event(struct screen *screen,
const SDL_WindowEvent *event) {
switch (event->event) {
case SDL_WINDOWEVENT_EXPOSED:
screen_render(screen);
break;
case SDL_WINDOWEVENT_SIZE_CHANGED:
if (!screen->fullscreen && !screen->maximized) {
// Backup the previous size: if we receive the MAXIMIZED event,
// then the new size must be ignored (it's the maximized size).
// We could not rely on the window flags due to race conditions
// (they could be updated asynchronously, at least on X11).
screen->windowed_window_size_backup =
screen->windowed_window_size;
// Save the windowed size, so that it is available once the
// window is maximized or fullscreen is enabled.
screen->windowed_window_size = get_window_size(screen->window);
}
screen_render(screen);
break;
case SDL_WINDOWEVENT_MAXIMIZED:
// The backup size must be non-nul.
assert(screen->windowed_window_size_backup.width);
assert(screen->windowed_window_size_backup.height);
// Revert the last size, it was updated while screen was maximized.
screen->windowed_window_size = screen->windowed_window_size_backup;
#ifdef DEBUG
// Reset the backup to invalid values to detect unexpected usage
screen->windowed_window_size_backup.width = 0;
screen->windowed_window_size_backup.height = 0;
#endif
screen->maximized = true;
break;
case SDL_WINDOWEVENT_RESTORED:
screen->maximized = false;
apply_windowed_size(screen);
break;
}
} }

View File

@@ -15,15 +15,17 @@ struct screen {
SDL_Renderer *renderer; SDL_Renderer *renderer;
SDL_Texture *texture; SDL_Texture *texture;
struct size frame_size; struct size frame_size;
// The window size the last time it was not maximized or fullscreen. //used only in fullscreen mode to know the windowed window size
struct size windowed_window_size; struct size windowed_window_size;
// Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be
// able to revert the size to its non-maximized value.
struct size windowed_window_size_backup;
bool has_frame; bool has_frame;
bool fullscreen; bool fullscreen;
bool maximized;
bool no_window; bool no_window;
#ifdef HIDPI_WORKAROUND
struct screen_sizes {
struct size window;
struct size drawable;
} sizes;
#endif
}; };
#define SCREEN_INITIALIZER { \ #define SCREEN_INITIALIZER { \
@@ -38,13 +40,8 @@ struct screen {
.width = 0, \ .width = 0, \
.height = 0, \ .height = 0, \
}, \ }, \
.windowed_window_size_backup = { \
.width = 0, \
.height = 0, \
}, \
.has_frame = false, \ .has_frame = false, \
.fullscreen = false, \ .fullscreen = false, \
.maximized = false, \
.no_window = false, \ .no_window = false, \
} }
@@ -55,9 +52,7 @@ screen_init(struct screen *screen);
// initialize screen, create window, renderer and texture (window is hidden) // initialize screen, create window, renderer and texture (window is hidden)
bool bool
screen_init_rendering(struct screen *screen, const char *window_title, screen_init_rendering(struct screen *screen, const char *window_title,
struct size frame_size, bool always_on_top, struct size frame_size, bool always_on_top);
int16_t window_x, int16_t window_y, uint16_t window_width,
uint16_t window_height, bool window_borderless);
// show the window // show the window
void void
@@ -71,6 +66,10 @@ screen_destroy(struct screen *screen);
bool bool
screen_update_frame(struct screen *screen, struct video_buffer *vb); screen_update_frame(struct screen *screen, struct video_buffer *vb);
// update content after window resizing
void
screen_window_resized(struct screen *screen);
// render the texture to the renderer // render the texture to the renderer
void void
screen_render(struct screen *screen); screen_render(struct screen *screen);
@@ -87,8 +86,4 @@ screen_resize_to_fit(struct screen *screen);
void void
screen_resize_to_pixel_perfect(struct screen *screen); screen_resize_to_pixel_perfect(struct screen *screen);
// react to window events
void
screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event);
#endif #endif

View File

@@ -1,22 +1,22 @@
#include "server.h" #include "server.h"
#include <assert.h>
#include <errno.h> #include <errno.h>
#include <inttypes.h> #include <inttypes.h>
#include <libgen.h> #include <libgen.h>
#include <stdio.h> #include <stdio.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_timer.h> #include <SDL2/SDL_timer.h>
#include "config.h" #include "config.h"
#include "command.h" #include "command.h"
#include "util/log.h" #include "log.h"
#include "util/net.h" #include "net.h"
#define SOCKET_NAME "scrcpy" #define SOCKET_NAME "scrcpy"
#define SERVER_FILENAME "scrcpy-server" #define SERVER_FILENAME "scrcpy-server"
#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME
#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" #define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME
static const char * static const char *
get_server_path(void) { get_server_path(void) {
@@ -118,13 +118,11 @@ static process_t
execute_server(struct server *server, const struct server_params *params) { execute_server(struct server *server, const struct server_params *params) {
char max_size_string[6]; char max_size_string[6];
char bit_rate_string[11]; char bit_rate_string[11];
char max_fps_string[6];
sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
sprintf(max_fps_string, "%"PRIu16, params->max_fps);
const char *const cmd[] = { const char *const cmd[] = {
"shell", "shell",
"CLASSPATH=" DEVICE_SERVER_PATH, "CLASSPATH=/data/local/tmp/" SERVER_FILENAME,
"app_process", "app_process",
#ifdef SERVER_DEBUGGER #ifdef SERVER_DEBUGGER
# define SERVER_DEBUGGER_PORT "5005" # define SERVER_DEBUGGER_PORT "5005"
@@ -133,10 +131,8 @@ execute_server(struct server *server, const struct server_params *params) {
#endif #endif
"/", // unused "/", // unused
"com.genymobile.scrcpy.Server", "com.genymobile.scrcpy.Server",
SCRCPY_VERSION,
max_size_string, max_size_string,
bit_rate_string, bit_rate_string,
max_fps_string,
server->tunnel_forward ? "true" : "false", server->tunnel_forward ? "true" : "false",
params->crop ? params->crop : "-", params->crop ? params->crop : "-",
"true", // always send frame meta (packet boundaries + timestamp) "true", // always send frame meta (packet boundaries + timestamp)
@@ -199,7 +195,7 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) {
static void static void
close_socket(socket_t *socket) { close_socket(socket_t *socket) {
assert(*socket != INVALID_SOCKET); SDL_assert(*socket != INVALID_SOCKET);
net_shutdown(*socket, SHUT_RDWR); net_shutdown(*socket, SHUT_RDWR);
if (!net_close(*socket)) { if (!net_close(*socket)) {
LOGW("Could not close socket"); LOGW("Could not close socket");
@@ -281,7 +277,7 @@ server_connect_to(struct server *server) {
server->control_socket = net_accept(server->server_socket); server->control_socket = net_accept(server->server_socket);
if (server->control_socket == INVALID_SOCKET) { if (server->control_socket == INVALID_SOCKET) {
// the video_socket will be cleaned up on destroy // the video_socket will be clean up on destroy
return false; return false;
} }
@@ -323,7 +319,7 @@ server_stop(struct server *server) {
close_socket(&server->control_socket); close_socket(&server->control_socket);
} }
assert(server->process != PROCESS_NONE); SDL_assert(server->process != PROCESS_NONE);
if (!cmd_terminate(server->process)) { if (!cmd_terminate(server->process)) {
LOGW("Could not terminate server"); LOGW("Could not terminate server");

View File

@@ -6,7 +6,7 @@
#include "config.h" #include "config.h"
#include "command.h" #include "command.h"
#include "util/net.h" #include "net.h"
struct server { struct server {
char *serial; char *serial;
@@ -35,7 +35,6 @@ struct server_params {
uint16_t local_port; uint16_t local_port;
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps;
bool control; bool control;
}; };

View File

@@ -1,8 +1,8 @@
#include "stream.h" #include "stream.h"
#include <assert.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include <libavutil/time.h> #include <libavutil/time.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_events.h> #include <SDL2/SDL_events.h>
#include <SDL2/SDL_mutex.h> #include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h> #include <SDL2/SDL_thread.h>
@@ -10,11 +10,12 @@
#include "config.h" #include "config.h"
#include "compat.h" #include "compat.h"
#include "buffer_util.h"
#include "decoder.h" #include "decoder.h"
#include "events.h" #include "events.h"
#include "lock_util.h"
#include "log.h"
#include "recorder.h" #include "recorder.h"
#include "util/buffer_util.h"
#include "util/log.h"
#define BUFSIZE 0x10000 #define BUFSIZE 0x10000
@@ -43,8 +44,7 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) {
uint64_t pts = buffer_read64be(header); uint64_t pts = buffer_read64be(header);
uint32_t len = buffer_read32be(&header[8]); uint32_t len = buffer_read32be(&header[8]);
assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0); SDL_assert(len);
assert(len);
if (av_new_packet(packet, len)) { if (av_new_packet(packet, len)) {
LOGE("Could not allocate packet"); LOGE("Could not allocate packet");
@@ -52,12 +52,12 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) {
} }
r = net_recv_all(stream->socket, packet->data, len); r = net_recv_all(stream->socket, packet->data, len);
if (r < 0 || ((uint32_t) r) < len) { if (r < len) {
av_packet_unref(packet); av_packet_unref(packet);
return false; return false;
} }
packet->pts = pts != NO_PTS ? (int64_t) pts : AV_NOPTS_VALUE; packet->pts = pts != NO_PTS ? pts : AV_NOPTS_VALUE;
return true; return true;
} }
@@ -107,9 +107,8 @@ stream_parse(struct stream *stream, AVPacket *packet) {
AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1); AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1);
// PARSER_FLAG_COMPLETE_FRAMES is set // PARSER_FLAG_COMPLETE_FRAMES is set
assert(r == in_len); SDL_assert(r == in_len);
(void) r; SDL_assert(out_len == in_len);
assert(out_len == in_len);
if (stream->parser->key_frame == 1) { if (stream->parser->key_frame == 1) {
packet->flags |= AV_PKT_FLAG_KEY; packet->flags |= AV_PKT_FLAG_KEY;

View File

@@ -8,7 +8,7 @@
#include <SDL2/SDL_thread.h> #include <SDL2/SDL_thread.h>
#include "config.h" #include "config.h"
#include "util/net.h" #include "net.h"
struct video_buffer; struct video_buffer;

View File

@@ -17,11 +17,10 @@
#include <sys/types.h> #include <sys/types.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <unistd.h> #include <unistd.h>
#include "log.h"
#include "util/log.h"
enum process_result enum process_result
cmd_execute(const char *const argv[], pid_t *pid) { cmd_execute(const char *path, const char *const argv[], pid_t *pid) {
int fd[2]; int fd[2];
if (pipe(fd) == -1) { if (pipe(fd) == -1) {
@@ -52,7 +51,7 @@ cmd_execute(const char *const argv[], pid_t *pid) {
// child close read side // child close read side
close(fd[0]); close(fd[0]);
if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) { if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) {
execvp(argv[0], (char *const *)argv); execvp(path, (char *const *)argv);
if (errno == ENOENT) { if (errno == ENOENT) {
ret = PROCESS_ERROR_MISSING_BINARY; ret = PROCESS_ERROR_MISSING_BINARY;
} else { } else {

View File

@@ -1,4 +1,4 @@
#include "util/net.h" #include "net.h"
#include <unistd.h> #include <unistd.h>

View File

@@ -1,8 +1,8 @@
#include "command.h" #include "command.h"
#include "config.h" #include "config.h"
#include "util/log.h" #include "log.h"
#include "util/str_util.h" #include "str_util.h"
static int static int
build_cmd(char *cmd, size_t len, const char *const argv[]) { build_cmd(char *cmd, size_t len, const char *const argv[]) {
@@ -19,7 +19,7 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) {
} }
enum process_result enum process_result
cmd_execute(const char *const argv[], HANDLE *handle) { cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
STARTUPINFOW si; STARTUPINFOW si;
PROCESS_INFORMATION pi; PROCESS_INFORMATION pi;
memset(&si, 0, sizeof(si)); memset(&si, 0, sizeof(si));

View File

@@ -1,7 +1,7 @@
#include "util/net.h" #include "net.h"
#include "config.h" #include "config.h"
#include "util/log.h" #include "log.h"
bool bool
net_init(void) { net_init(void) {

View File

@@ -1,13 +1,12 @@
#include "tiny_xpm.h" #include "tiny_xpm.h"
#include <assert.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include "config.h" #include "config.h"
#include "util/log.h" #include "log.h"
struct index { struct index {
char c; char c;
@@ -37,7 +36,7 @@ find_color(struct index *index, int len, char c, uint32_t *color) {
// (non-const) "char *" // (non-const) "char *"
SDL_Surface * SDL_Surface *
read_xpm(char *xpm[]) { read_xpm(char *xpm[]) {
#ifndef NDEBUG #if SDL_ASSERT_LEVEL >= 2
// patch the XPM to change the icon color in debug mode // patch the XPM to change the icon color in debug mode
xpm[2] = ". c #CC00CC"; xpm[2] = ". c #CC00CC";
#endif #endif
@@ -52,26 +51,24 @@ read_xpm(char *xpm[]) {
int chars = strtol(endptr + 1, &endptr, 10); int chars = strtol(endptr + 1, &endptr, 10);
// sanity checks // sanity checks
assert(0 <= width && width < 256); SDL_assert(0 <= width && width < 256);
assert(0 <= height && height < 256); SDL_assert(0 <= height && height < 256);
assert(0 <= colors && colors < 256); SDL_assert(0 <= colors && colors < 256);
assert(chars == 1); // this implementation does not support more SDL_assert(chars == 1); // this implementation does not support more
(void) chars;
// init index // init index
struct index index[colors]; struct index index[colors];
for (int i = 0; i < colors; ++i) { for (int i = 0; i < colors; ++i) {
const char *line = xpm[1+i]; const char *line = xpm[1+i];
index[i].c = line[0]; index[i].c = line[0];
assert(line[1] == '\t'); SDL_assert(line[1] == '\t');
assert(line[2] == 'c'); SDL_assert(line[2] == 'c');
assert(line[3] == ' '); SDL_assert(line[3] == ' ');
if (line[4] == '#') { if (line[4] == '#') {
index[i].color = 0xff000000 | strtol(&line[5], &endptr, 0x10); index[i].color = 0xff000000 | strtol(&line[5], &endptr, 0x10);
assert(*endptr == '\0'); SDL_assert(*endptr == '\0');
} else { } else {
assert(!strcmp("None", &line[4])); SDL_assert(!strcmp("None", &line[4]));
index[i].color = 0; index[i].color = 0;
} }
} }
@@ -88,8 +85,7 @@ read_xpm(char *xpm[]) {
char c = line[x]; char c = line[x];
uint32_t color; uint32_t color;
bool color_found = find_color(index, colors, c, &color); bool color_found = find_color(index, colors, c, &color);
assert(color_found); SDL_assert(color_found);
(void) color_found;
pixels[y * width + x] = color; pixels[y * width + x] = color;
} }
} }

View File

@@ -1,74 +0,0 @@
#ifndef LOCK_H
#define LOCK_H
#include <stdint.h>
#include <SDL2/SDL_mutex.h>
#include "config.h"
#include "log.h"
static inline void
mutex_lock(SDL_mutex *mutex) {
int r = SDL_LockMutex(mutex);
#ifndef NDEBUG
if (r) {
LOGC("Could not lock mutex: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
}
static inline void
mutex_unlock(SDL_mutex *mutex) {
int r = SDL_UnlockMutex(mutex);
#ifndef NDEBUG
if (r) {
LOGC("Could not unlock mutex: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
}
static inline void
cond_wait(SDL_cond *cond, SDL_mutex *mutex) {
int r = SDL_CondWait(cond, mutex);
#ifndef NDEBUG
if (r) {
LOGC("Could not wait on condition: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
}
static inline int
cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) {
int r = SDL_CondWaitTimeout(cond, mutex, ms);
#ifndef NDEBUG
if (r < 0) {
LOGC("Could not wait on condition with timeout: %s", SDL_GetError());
abort();
}
#endif
return r;
}
static inline void
cond_signal(SDL_cond *cond) {
int r = SDL_CondSignal(cond);
#ifndef NDEBUG
if (r) {
LOGC("Could not signal a condition: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
}
#endif

View File

@@ -1,13 +1,13 @@
#include "video_buffer.h" #include "video_buffer.h"
#include <assert.h> #include <SDL2/SDL_assert.h>
#include <SDL2/SDL_mutex.h> #include <SDL2/SDL_mutex.h>
#include <libavutil/avutil.h> #include <libavutil/avutil.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include "config.h" #include "config.h"
#include "util/lock.h" #include "lock_util.h"
#include "util/log.h" #include "log.h"
bool bool
video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter,
@@ -91,7 +91,7 @@ video_buffer_offer_decoded_frame(struct video_buffer *vb,
const AVFrame * const AVFrame *
video_buffer_consume_rendered_frame(struct video_buffer *vb) { video_buffer_consume_rendered_frame(struct video_buffer *vb) {
assert(!vb->rendering_frame_consumed); SDL_assert(!vb->rendering_frame_consumed);
vb->rendering_frame_consumed = true; vb->rendering_frame_consumed = true;
fps_counter_add_rendered_frame(vb->fps_counter); fps_counter_add_rendered_frame(vb->fps_counter);
if (vb->render_expired_frames) { if (vb->render_expired_frames) {

View File

@@ -1,76 +0,0 @@
#include <assert.h>
#include "util/buffer_util.h"
static void test_buffer_write16be(void) {
uint16_t val = 0xABCD;
uint8_t buf[2];
buffer_write16be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
}
static void test_buffer_write32be(void) {
uint32_t val = 0xABCD1234;
uint8_t buf[4];
buffer_write32be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
assert(buf[2] == 0x12);
assert(buf[3] == 0x34);
}
static void test_buffer_write64be(void) {
uint64_t val = 0xABCD1234567890EF;
uint8_t buf[8];
buffer_write64be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
assert(buf[2] == 0x12);
assert(buf[3] == 0x34);
assert(buf[4] == 0x56);
assert(buf[5] == 0x78);
assert(buf[6] == 0x90);
assert(buf[7] == 0xEF);
}
static void test_buffer_read16be(void) {
uint8_t buf[2] = {0xAB, 0xCD};
uint16_t val = buffer_read16be(buf);
assert(val == 0xABCD);
}
static void test_buffer_read32be(void) {
uint8_t buf[4] = {0xAB, 0xCD, 0x12, 0x34};
uint32_t val = buffer_read32be(buf);
assert(val == 0xABCD1234);
}
static void test_buffer_read64be(void) {
uint8_t buf[8] = {0xAB, 0xCD, 0x12, 0x34,
0x56, 0x78, 0x90, 0xEF};
uint64_t val = buffer_read64be(buf);
assert(val == 0xABCD1234567890EF);
}
int main(void) {
test_buffer_write16be();
test_buffer_write32be();
test_buffer_write64be();
test_buffer_read16be();
test_buffer_read32be();
test_buffer_read64be();
return 0;
}

View File

@@ -1,7 +1,7 @@
#include <assert.h> #include <assert.h>
#include <string.h> #include <string.h>
#include "util/cbuf.h" #include "cbuf.h"
struct int_queue CBUF(int, 32); struct int_queue CBUF(int, 32);

View File

@@ -236,21 +236,6 @@ static void test_serialize_set_screen_power_mode(void) {
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
static void test_serialize_rotate_device(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_ROTATE_DEVICE,
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 1);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_ROTATE_DEVICE,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
int main(void) { int main(void) {
test_serialize_inject_keycode(); test_serialize_inject_keycode();
test_serialize_inject_text(); test_serialize_inject_text();
@@ -263,6 +248,5 @@ int main(void) {
test_serialize_get_clipboard(); test_serialize_get_clipboard();
test_serialize_set_clipboard(); test_serialize_set_clipboard();
test_serialize_set_screen_power_mode(); test_serialize_set_screen_power_mode();
test_serialize_rotate_device();
return 0; return 0;
} }

View File

@@ -1,6 +1,6 @@
#include <assert.h> #include <assert.h>
#include "util/queue.h" #include <queue.h>
struct foo { struct foo {
int value; int value;

View File

@@ -1,8 +1,7 @@
#include <assert.h> #include <assert.h>
#include <string.h> #include <string.h>
#include <SDL2/SDL.h>
#include "util/str_util.h" #include "str_util.h"
static void test_xstrncpy_simple(void) { static void test_xstrncpy_simple(void) {
char s[] = "xxxxxxxxxx"; char s[] = "xxxxxxxxxx";
@@ -127,16 +126,6 @@ static void test_xstrjoin_truncated_after_sep(void) {
assert(!strcmp("abc de ", s)); assert(!strcmp("abc de ", s));
} }
static void test_strquote(void) {
const char *s = "abcde";
char *out = strquote(s);
// add '"' at the beginning and the end
assert(!strcmp("\"abcde\"", out));
SDL_free(out);
}
static void test_utf8_truncate(void) { static void test_utf8_truncate(void) {
const char *s = "aÉbÔc"; const char *s = "aÉbÔc";
assert(strlen(s) == 7); // É and Ô are 2 bytes-wide assert(strlen(s) == 7); // É and Ô are 2 bytes-wide
@@ -177,7 +166,6 @@ int main(void) {
test_xstrjoin_truncated_in_token(); test_xstrjoin_truncated_in_token();
test_xstrjoin_truncated_before_sep(); test_xstrjoin_truncated_before_sep();
test_xstrjoin_truncated_after_sep(); test_xstrjoin_truncated_after_sep();
test_strquote();
test_utf8_truncate(); test_utf8_truncate();
return 0; return 0;
} }

View File

@@ -54,7 +54,7 @@ page at http://checkstyle.sourceforge.net/config.html -->
<module name="SuppressWarningsHolder"/> <module name="SuppressWarningsHolder"/>
<!-- Checks for imports --> <!-- Checks for imports -->
<!-- See http://checkstyle.sf.net/config_imports.html --> <!-- See http://checkstyle.sf.net/config_import.html -->
<module name="AvoidStarImport"> <module name="AvoidStarImport">
<property name="allowStaticMemberImports" value="true" /> <property name="allowStaticMemberImports" value="true" />
</module> </module>
@@ -99,7 +99,7 @@ page at http://checkstyle.sourceforge.net/config.html -->
<module name="WhitespaceAround" /> <module name="WhitespaceAround" />
<!-- Modifier Checks --> <!-- Modifier Checks -->
<!-- See http://checkstyle.sf.net/config_modifier.html --> <!-- See http://checkstyle.sf.net/config_modifiers.html -->
<module name="ModifierOrder" /> <module name="ModifierOrder" />
<module name="RedundantModifier" /> <module name="RedundantModifier" />

View File

@@ -15,6 +15,6 @@ cpu = 'i686'
endian = 'little' endian = 'little'
[properties] [properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win32-shared' prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win32-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win32-dev' prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win32-dev'
prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32' prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32'

View File

@@ -15,6 +15,6 @@ cpu = 'x86_64'
endian = 'little' endian = 'little'
[properties] [properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win64-shared' prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win64-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win64-dev' prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win64-dev'
prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32' prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32'

View File

@@ -1,10 +1,7 @@
project('scrcpy', 'c', project('scrcpy', 'c',
version: '1.11', version: '1.10',
meson_version: '>= 0.37', meson_version: '>= 0.37',
default_options: [ default_options: 'c_std=c11')
'c_std=c11',
'warning_level=2',
])
if get_option('compile_app') if get_option('compile_app')
subdir('app') subdir('app')

View File

@@ -4,5 +4,5 @@ option('crossbuild_windows', type: 'boolean', value: false, description: 'Build
option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)')
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')
option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached')
option('hidpi_workaround', type: 'boolean', value: true, description: 'Enable a workaround for bug #15')

View File

@@ -10,24 +10,24 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32
prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
prepare-ffmpeg-shared-win32: prepare-ffmpeg-shared-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.1-win32-shared.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1.4-win32-shared.zip \
9208255f409410d95147151d7e829b5699bf8d91bfe1e81c3f470f47c2fa66d2 \ 596608277f6b937c3dea7c46e854665d75b3de56790bae07f655ca331440f003 \
ffmpeg-4.2.1-win32-shared ffmpeg-4.1.4-win32-shared
prepare-ffmpeg-dev-win32: prepare-ffmpeg-dev-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.1-win32-dev.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.4-win32-dev.zip \
c3469e6c5f031cbcc8cba88dee92d6548c5c6b6ff14f4097f18f72a92d0d70c4 \ a80c86e263cfad26e202edfa5e6e939a2c88843ae26f031d3e0d981a39fd03fb \
ffmpeg-4.2.1-win32-dev ffmpeg-4.1.4-win32-dev
prepare-ffmpeg-shared-win64: prepare-ffmpeg-shared-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.1-win64-shared.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.4-win64-shared.zip \
55063d3cf750a75485c7bf196031773d81a1b25d0980c7db48ecfc7701a42331 \ a90889871de2cab8a79b392591313a188189a353f69dde1db98aebe20b280989 \
ffmpeg-4.2.1-win64-shared ffmpeg-4.1.4-win64-shared
prepare-ffmpeg-dev-win64: prepare-ffmpeg-dev-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.1-win64-dev.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.4-win64-dev.zip \
5af393be5f25c0a71aa29efce768e477c35347f7f8e0d9696767d5b9d405b74e \ 6c9d53f9e94ce1821e975ec668e5b9d6e9deb4a45d0d7e30264685d3dfbbb068 \
ffmpeg-4.2.1-win64-dev ffmpeg-4.1.4-win64-dev
prepare-sdl2: prepare-sdl2:
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \ @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \
@@ -35,6 +35,6 @@ prepare-sdl2:
SDL2-2.0.10 SDL2-2.0.10
prepare-adb: prepare-adb:
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \ @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.2-windows.zip \
2df06160056ec9a84c7334af2a1e42740befbb1a2e34370e7af544a2cc78152c \ d78f02e5e2c9c4c1d046dcd4e6fbdf586e5f57ef66eb0da5c2b49d745d85d5ee \
platform-tools platform-tools

View File

@@ -6,8 +6,8 @@ android {
applicationId "com.genymobile.scrcpy" applicationId "com.genymobile.scrcpy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode 12 versionCode 11
versionName "1.11" versionName "1.10"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {

View File

@@ -11,9 +11,6 @@
set -e set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=1.11
PLATFORM=${ANDROID_PLATFORM:-29} PLATFORM=${ANDROID_PLATFORM:-29}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}
@@ -33,8 +30,7 @@ mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy"
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
public final class BuildConfig { public final class BuildConfig {
public static final boolean DEBUG = $SCRCPY_DEBUG; public static final boolean DEBUG = false;
public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME";
} }
EOF EOF

View File

@@ -15,7 +15,6 @@ public final class ControlMessage {
public static final int TYPE_GET_CLIPBOARD = 7; public static final int TYPE_GET_CLIPBOARD = 7;
public static final int TYPE_SET_CLIPBOARD = 8; public static final int TYPE_SET_CLIPBOARD = 8;
public static final int TYPE_SET_SCREEN_POWER_MODE = 9; public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
public static final int TYPE_ROTATE_DEVICE = 10;
private int type; private int type;
private String text; private String text;
@@ -48,7 +47,8 @@ public final class ControlMessage {
return msg; return msg;
} }
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) { public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure,
int buttons) {
ControlMessage msg = new ControlMessage(); ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_TOUCH_EVENT; msg.type = TYPE_INJECT_TOUCH_EVENT;
msg.action = action; msg.action = action;

View File

@@ -76,7 +76,6 @@ public class ControlMessageReader {
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
case ControlMessage.TYPE_GET_CLIPBOARD: case ControlMessage.TYPE_GET_CLIPBOARD:
case ControlMessage.TYPE_ROTATE_DEVICE:
msg = ControlMessage.createEmpty(type); msg = ControlMessage.createEmpty(type);
break; break;
default: default:

View File

@@ -13,8 +13,6 @@ import java.io.IOException;
public class Controller { public class Controller {
private static final int DEVICE_ID_VIRTUAL = -1;
private final Device device; private final Device device;
private final DesktopConnection connection; private final DesktopConnection connection;
private final DeviceMessageSender sender; private final DeviceMessageSender sender;
@@ -23,8 +21,10 @@ public class Controller {
private long lastTouchDown; private long lastTouchDown;
private final PointersState pointersState = new PointersState(); private final PointersState pointersState = new PointersState();
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; private final MotionEvent.PointerProperties[] pointerProperties =
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
private final MotionEvent.PointerCoords[] pointerCoords =
new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
public Controller(Device device, DesktopConnection connection) { public Controller(Device device, DesktopConnection connection) {
this.device = device; this.device = device;
@@ -106,9 +106,6 @@ public class Controller {
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
device.setScreenPowerMode(msg.getAction()); device.setScreenPowerMode(msg.getAction());
break; break;
case ControlMessage.TYPE_ROTATE_DEVICE:
device.rotateDevice();
break;
default: default:
// do nothing // do nothing
} }
@@ -179,9 +176,8 @@ public class Controller {
} }
} }
MotionEvent event = MotionEvent MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties,
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0, pointerCoords, 0, buttons, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
InputDevice.SOURCE_TOUCHSCREEN, 0);
return injectEvent(event); return injectEvent(event);
} }
@@ -202,16 +198,15 @@ public class Controller {
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 MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties,
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0, pointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0);
InputDevice.SOURCE_MOUSE, 0);
return injectEvent(event); return injectEvent(event);
} }
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
long now = SystemClock.uptimeMillis(); long now = SystemClock.uptimeMillis();
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD,
InputDevice.SOURCE_KEYBOARD); 0, 0, InputDevice.SOURCE_KEYBOARD);
return injectEvent(event); return injectEvent(event);
} }

View File

@@ -2,7 +2,6 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.SurfaceControl;
import com.genymobile.scrcpy.wrappers.WindowManager;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Build; import android.os.Build;
@@ -171,27 +170,6 @@ public final class Device {
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
} }
/**
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
*/
public void rotateDevice() {
WindowManager wm = serviceManager.getWindowManager();
boolean accelerometerRotation = !wm.isRotationFrozen();
int currentRotation = wm.getRotation();
int newRotation = (currentRotation & 1) ^ 1; // 0->1, 1->0, 2->1, 3->0
String newRotationString = newRotation == 0 ? "portrait" : "landscape";
Ln.i("Device rotation requested: " + newRotationString);
wm.freezeRotation(newRotation);
// restore auto-rotate if necessary
if (accelerometerRotation) {
wm.thawRotation();
}
}
static Rect flipRect(Rect crop) { static Rect flipRect(Rect crop) {
return new Rect(crop.top, crop.left, crop.bottom, crop.right); return new Rect(crop.top, crop.left, crop.bottom, crop.right);
} }

View File

@@ -12,7 +12,10 @@ public final class Ln {
private static final String PREFIX = "[server] "; private static final String PREFIX = "[server] ";
enum Level { enum Level {
DEBUG, INFO, WARN, ERROR DEBUG,
INFO,
WARN,
ERROR;
} }
private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO; private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO;

View File

@@ -5,7 +5,6 @@ import android.graphics.Rect;
public class Options { public class Options {
private int maxSize; private int maxSize;
private int bitRate; private int bitRate;
private int maxFps;
private boolean tunnelForward; private boolean tunnelForward;
private Rect crop; private Rect crop;
private boolean sendFrameMeta; // send PTS so that the client may record properly private boolean sendFrameMeta; // send PTS so that the client may record properly
@@ -27,14 +26,6 @@ public class Options {
this.bitRate = bitRate; this.bitRate = bitRate;
} }
public int getMaxFps() {
return maxFps;
}
public void setMaxFps(int maxFps) {
this.maxFps = maxFps;
}
public boolean isTunnelForward() { public boolean isTunnelForward() {
return tunnelForward; return tunnelForward;
} }

View File

@@ -28,7 +28,8 @@ public class Point {
return false; return false;
} }
Point point = (Point) o; Point point = (Point) o;
return x == point.x && y == point.y; return x == point.x
&& y == point.y;
} }
@Override @Override
@@ -38,6 +39,9 @@ public class Point {
@Override @Override
public String toString() { public String toString() {
return "Point{" + "x=" + x + ", y=" + y + '}'; return "Point{"
+ "x=" + x
+ ", y=" + y
+ '}';
} }
} }

View File

@@ -32,7 +32,8 @@ public class Position {
return false; return false;
} }
Position position = (Position) o; Position position = (Position) o;
return Objects.equals(point, position.point) && Objects.equals(screenSize, position.screenSize); return Objects.equals(point, position.point)
&& Objects.equals(screenSize, position.screenSize);
} }
@Override @Override
@@ -42,7 +43,10 @@ public class Position {
@Override @Override
public String toString() { public String toString() {
return "Position{" + "point=" + point + ", screenSize=" + screenSize + '}'; return "Position{"
+ "point=" + point
+ ", screenSize=" + screenSize
+ '}';
} }
} }

View File

@@ -6,7 +6,6 @@ import android.graphics.Rect;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.view.Surface; import android.view.Surface;
@@ -17,29 +16,32 @@ import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener { public class ScreenEncoder implements Device.RotationListener {
private static final int DEFAULT_FRAME_RATE = 60; // fps
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
private static final int REPEAT_FRAME_DELAY = 6; // repeat after 6 frames
private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000;
private static final int NO_PTS = -1; private static final int NO_PTS = -1;
private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
private int bitRate; private int bitRate;
private int maxFps; private int frameRate;
private int iFrameInterval; private int iFrameInterval;
private boolean sendFrameMeta; private boolean sendFrameMeta;
private long ptsOrigin; private long ptsOrigin;
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) { public ScreenEncoder(boolean sendFrameMeta, int bitRate, int frameRate, int iFrameInterval) {
this.sendFrameMeta = sendFrameMeta; this.sendFrameMeta = sendFrameMeta;
this.bitRate = bitRate; this.bitRate = bitRate;
this.maxFps = maxFps; this.frameRate = frameRate;
this.iFrameInterval = iFrameInterval; this.iFrameInterval = iFrameInterval;
} }
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { public ScreenEncoder(boolean sendFrameMeta, int bitRate) {
this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); this(sendFrameMeta, bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
} }
@Override @Override
@@ -52,10 +54,7 @@ public class ScreenEncoder implements Device.RotationListener {
} }
public void streamScreen(Device device, FileDescriptor fd) throws IOException { public void streamScreen(Device device, FileDescriptor fd) throws IOException {
Workarounds.prepareMainLooper(); MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval);
Workarounds.fillAppInfo();
MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval);
device.setRotationListener(this); device.setRotationListener(this);
boolean alive; boolean alive;
try { try {
@@ -138,24 +137,15 @@ public class ScreenEncoder implements Device.RotationListener {
return MediaCodec.createEncoderByType("video/avc"); return MediaCodec.createEncoderByType("video/avc");
} }
@SuppressWarnings("checkstyle:MagicNumber") private static MediaFormat createFormat(int bitRate, int frameRate, int iFrameInterval) throws IOException {
private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) {
MediaFormat format = new MediaFormat(); MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, "video/avc"); format.setString(MediaFormat.KEY_MIME, "video/avc");
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
// display the very first frame, and recover from bad quality when no new frames // display the very first frame, and recover from bad quality when no new frames
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate); // µs
if (maxFps > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
format.setFloat(MediaFormat.KEY_MAX_FPS_TO_ENCODER, maxFps);
} else {
Ln.w("Max FPS is only supported since Android 10, the option has been ignored");
}
}
return format; return format;
} }

View File

@@ -1,15 +1,13 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.graphics.Rect; import android.graphics.Rect;
import android.media.MediaCodec;
import android.os.Build;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
public final class Server { public final class Server {
private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server";
private Server() { private Server() {
// not instantiable // not instantiable
@@ -19,7 +17,7 @@ public final class Server {
final Device device = new Device(options); final Device device = new Device(options);
boolean tunnelForward = options.isTunnelForward(); boolean tunnelForward = options.isTunnelForward();
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps()); ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate());
if (options.getControl()) { if (options.getControl()) {
Controller controller = new Controller(device, connection); Controller controller = new Controller(device, connection);
@@ -69,42 +67,29 @@ public final class Server {
@SuppressWarnings("checkstyle:MagicNumber") @SuppressWarnings("checkstyle:MagicNumber")
private static Options createOptions(String... args) { private static Options createOptions(String... args) {
if (args.length < 1) { if (args.length != 6) {
throw new IllegalArgumentException("Missing client version"); throw new IllegalArgumentException("Expecting 6 parameters");
}
String clientVersion = args[0];
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
throw new IllegalArgumentException(
"The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")");
}
if (args.length != 8) {
throw new IllegalArgumentException("Expecting 8 parameters");
} }
Options options = new Options(); Options options = new Options();
int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8 int maxSize = Integer.parseInt(args[0]) & ~7; // multiple of 8
options.setMaxSize(maxSize); options.setMaxSize(maxSize);
int bitRate = Integer.parseInt(args[2]); int bitRate = Integer.parseInt(args[1]);
options.setBitRate(bitRate); options.setBitRate(bitRate);
int maxFps = Integer.parseInt(args[3]);
options.setMaxFps(maxFps);
// use "adb forward" instead of "adb tunnel"? (so the server must listen) // use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(args[4]); boolean tunnelForward = Boolean.parseBoolean(args[2]);
options.setTunnelForward(tunnelForward); options.setTunnelForward(tunnelForward);
Rect crop = parseCrop(args[5]); Rect crop = parseCrop(args[3]);
options.setCrop(crop); options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[6]); boolean sendFrameMeta = Boolean.parseBoolean(args[4]);
options.setSendFrameMeta(sendFrameMeta); options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[7]); boolean control = Boolean.parseBoolean(args[5]);
options.setControl(control); options.setControl(control);
return options; return options;
@@ -135,25 +120,11 @@ public final class Server {
} }
} }
private static void suggestFix(Throwable e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (e instanceof MediaCodec.CodecException) {
MediaCodec.CodecException mce = (MediaCodec.CodecException) e;
if (mce.getErrorCode() == 0xfffffc0e) {
Ln.e("The hardware encoder is not able to encode at the given definition.");
Ln.e("Try with a lower definition:");
Ln.e(" scrcpy -m 1024");
}
}
}
}
public static void main(String... args) throws Exception { public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override @Override
public void uncaughtException(Thread t, Throwable e) { public void uncaughtException(Thread t, Throwable e) {
Ln.e("Exception on thread " + t, e); Ln.e("Exception on thread " + t, e);
suggestFix(e);
} }
}); });

View File

@@ -38,7 +38,8 @@ public final class Size {
return false; return false;
} }
Size size = (Size) o; Size size = (Size) o;
return width == size.width && height == size.height; return width == size.width
&& height == size.height;
} }
@Override @Override
@@ -48,6 +49,9 @@ public final class Size {
@Override @Override
public String toString() { public String toString() {
return "Size{" + "width=" + width + ", height=" + height + '}'; return "Size{"
+ "width=" + width
+ ", height=" + height
+ '}';
} }
} }

View File

@@ -1,79 +0,0 @@
package com.genymobile.scrcpy;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Looper;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public final class Workarounds {
private Workarounds() {
// not instantiable
}
public static void prepareMainLooper() {
// Some devices internally create a Handler when creating an input Surface, causing an exception:
// "Can't create handler inside thread that has not called Looper.prepare()"
// <https://github.com/Genymobile/scrcpy/issues/240>
//
// Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException:
// "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue'
// on a null object reference"
// <https://github.com/Genymobile/scrcpy/issues/921>
Looper.prepareMainLooper();
}
@SuppressLint("PrivateApi")
public static void fillAppInfo() {
try {
// ActivityThread activityThread = new ActivityThread();
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
activityThreadConstructor.setAccessible(true);
Object activityThread = activityThreadConstructor.newInstance();
// ActivityThread.sCurrentActivityThread = activityThread;
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
sCurrentActivityThreadField.set(null, activityThread);
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
Constructor<?> appBindDataConstructor = appBindDataClass.getDeclaredConstructor();
appBindDataConstructor.setAccessible(true);
Object appBindData = appBindDataConstructor.newInstance();
ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.packageName = "com.genymobile.scrcpy";
// appBindData.appInfo = applicationInfo;
Field appInfoField = appBindDataClass.getDeclaredField("appInfo");
appInfoField.setAccessible(true);
appInfoField.set(appBindData, applicationInfo);
// activityThread.mBoundApplication = appBindData;
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
mBoundApplicationField.setAccessible(true);
mBoundApplicationField.set(activityThread, appBindData);
// Context ctx = activityThread.getSystemContext();
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
Context ctx = (Context) getSystemContextMethod.invoke(activityThread);
Application app = Instrumentation.newApplication(Application.class, ctx);
// activityThread.mInitialApplication = app;
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
mInitialApplicationField.setAccessible(true);
mInitialApplicationField.set(activityThread, app);
} catch (Throwable throwable) {
// this is a workaround, so failing is not an error
Ln.w("Could not fill app info: " + throwable.getMessage());
}
}
}

View File

@@ -22,37 +22,47 @@ public class ClipboardManager {
this.manager = manager; this.manager = manager;
} }
private Method getGetPrimaryClipMethod() throws NoSuchMethodException { private Method getGetPrimaryClipMethod() {
if (getPrimaryClipMethod == null) { if (getPrimaryClipMethod == null) {
try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
} else { } else {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
} }
} catch (NoSuchMethodException e) {
Ln.e("Could not find method", e);
}
} }
return getPrimaryClipMethod; return getPrimaryClipMethod;
} }
private Method getSetPrimaryClipMethod() throws NoSuchMethodException { private Method getSetPrimaryClipMethod() {
if (setPrimaryClipMethod == null) { if (setPrimaryClipMethod == null) {
try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
} else { } else {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class,
String.class, int.class);
}
} catch (NoSuchMethodException e) {
Ln.e("Could not find method", e);
} }
} }
return setPrimaryClipMethod; return setPrimaryClipMethod;
} }
private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException { private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException,
IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return (ClipData) method.invoke(manager, PACKAGE_NAME); return (ClipData) method.invoke(manager, PACKAGE_NAME);
} }
return (ClipData) method.invoke(manager, PACKAGE_NAME, USER_ID); return (ClipData) method.invoke(manager, PACKAGE_NAME, USER_ID);
} }
private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) throws InvocationTargetException,
throws InvocationTargetException, IllegalAccessException { IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, clipData, PACKAGE_NAME); method.invoke(manager, clipData, PACKAGE_NAME);
} else { } else {
@@ -61,26 +71,32 @@ public class ClipboardManager {
} }
public CharSequence getText() { public CharSequence getText() {
try {
Method method = getGetPrimaryClipMethod(); Method method = getGetPrimaryClipMethod();
if (method == null) {
return null;
}
try {
ClipData clipData = getPrimaryClip(method, manager); ClipData clipData = getPrimaryClip(method, manager);
if (clipData == null || clipData.getItemCount() == 0) { if (clipData == null || clipData.getItemCount() == 0) {
return null; return null;
} }
return clipData.getItemAt(0).getText(); return clipData.getItemAt(0).getText();
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke " + method.getName(), e);
return null; return null;
} }
} }
public void setText(CharSequence text) { public void setText(CharSequence text) {
try {
Method method = getSetPrimaryClipMethod(); Method method = getSetPrimaryClipMethod();
if (method == null) {
return;
}
ClipData clipData = ClipData.newPlainText(null, text); ClipData clipData = ClipData.newPlainText(null, text);
try {
setPrimaryClip(method, manager, clipData); setPrimaryClip(method, manager, clipData);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke " + method.getName(), e);
} }
} }
} }

View File

@@ -21,19 +21,26 @@ public final class InputManager {
this.manager = manager; this.manager = manager;
} }
private Method getInjectInputEventMethod() throws NoSuchMethodException { private Method getInjectInputEventMethod() {
if (injectInputEventMethod == null) { if (injectInputEventMethod == null) {
try {
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
} catch (NoSuchMethodException e) {
Ln.e("Could not find method", e);
}
} }
return injectInputEventMethod; return injectInputEventMethod;
} }
public boolean injectInputEvent(InputEvent inputEvent, int mode) { public boolean injectInputEvent(InputEvent inputEvent, int mode) {
try {
Method method = getInjectInputEventMethod(); Method method = getInjectInputEventMethod();
return (boolean) method.invoke(manager, inputEvent, mode); if (method == null) {
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { return false;
Ln.e("Could not invoke method", e); }
try {
return (Boolean) method.invoke(manager, inputEvent, mode);
} catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke " + method.getName(), e);
return false; return false;
} }
} }

View File

@@ -17,21 +17,28 @@ public final class PowerManager {
this.manager = manager; this.manager = manager;
} }
private Method getIsScreenOnMethod() throws NoSuchMethodException { private Method getIsScreenOnMethod() {
if (isScreenOnMethod == null) { if (isScreenOnMethod == null) {
try {
@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);
} catch (NoSuchMethodException e) {
Ln.e("Could not find method", e);
}
} }
return isScreenOnMethod; return isScreenOnMethod;
} }
public boolean isScreenOn() { public boolean isScreenOn() {
try {
Method method = getIsScreenOnMethod(); Method method = getIsScreenOnMethod();
return (boolean) method.invoke(manager); if (method == null) {
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { return false;
Ln.e("Could not invoke method", e); }
try {
return (Boolean) method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke " + method.getName(), e);
return false; return false;
} }
} }

View File

@@ -17,35 +17,49 @@ public class StatusBarManager {
this.manager = manager; this.manager = manager;
} }
private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException { private Method getExpandNotificationsPanelMethod() {
if (expandNotificationsPanelMethod == null) { if (expandNotificationsPanelMethod == null) {
try {
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
} catch (NoSuchMethodException e) {
Ln.e("Could not find method", e);
}
} }
return expandNotificationsPanelMethod; return expandNotificationsPanelMethod;
} }
private Method getCollapsePanelsMethod() throws NoSuchMethodException { private Method getCollapsePanelsMethod() {
if (collapsePanelsMethod == null) { if (collapsePanelsMethod == null) {
try {
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
} catch (NoSuchMethodException e) {
Ln.e("Could not find method", e);
}
} }
return collapsePanelsMethod; return collapsePanelsMethod;
} }
public void expandNotificationsPanel() { public void expandNotificationsPanel() {
try {
Method method = getExpandNotificationsPanelMethod(); Method method = getExpandNotificationsPanelMethod();
if (method == null) {
return;
}
try {
method.invoke(manager); method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke " + method.getName(), e);
} }
} }
public void collapsePanels() { public void collapsePanels() {
try {
Method method = getCollapsePanelsMethod(); Method method = getCollapsePanelsMethod();
if (method == null) {
return;
}
try {
method.invoke(manager); method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke " + method.getName(), e);
} }
} }
} }

View File

@@ -84,8 +84,9 @@ public final class SurfaceControl {
} }
} }
private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException { private static Method getGetBuiltInDisplayMethod() {
if (getBuiltInDisplayMethod == null) { if (getBuiltInDisplayMethod == null) {
try {
// the method signature has changed in Android Q // the method signature has changed in Android Q
// <https://github.com/Genymobile/scrcpy/issues/586> // <https://github.com/Genymobile/scrcpy/issues/586>
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@@ -93,14 +94,19 @@ public final class SurfaceControl {
} else { } else {
getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken"); getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken");
} }
} catch (NoSuchMethodException e) {
Ln.e("Could not find method", e);
}
} }
return getBuiltInDisplayMethod; return getBuiltInDisplayMethod;
} }
public static IBinder getBuiltInDisplay() { public static IBinder getBuiltInDisplay() {
try {
Method method = getGetBuiltInDisplayMethod(); Method method = getGetBuiltInDisplayMethod();
if (method == null) {
return null;
}
try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// call getBuiltInDisplay(0) // call getBuiltInDisplay(0)
return (IBinder) method.invoke(null, 0); return (IBinder) method.invoke(null, 0);
@@ -108,25 +114,32 @@ public final class SurfaceControl {
// call getInternalDisplayToken() // call getInternalDisplayToken()
return (IBinder) method.invoke(null); return (IBinder) method.invoke(null);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke " + method.getName(), e);
return null; return null;
} }
} }
private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException { private static Method getSetDisplayPowerModeMethod() {
if (setDisplayPowerModeMethod == null) { if (setDisplayPowerModeMethod == null) {
try {
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class); setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);
} catch (NoSuchMethodException e) {
Ln.e("Could not find method", e);
}
} }
return setDisplayPowerModeMethod; return setDisplayPowerModeMethod;
} }
public static void setDisplayPowerMode(IBinder displayToken, int mode) { public static void setDisplayPowerMode(IBinder displayToken, int mode) {
try {
Method method = getSetDisplayPowerModeMethod(); Method method = getSetDisplayPowerModeMethod();
if (method == null) {
return;
}
try {
method.invoke(null, displayToken, mode); method.invoke(null, displayToken, mode);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke " + method.getName(), e);
} }
} }

View File

@@ -1,95 +1,27 @@
package com.genymobile.scrcpy.wrappers; package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.os.IInterface; import android.os.IInterface;
import android.view.IRotationWatcher; import android.view.IRotationWatcher;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public final class WindowManager { public final class WindowManager {
private final IInterface manager; private final IInterface manager;
private Method getRotationMethod;
private Method freezeRotationMethod;
private Method isRotationFrozenMethod;
private Method thawRotationMethod;
public WindowManager(IInterface manager) { public WindowManager(IInterface manager) {
this.manager = manager; this.manager = manager;
} }
private Method getGetRotationMethod() throws NoSuchMethodException {
if (getRotationMethod == null) {
Class<?> cls = manager.getClass();
try {
// method changed since this commit:
// https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2
getRotationMethod = cls.getMethod("getDefaultDisplayRotation");
} catch (NoSuchMethodException e) {
// old version
getRotationMethod = cls.getMethod("getRotation");
}
}
return getRotationMethod;
}
private Method getFreezeRotationMethod() throws NoSuchMethodException {
if (freezeRotationMethod == null) {
freezeRotationMethod = manager.getClass().getMethod("freezeRotation", int.class);
}
return freezeRotationMethod;
}
private Method getIsRotationFrozenMethod() throws NoSuchMethodException {
if (isRotationFrozenMethod == null) {
isRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen");
}
return isRotationFrozenMethod;
}
private Method getThawRotationMethod() throws NoSuchMethodException {
if (thawRotationMethod == null) {
thawRotationMethod = manager.getClass().getMethod("thawRotation");
}
return thawRotationMethod;
}
public int getRotation() { public int getRotation() {
try { try {
Method method = getGetRotationMethod(); Class<?> cls = manager.getClass();
return (int) method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return 0;
}
}
public void freezeRotation(int rotation) {
try { try {
Method method = getFreezeRotationMethod(); return (Integer) cls.getMethod("getRotation").invoke(manager);
method.invoke(manager, rotation); } catch (NoSuchMethodException e) {
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { // method changed since this commit:
Ln.e("Could not invoke method", e); // https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2
return (Integer) cls.getMethod("getDefaultDisplayRotation").invoke(manager);
} }
} } catch (Exception e) {
throw new AssertionError(e);
public boolean isRotationFrozen() {
try {
Method method = getIsRotationFrozenMethod();
return (boolean) method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return false;
}
}
public void thawRotation() {
try {
Method method = getThawRotationMethod();
method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
} }
} }
@@ -97,12 +29,11 @@ public final class WindowManager {
try { try {
Class<?> cls = manager.getClass(); Class<?> cls = manager.getClass();
try { try {
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
} catch (NoSuchMethodException e) {
// display parameter added since this commit: // display parameter added since this commit:
// https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1 // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0); cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0);
} catch (NoSuchMethodException e) {
// old version
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
} }
} catch (Exception e) { } catch (Exception e) {
throw new AssertionError(e); throw new AssertionError(e);

View File

@@ -240,22 +240,6 @@ public class ControlMessageReaderTest {
Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction()); Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction());
} }
@Test
public void testParseRotateDevice() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_ROTATE_DEVICE);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType());
}
@Test @Test
public void testMultiEvents() throws IOException { public void testMultiEvents() throws IOException {
ControlMessageReader reader = new ControlMessageReader(); ControlMessageReader reader = new ControlMessageReader();