Merge pull request #13 from Genymobile/master

update
This commit is contained in:
feiser2016 2019-11-20 09:22:03 +08:00 committed by GitHub
commit aca0632d52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
94 changed files with 2910 additions and 1056 deletions

View file

@ -43,7 +43,7 @@ Install the required packages from your package manager.
sudo apt install ffmpeg libsdl2-2.0-0
# client build dependencies
sudo apt install make gcc git pkg-config meson ninja-build \
sudo apt install gcc git pkg-config meson ninja-build \
libavcodec-dev libavformat-dev libavutil-dev \
libsdl2-dev
@ -70,7 +70,7 @@ sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-rele
sudo dnf install SDL2-devel ffms2-devel meson gcc make
# server build dependencies
sudo dnf install java
sudo dnf install java-devel
```
@ -195,8 +195,7 @@ Then, build:
```bash
meson x --buildtype release --strip -Db_lto=true
cd x
ninja
ninja -Cx
```
_Note: `ninja` [must][ninja-user] be run as a non-root user (only `ninja
@ -219,13 +218,13 @@ To run without installing:
After a successful build, you can install _scrcpy_ on the system:
```bash
sudo ninja install # without sudo on Windows
sudo ninja -Cx install # without sudo on Windows
```
This installs two files:
- `/usr/local/bin/scrcpy`
- `/usr/local/share/scrcpy/scrcpy-server.jar`
- `/usr/local/share/scrcpy/scrcpy-server`
Just remove them to "uninstall" the application.
@ -234,18 +233,17 @@ You can then [run](README.md#run) _scrcpy_.
## Prebuilt server
- [`scrcpy-server-v1.9.jar`][direct-scrcpy-server]
_(SHA-256: ad7e539f100e48259b646f26982bc63e0a60a81ac87ae135e242855bef69bd1a)_
- [`scrcpy-server-v1.11`][direct-scrcpy-server]
_(SHA-256: ff3a454012e91d9185cfe8ca7691cea16c43a7dcc08e92fa47ab9f0ea675abd1)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-server-v1.9.jar
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-server-v1.11
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:
```bash
meson x --buildtype release --strip -Db_lto=true \
-Dprebuilt_server=/path/to/scrcpy-server.jar
cd x
ninja
sudo ninja install
-Dprebuilt_server=/path/to/scrcpy-server
ninja -Cx
sudo ninja -Cx install
```

View file

@ -3,7 +3,7 @@
## Overview
This application is composed of two parts:
- the server (`scrcpy-server.jar`), to be executed on the device,
- the server (`scrcpy-server`), to be executed on the device,
- the client (the `scrcpy` binary), executed on the host computer.
The client is responsible to push the server to the device and start its
@ -49,7 +49,7 @@ application may not replace the server just before the client executes it._
Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing
`classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle
build system, the server is built to an (unsigned) APK (renamed to
`scrcpy-server.jar`).
`scrcpy-server`).
[dex]: https://en.wikipedia.org/wiki/Dalvik_(software)
[apk]: https://en.wikipedia.org/wiki/Android_application_package
@ -268,3 +268,33 @@ For more details, go read the code!
If you find a bug, or have an awesome idea to implement, please discuss and
contribute ;-)
### Debug the server
The server is pushed to the device by the client on startup.
To debug it, enable the server debugger during configuration:
```bash
meson x -Dserver_debugger=true
# or, if x is already configured
meson configure x -Dserver_debugger=true
```
Then recompile.
When you start scrcpy, it will start a debugger on port 5005 on the device.
Redirect that port to the computer:
```bash
adb forward tcp:5005 tcp:5005
```
In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on
`+`, _Remote_, and fill the form:
- Host: `localhost`
- Port: `5005`
Then click on _Debug_.

44
FAQ.md
View file

@ -1,10 +1,5 @@
# Frequently Asked Questions
## Common issues
The application is very young, it is not unlikely that you encounter problems
with it.
Here are the common reported problems and their status.
@ -20,9 +15,13 @@ Windows may need some [drivers] to detect your device.
[drivers]: https://developer.android.com/studio/run/oem-usb.html
### Mouse clicks do not work
### I can only mirror, I cannot interact with the device
On some devices, you may need to enable an option to allow [simulating input].
In developer options, enable:
> **USB debugging (Security settings)**
> _Allow granting permissions and simulating input via USB debugging_
[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
@ -43,6 +42,16 @@ meson x --buildtype release -Dhidpi_support=false
However, the video will be displayed at lower resolution.
### The quality is low on HiDPI display
On Windows, you may need to configure the [scaling behavior].
> `scrcpy.exe` > Properties > Compatibility > Change high DPI settings >
> Override high DPI scaling behavior > Scaling performed by: _Application_.
[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723
### KWin compositor crashes
On Plasma Desktop, compositor is disabled while _scrcpy_ is running.
@ -50,3 +59,26 @@ On Plasma Desktop, compositor is disabled while _scrcpy_ is running.
As a workaround, [disable "Block compositing"][kwin].
[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613
### I get an error "Could not open video stream"
There may be many reasons. One common cause is that the hardware encoder of your
device is not able to encode at the given definition:
```
ERROR: Exception on thread Thread[main,5,main]
android.media.MediaCodec$CodecException: Error 0xfffffc0e
...
Exit due to uncaughtException in main thread:
ERROR: Could not open video stream
INFO: Initial texture: 1080x2336
```
Just try with a lower definition:
```
scrcpy -m 1920
scrcpy -m 1024
scrcpy -m 800
```

View file

@ -188,6 +188,7 @@
identification within third-party archives.
Copyright (C) 2018 Genymobile
Copyright (C) 2018-2019 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -3,7 +3,7 @@
#
# Here, "portable" means that the client and server binaries are expected to be
# anywhere, but in the same directory, instead of well-defined separate
# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server.jar).
# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server).
#
# In particular, this implies to change the location from where the client push
# the server to the device.
@ -44,7 +44,7 @@ clean:
build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
meson "$(SERVER_BUILD_DIR)" \
--buildtype release -Dbuild_app=false )
--buildtype release -Dcompile_app=false )
ninja -C "$(SERVER_BUILD_DIR)"
prepare-deps-win32:
@ -56,7 +56,7 @@ build-win32: prepare-deps-win32
--cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Dcompile_server=false \
-Dportable=true )
ninja -C "$(WIN32_BUILD_DIR)"
@ -66,7 +66,7 @@ build-win32-noconsole: prepare-deps-win32
--cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Dcompile_server=false \
-Dwindows_noconsole=true \
-Dportable=true )
ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)"
@ -80,7 +80,7 @@ build-win64: prepare-deps-win64
--cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Dcompile_server=false \
-Dportable=true )
ninja -C "$(WIN64_BUILD_DIR)"
@ -90,47 +90,48 @@ build-win64-noconsole: prepare-deps-win64
--cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Dcompile_server=false \
-Dwindows_noconsole=true \
-Dportable=true )
ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)"
dist-win32: build-server build-win32 build-win32-noconsole
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(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_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-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.2.1-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.2.1-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/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.8/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.10/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64 build-win64-noconsole
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(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_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-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.2.1-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.2.1-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/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.8/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.10/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32
cd "$(DIST)"; \
zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)"
cd "$(DIST)/$(WIN32_TARGET_DIR)"; \
zip -r "../$(WIN32_TARGET)" .
zip-win64: dist-win64
cd "$(DIST)"; \
zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)"
cd "$(DIST)/$(WIN64_TARGET_DIR)"; \
zip -r "../$(WIN64_TARGET)" .
sums:
cd "$(DIST)"; \

335
README.md
View file

@ -1,15 +1,26 @@
# scrcpy (v1.9)
# scrcpy (v1.11)
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.
It works on _GNU/Linux_, _Windows_ and _MacOS_.
It works on _GNU/Linux_, _Windows_ and _macOS_.
![screenshot](assets/screenshot-debian-600.jpg)
It focuses on:
- **lightness** (native, displays only the device screen)
- **performance** (30~60fps)
- **quality** (1920×1080 or above)
- **low latency** ([35~70ms][lowlatency])
- **low startup time** (~1 second to display the first image)
- **non-intrusiveness** (nothing is left installed on the device)
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
## Requirements
The Android part requires at least API 21 (Android 5.0).
The Android device requires at least API 21 (Android 5.0).
Make sure you [enabled adb debugging][enable-adb] on your device(s).
@ -51,18 +62,18 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
For Windows, for simplicity, prebuilt archives with all the dependencies
(including `adb`) are available:
- [`scrcpy-win32-v1.9.zip`][direct-win32]
_(SHA-256: 3234f7fbcc26b9e399f50b5ca9ed085708954c87fda1b0dd32719d6e7dd861ef)_
- [`scrcpy-win64-v1.9.zip`][direct-win64]
_(SHA-256: 0088eca1811ea7c7ac350d636c8465b266e6c830bb268770ff88fddbb493077e)_
- [`scrcpy-win32-v1.11.zip`][direct-win32]
_(SHA-256: f25ed46e6f3e81e0ff9b9b4df7fe1a4bbd13f8396b7391be0a488b64c675b41e)_
- [`scrcpy-win64-v1.11.zip`][direct-win64]
_(SHA-256: 3802c9ea0307d437947ff150ec65e53990b0beaacd0c8d0bed19c7650ce141bd)_
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win32-v1.9.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win64-v1.9.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-win32-v1.11.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-win64-v1.11.zip
You can also [build the app manually][BUILD].
### Mac OS
### macOS
The application is available in [Homebrew]. Just install it:
@ -97,70 +108,55 @@ scrcpy --help
## Features
### Capture configuration
### Reduce size
#### Reduce size
Sometimes, it is useful to mirror an Android device at a lower definition to
increase performances.
increase performance.
To limit both width and height to some value (e.g. 1024):
To limit both the width and height to some value (e.g. 1024):
```bash
scrcpy --max-size 1024
scrcpy -m 1024 # short version
```
The other dimension is computed to that the device aspect-ratio is preserved.
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.
### Change bit-rate
#### Change bit-rate
The default bit-rate is 8Mbps. To change the video bitrate (e.g. to 2Mbps):
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
```bash
scrcpy --bit-rate 2M
scrcpy -b 2M # short version
```
#### Limit frame rate
### Crop
On devices with Android >= 10, the capture frame rate can be limited:
```bash
scrcpy --max-fps 15
```
#### Crop
The device screen may be cropped to mirror only part of the screen.
This is useful for example to mirror only 1 eye of the Oculus Go:
This is useful for example to mirror only one eye of the Oculus Go:
```bash
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.
### 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
### Recording
It is possible to record the screen while mirroring:
@ -185,7 +181,31 @@ variation] does not impact the recorded file.
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
### Multi-devices
### Connection
#### 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_:
@ -196,8 +216,65 @@ scrcpy -s 0123456789abcdef # short version
You can start several instances of _scrcpy_ for several devices.
#### SSH tunnel
### Fullscreen
To connect to a remote device, it is possible to connect a local `adb` client to
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:
@ -209,17 +286,45 @@ scrcpy -f # short version
Fullscreen can then be toggled dynamically with `Ctrl`+`f`.
### Always on top
### Other mirroring options
The window of app can always be above others by:
#### Read-only
To disable controls (everything which can interact with the device: input keys,
mouse events, drag&drop files):
```bash
scrcpy --always-on-top
scrcpy -T # short version
scrcpy --no-control
scrcpy -n
```
#### Turn screen off
### Show touches
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
```
#### Show touches
For presentations, it may be useful to show physical touches (on the physical
device).
@ -236,7 +341,43 @@ scrcpy -t
Note that it only shows _physical_ touches (with the finger on the device).
### Install APK
### Input control
#### 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_
window.
@ -244,87 +385,52 @@ window.
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
_scrcpy_ window.
There is no visual feedback, a log is printed to the console.
### Read-only
To disable controls (everything which can interact with the device: input keys,
mouse events, drag&drop files):
The target directory can be changed on start:
```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
scrcpy --push-target /sdcard/foo/bar/
```
### Forward audio
### Audio forwarding
Audio is not forwarded by _scrcpy_.
Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only).
There is a limited solution using [AOA], implemented in the [`audio`] branch. If
you are interested, see [issue 14].
Also see [issue #14].
[AOA]: https://source.android.com/devices/accessories/aoa2
[`audio`]: https://github.com/Genymobile/scrcpy/commits/audio
[issue 14]: https://github.com/Genymobile/scrcpy/issues/14
[USBaudio]: https://github.com/rom1v/usbaudio
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
## Shortcuts
| Action | Shortcut |
| -------------------------------------- |:---------------------------- |
| switch fullscreen mode | `Ctrl`+`f` |
| resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` |
| resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ |
| click on `HOME` | `Ctrl`+`h` \| _Middle-click_ |
| click on `BACK` | `Ctrl`+`b` \| _Right-click²_ |
| click on `APP_SWITCH` | `Ctrl`+`s` |
| click on `MENU` | `Ctrl`+`m` |
| click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ (`Cmd`+`↑` on MacOS) |
| click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on MacOS) |
| click on `POWER` | `Ctrl`+`p` |
| power on | _Right-click²_ |
| turn device screen off (keep mirroring)| `Ctrl`+`o` |
| expand notification panel | `Ctrl`+`n` |
| collapse notification panel | `Ctrl`+`Shift`+`n` |
| copy device clipboard to computer | `Ctrl`+`c` |
| paste computer clipboard to device | `Ctrl`+`v` |
| copy computer clipboard to device | `Ctrl`+`Shift`+`v` |
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
| Action | Shortcut | Shortcut (macOS)
| -------------------------------------- |:----------------------------- |:-----------------------------
| Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f`
| Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g`
| Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_
| Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_
| Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_
| Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s`
| Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m`
| Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_
| Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
| Power on | _Right-click²_ | _Right-click²_
| Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o`
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`
| Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v`
| Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v`
| Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i`
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._
@ -337,7 +443,7 @@ To use a specific _adb_ binary, configure its path in the environment variable
ADB=/path/to/adb scrcpy
To override the path of the `scrcpy-server.jar` file, configure its path in
To override the path of the `scrcpy-server` file, configure its path in
`SCRCPY_SERVER_PATH`.
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
@ -375,6 +481,7 @@ Read the [developers page].
## Licence
Copyright (C) 2018 Genymobile
Copyright (C) 2018-2019 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -3,10 +3,10 @@ src = [
'src/command.c',
'src/control_msg.c',
'src/controller.c',
'src/convert.c',
'src/decoder.c',
'src/device.c',
'src/device_msg.c',
'src/event_converter.c',
'src/file_handler.c',
'src/fps_counter.c',
'src/input_manager.c',
@ -93,7 +93,7 @@ conf.set_quoted('SCRCPY_VERSION', meson.project_version())
# the prefix used during configuration (meson --prefix=PREFIX)
conf.set_quoted('PREFIX', get_option('prefix'))
# build a "portable" version (with scrcpy-server.jar accessible from the same
# build a "portable" version (with scrcpy-server accessible from the same
# directory as the executable)
conf.set('PORTABLE', get_option('portable'))
@ -115,15 +115,16 @@ conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))
# disable console on Windows
conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole'))
# run a server debugger and wait for a client to be attached
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
configure_file(configuration: conf, output: 'config.h')
src_dir = include_directories('src')
if get_option('windows_noconsole')
c_args = [ '-mwindows' ]
link_args = [ '-mwindows' ]
link_args = [ '-Wl,--subsystem,windows' ]
else
c_args = []
link_args = []
endif
@ -131,9 +132,11 @@ executable('scrcpy', src,
dependencies: dependencies,
include_directories: src_dir,
install: true,
c_args: c_args,
c_args: [],
link_args: link_args)
install_man('scrcpy.1')
### TESTS
@ -150,6 +153,9 @@ tests = [
'tests/test_device_msg_deserialize.c',
'src/device_msg.c'
]],
['test_queue', [
'tests/test_queue.c',
]],
['test_strutil', [
'tests/test_strutil.c',
'src/str_util.c'

269
app/scrcpy.1 Normal file
View file

@ -0,0 +1,269 @@
.TH "scrcpy" "1"
.SH NAME
scrcpy \- Display and control your Android device
.SH SYNOPSIS
.B scrcpy
.RI [ options ]
.SH DESCRIPTION
.B scrcpy
provides display and control of Android devices connected on USB (or over TCP/IP). It does not require any root access.
.SH OPTIONS
.TP
.B \-\-always\-on\-top
Make scrcpy window always on top (above other windows).
.TP
.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).
Default is 8000000.
.TP
.BI \-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
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
.B \-\-max\-size
value is computed on the cropped size.
.TP
.B \-f, \-\-fullscreen
Start in fullscreen.
.TP
.B \-h, \-\-help
Print this help.
.TP
.BI \-\-max\-fps " value
Limit the framerate of screen capture (only supported on devices with Android >= 10).
.TP
.BI "\-m, \-\-max\-size " value
Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved.
Default is 0 (unlimited).
.TP
.B \-n, \-\-no\-control
Disable device control (mirror the device in read\-only).
.TP
.B \-N, \-\-no\-display
Do not display device (only when screen recording is enabled).
.TP
.BI "\-p, \-\-port " port
Set the TCP port the client listens on.
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
.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".
Default is "/sdcard/".
.TP
.BI "\-r, \-\-record " file
Record screen to
.IR file .
The format is determined by the
.B \-\-record\-format
option if set, or by the file extension (.mp4 or .mkv).
.TP
.BI \-\-record\-format " format
Force recording format (either mp4 or mkv).
.TP
.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.
.TP
.BI "\-s, \-\-serial " number
The device serial number. Mandatory only if several devices are connected to adb.
.TP
.B \-S, \-\-turn\-screen\-off
Turn the device screen off immediately.
.TP
.B \-t, \-\-show\-touches
Enable "show touches" on start, disable on quit.
It only shows physical touches (not clicks from scrcpy).
.TP
.B \-v, \-\-version
Print the version of scrcpy.
.TP
.B \-\-window\-borderless
Disable window decorations (display borderless window).
.TP
.BI \-\-window\-title " text
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
.TP
.B Ctrl+f
switch fullscreen mode
.TP
.B Ctrl+g
resize window to 1:1 (pixel\-perfect)
.TP
.B Ctrl+x, Double\-click on black borders
resize window to remove black borders
.TP
.B Ctrl+h, Home, Middle\-click
Click on HOME
.TP
.B Ctrl+b, Ctrl+Backspace, Right\-click (when screen is on)
Click on BACK
.TP
.B Ctrl+s
Click on APP_SWITCH
.TP
.B Ctrl+m
Click on MENU
.TP
.B Ctrl+Up
Click on VOLUME_UP
.TP
.B Ctrl+Down
Click on VOLUME_DOWN
.TP
.B Ctrl+p
Click on POWER (turn screen on/off)
.TP
.B Right\-click (when screen is off)
turn screen on
.TP
.B Ctrl+o
turn device screen off (keep mirroring)
.TP
.B Ctrl+n
expand notification panel
.TP
.B Ctrl+Shift+n
collapse notification panel
.TP
.B Ctrl+c
copy device clipboard to computer
.TP
.B Ctrl+v
paste computer clipboard to device
.TP
.B Ctrl+Shift+v
copy computer clipboard to device
.TP
.B Ctrl+i
enable/disable FPS counter (print frames/second in logs)
.TP
.B Drag & drop APK file
install APK from computer
.SH Environment variables
.TP
.B ADB
Specify the path to adb.
.TP
.B SCRCPY_SERVER_PATH
Specify the path to server binary.
.SH AUTHORS
.B scrcpy
is written by Romain Vimont.
This manual page was written by
.MT mmyangfl@gmail.com
Yangfl
.ME
for the Debian Project (and may be used by others).
.SH "REPORTING BUGS"
Report bugs to
.UR https://github.com/Genymobile/scrcpy/issues
.UE .
.SH COPYRIGHT
Copyright \(co 2018 Genymobile
.UR https://www.genymobile.com
Genymobile
.UE
Copyright \(co 2018\-2019
.MT rom@rom1v.com
Romain Vimont
.ME
Licensed under the Apache License, Version 2.0.
.SH WWW
.UR https://github.com/Genymobile/scrcpy
.UE

View file

@ -4,6 +4,8 @@
#include <stdbool.h>
#include <stdint.h>
#include "config.h"
static inline void
buffer_write16be(uint8_t *buf, uint16_t value) {
buf[0] = value >> 8;
@ -18,6 +20,12 @@ buffer_write32be(uint8_t *buf, uint32_t value) {
buf[3] = value;
}
static inline void
buffer_write64be(uint8_t *buf, uint64_t value) {
buffer_write32be(buf, value >> 32);
buffer_write32be(&buf[4], (uint32_t) value);
}
static inline uint16_t
buffer_read16be(const uint8_t *buf) {
return (buf[0] << 8) | buf[1];

View file

@ -5,8 +5,10 @@
#include <stdbool.h>
#include <unistd.h>
#include "config.h"
// To define a circular buffer type of 20 ints:
// typedef CBUF(int, 20) my_cbuf_t;
// struct cbuf_int CBUF(int, 20);
//
// data has length CAP + 1 to distinguish empty vs full.
#define CBUF(TYPE, CAP) { \
@ -35,7 +37,7 @@
(PCBUF)->head = ((PCBUF)->head + 1) % cbuf_size_(PCBUF); \
} \
ok; \
}) \
})
#define cbuf_take(PCBUF, PITEM) \
({ \

View file

@ -5,6 +5,7 @@
#include <stdlib.h>
#include <string.h>
#include "config.h"
#include "common.h"
#include "log.h"
#include "str_util.h"

View file

@ -33,6 +33,8 @@
#endif
#include "config.h"
# define NO_EXIT_CODE -1
enum process_result {

View file

@ -3,6 +3,8 @@
#include <stdint.h>
#include "config.h"
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
#define MIN(X,Y) (X) < (Y) ? (X) : (Y)
#define MAX(X,Y) (X) > (Y) ? (X) : (Y)

View file

@ -1,7 +1,9 @@
#include "control_msg.h"
#include <string.h>
#include <SDL2/SDL_assert.h>
#include "config.h"
#include "buffer_util.h"
#include "log.h"
#include "str_util.h"
@ -23,6 +25,16 @@ write_string(const char *utf8, size_t max_len, unsigned char *buf) {
return 2 + len;
}
static uint16_t
to_fixed_point_16(float f) {
SDL_assert(f >= 0.0f && f <= 1.0f);
uint32_t u = f * 0x1p16f; // 2^16
if (u >= 0xffff) {
u = 0xffff;
}
return (uint16_t) u;
}
size_t
control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
buf[0] = msg->type;
@ -37,11 +49,15 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
CONTROL_MSG_TEXT_MAX_LENGTH, &buf[1]);
return 1 + len;
}
case CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT:
buf[1] = msg->inject_mouse_event.action;
buffer_write32be(&buf[2], msg->inject_mouse_event.buttons);
write_position(&buf[6], &msg->inject_mouse_event.position);
return 18;
case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT:
buf[1] = msg->inject_touch_event.action;
buffer_write64be(&buf[2], msg->inject_touch_event.pointer_id);
write_position(&buf[10], &msg->inject_touch_event.position);
uint16_t pressure =
to_fixed_point_16(msg->inject_touch_event.pressure);
buffer_write16be(&buf[22], pressure);
buffer_write32be(&buf[24], msg->inject_touch_event.buttons);
return 28;
case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
write_position(&buf[1], &msg->inject_scroll_event.position);
buffer_write32be(&buf[13],

View file

@ -5,6 +5,7 @@
#include <stddef.h>
#include <stdint.h>
#include "config.h"
#include "android/input.h"
#include "android/keycodes.h"
#include "common.h"
@ -14,10 +15,12 @@
#define CONTROL_MSG_SERIALIZED_MAX_SIZE \
(3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH)
#define POINTER_ID_MOUSE UINT64_C(-1);
enum control_msg_type {
CONTROL_MSG_TYPE_INJECT_KEYCODE,
CONTROL_MSG_TYPE_INJECT_TEXT,
CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT,
CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
@ -47,8 +50,10 @@ struct control_msg {
struct {
enum android_motionevent_action action;
enum android_motionevent_buttons buttons;
uint64_t pointer_id;
struct position position;
} inject_mouse_event;
float pressure;
} inject_touch_event;
struct {
struct position position;
int32_t hscroll;

View file

@ -91,7 +91,7 @@ run_controller(void *data) {
bool ok = process_msg(controller, &msg);
control_msg_destroy(&msg);
if (!ok) {
LOGD("Cannot write msg to socket");
LOGD("Could not write msg to socket");
break;
}
}

View file

@ -5,6 +5,7 @@
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "config.h"
#include "cbuf.h"
#include "control_msg.h"
#include "net.h"

View file

@ -1,41 +0,0 @@
#ifndef CONVERT_H
#define CONVERT_H
#include <stdbool.h>
#include <SDL2/SDL_events.h>
#include "control_msg.h"
struct complete_mouse_motion_event {
SDL_MouseMotionEvent *mouse_motion_event;
struct size screen_size;
};
struct complete_mouse_wheel_event {
SDL_MouseWheelEvent *mouse_wheel_event;
struct point position;
};
bool
input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
struct control_msg *to);
bool
mouse_button_from_sdl_to_android(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
mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
struct size screen_size,
struct control_msg *to);
// on Android, a scroll event requires the current mouse position
bool
mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
struct position position,
struct control_msg *to);
#endif

View file

@ -8,8 +8,8 @@
#include <SDL2/SDL_thread.h>
#include <unistd.h>
#include "compat.h"
#include "config.h"
#include "compat.h"
#include "buffer_util.h"
#include "events.h"
#include "lock_util.h"

View file

@ -4,6 +4,8 @@
#include <stdbool.h>
#include <libavformat/avformat.h>
#include "config.h"
struct video_buffer;
struct decoder {

View file

@ -1,4 +1,6 @@
#include "device.h"
#include "config.h"
#include "log.h"
bool

View file

@ -3,11 +3,11 @@
#include <stdbool.h>
#include "config.h"
#include "common.h"
#include "net.h"
#define DEVICE_NAME_FIELD_LENGTH 64
#define DEVICE_SDCARD_PATH "/sdcard/"
// name must be at least DEVICE_NAME_FIELD_LENGTH bytes
bool

View file

@ -3,6 +3,7 @@
#include <string.h>
#include <SDL2/SDL_assert.h>
#include "config.h"
#include "buffer_util.h"
#include "log.h"

View file

@ -5,6 +5,8 @@
#include <stdint.h>
#include <unistd.h>
#include "config.h"
#define DEVICE_MSG_TEXT_MAX_LENGTH 4093
#define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH)

View file

@ -1,9 +1,11 @@
#include "convert.h"
#include "event_converter.h"
#include "config.h"
#define MAP(FROM, TO) case FROM: *to = TO; return true
#define FAIL default: return false
static bool
bool
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
switch (from) {
MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN);
@ -31,8 +33,7 @@ autocomplete_metastate(enum android_metastate metastate) {
return metastate;
}
static enum android_metastate
enum android_metastate
convert_meta_state(SDL_Keymod mod) {
enum android_metastate metastate = 0;
if (mod & KMOD_LSHIFT) {
@ -73,8 +74,9 @@ convert_meta_state(SDL_Keymod mod) {
return autocomplete_metastate(metastate);
}
static bool
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
bool
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
bool prefer_text) {
switch (from) {
MAP(SDLK_RETURN, AKEYCODE_ENTER);
MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER);
@ -91,6 +93,12 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN);
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)) {
return false;
}
@ -127,16 +135,7 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
}
}
static bool
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
switch (from) {
MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN);
MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP);
FAIL;
}
}
static enum android_motionevent_buttons
enum android_motionevent_buttons
convert_mouse_buttons(uint32_t state) {
enum android_motionevent_buttons buttons = 0;
if (state & SDL_BUTTON_LMASK) {
@ -148,81 +147,30 @@ convert_mouse_buttons(uint32_t state) {
if (state & SDL_BUTTON_MMASK) {
buttons |= AMOTION_EVENT_BUTTON_TERTIARY;
}
if (state & SDL_BUTTON_X1) {
if (state & SDL_BUTTON_X1MASK) {
buttons |= AMOTION_EVENT_BUTTON_BACK;
}
if (state & SDL_BUTTON_X2) {
if (state & SDL_BUTTON_X2MASK) {
buttons |= AMOTION_EVENT_BUTTON_FORWARD;
}
return buttons;
}
bool
input_key_from_sdl_to_android(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;
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
switch (from) {
MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN);
MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP);
FAIL;
}
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;
}
bool
mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
struct size screen_size,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT;
if (!convert_mouse_action(from->type, &to->inject_mouse_event.action)) {
return false;
convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) {
switch (from) {
MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE);
MAP(SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN);
MAP(SDL_FINGERUP, AMOTION_EVENT_ACTION_UP);
FAIL;
}
to->inject_mouse_event.buttons =
convert_mouse_buttons(SDL_BUTTON(from->button));
to->inject_mouse_event.position.screen_size = screen_size;
to->inject_mouse_event.position.point.x = from->x;
to->inject_mouse_event.position.point.y = from->y;
return true;
}
bool
mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
struct size screen_size,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT;
to->inject_mouse_event.action = AMOTION_EVENT_ACTION_MOVE;
to->inject_mouse_event.buttons = convert_mouse_buttons(from->state);
to->inject_mouse_event.position.screen_size = screen_size;
to->inject_mouse_event.position.point.x = from->x;
to->inject_mouse_event.position.point.y = from->y;
return true;
}
bool
mouse_wheel_from_sdl_to_android(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;
}

29
app/src/event_converter.h Normal file
View file

@ -0,0 +1,29 @@
#ifndef CONVERT_H
#define CONVERT_H
#include <stdbool.h>
#include <SDL2/SDL_events.h>
#include "config.h"
#include "control_msg.h"
bool
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to);
enum android_metastate
convert_meta_state(SDL_Keymod mod);
bool
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
bool prefer_text);
enum android_motionevent_buttons
convert_mouse_buttons(uint32_t state);
bool
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to);
bool
convert_touch_action(SDL_EventType from, enum android_motionevent_action *to);
#endif

View file

@ -5,17 +5,19 @@
#include "config.h"
#include "command.h"
#include "device.h"
#include "lock_util.h"
#include "log.h"
#define DEFAULT_PUSH_TARGET "/sdcard/"
static void
file_handler_request_destroy(struct file_handler_request *req) {
SDL_free(req->file);
}
bool
file_handler_init(struct file_handler *file_handler, const char *serial) {
file_handler_init(struct file_handler *file_handler, const char *serial,
const char *push_target) {
cbuf_init(&file_handler->queue);
@ -31,7 +33,7 @@ file_handler_init(struct file_handler *file_handler, const char *serial) {
if (serial) {
file_handler->serial = SDL_strdup(serial);
if (!file_handler->serial) {
LOGW("Cannot strdup serial");
LOGW("Could not strdup serial");
SDL_DestroyCond(file_handler->event_cond);
SDL_DestroyMutex(file_handler->mutex);
return false;
@ -46,6 +48,8 @@ file_handler_init(struct file_handler *file_handler, const char *serial) {
file_handler->stopped = false;
file_handler->current_process = PROCESS_NONE;
file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET;
return true;
}
@ -67,8 +71,8 @@ install_apk(const char *serial, const char *file) {
}
static process_t
push_file(const char *serial, const char *file) {
return adb_push(serial, file, DEVICE_SDCARD_PATH);
push_file(const char *serial, const char *file, const char *push_target) {
return adb_push(serial, file, push_target);
}
bool
@ -124,7 +128,8 @@ run_file_handler(void *data) {
process = install_apk(file_handler->serial, req.file);
} else {
LOGI("Pushing %s...", req.file);
process = push_file(file_handler->serial, req.file);
process = push_file(file_handler->serial, req.file,
file_handler->push_target);
}
file_handler->current_process = process;
mutex_unlock(file_handler->mutex);
@ -137,9 +142,11 @@ run_file_handler(void *data) {
}
} else {
if (process_check_success(process, "adb push")) {
LOGI("%s successfully pushed to /sdcard/", req.file);
LOGI("%s successfully pushed to %s", req.file,
file_handler->push_target);
} else {
LOGE("Failed to push %s to /sdcard/", req.file);
LOGE("Failed to push %s to %s", req.file,
file_handler->push_target);
}
}
@ -169,7 +176,7 @@ file_handler_stop(struct file_handler *file_handler) {
cond_signal(file_handler->event_cond);
if (file_handler->current_process != PROCESS_NONE) {
if (!cmd_terminate(file_handler->current_process)) {
LOGW("Cannot terminate install process");
LOGW("Could not terminate install process");
}
cmd_simple_wait(file_handler->current_process, NULL);
file_handler->current_process = PROCESS_NONE;

View file

@ -5,6 +5,7 @@
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "config.h"
#include "cbuf.h"
#include "command.h"
@ -22,6 +23,7 @@ struct file_handler_request_queue CBUF(struct file_handler_request, 16);
struct file_handler {
char *serial;
const char *push_target;
SDL_Thread *thread;
SDL_mutex *mutex;
SDL_cond *event_cond;
@ -32,7 +34,8 @@ struct file_handler {
};
bool
file_handler_init(struct file_handler *file_handler, const char *serial);
file_handler_init(struct file_handler *file_handler, const char *serial,
const char *push_target);
void
file_handler_destroy(struct file_handler *file_handler);

View file

@ -3,6 +3,7 @@
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_timer.h>
#include "config.h"
#include "lock_util.h"
#include "log.h"

View file

@ -7,6 +7,8 @@
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "config.h"
struct fps_counter {
SDL_Thread *thread;
SDL_mutex *mutex;

View file

@ -1,7 +1,9 @@
#include "input_manager.h"
#include <SDL2/SDL_assert.h>
#include "convert.h"
#include "config.h"
#include "event_converter.h"
#include "lock_util.h"
#include "log.h"
@ -47,7 +49,7 @@ send_keycode(struct controller *controller, enum android_keycode keycode,
if (actions & ACTION_DOWN) {
msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN;
if (!controller_push_msg(controller, &msg)) {
LOGW("Cannot request 'inject %s (DOWN)'", name);
LOGW("Could not request 'inject %s (DOWN)'", name);
return;
}
}
@ -55,7 +57,7 @@ send_keycode(struct controller *controller, enum android_keycode keycode,
if (actions & ACTION_UP) {
msg.inject_keycode.action = AKEY_EVENT_ACTION_UP;
if (!controller_push_msg(controller, &msg)) {
LOGW("Cannot request 'inject %s (UP)'", name);
LOGW("Could not request 'inject %s (UP)'", name);
}
}
}
@ -102,7 +104,7 @@ press_back_or_turn_screen_on(struct controller *controller) {
msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
if (!controller_push_msg(controller, &msg)) {
LOGW("Cannot request 'turn screen on'");
LOGW("Could not request 'press back or turn screen on'");
}
}
@ -112,7 +114,7 @@ expand_notification_panel(struct controller *controller) {
msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL;
if (!controller_push_msg(controller, &msg)) {
LOGW("Cannot request 'expand notification panel'");
LOGW("Could not request 'expand notification panel'");
}
}
@ -122,7 +124,7 @@ collapse_notification_panel(struct controller *controller) {
msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL;
if (!controller_push_msg(controller, &msg)) {
LOGW("Cannot request 'collapse notification panel'");
LOGW("Could not request 'collapse notification panel'");
}
}
@ -132,7 +134,7 @@ request_device_clipboard(struct controller *controller) {
msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD;
if (!controller_push_msg(controller, &msg)) {
LOGW("Cannot request device clipboard");
LOGW("Could not request device clipboard");
}
}
@ -140,7 +142,7 @@ static void
set_device_clipboard(struct controller *controller) {
char *text = SDL_GetClipboardText();
if (!text) {
LOGW("Cannot get clipboard text: %s", SDL_GetError());
LOGW("Could not get clipboard text: %s", SDL_GetError());
return;
}
if (!*text) {
@ -155,7 +157,7 @@ set_device_clipboard(struct controller *controller) {
if (!controller_push_msg(controller, &msg)) {
SDL_free(text);
LOGW("Cannot request 'set device clipboard'");
LOGW("Could not request 'set device clipboard'");
}
}
@ -167,7 +169,7 @@ set_screen_power_mode(struct controller *controller,
msg.set_screen_power_mode.mode = mode;
if (!controller_push_msg(controller, &msg)) {
LOGW("Cannot request 'set screen power mode'");
LOGW("Could not request 'set screen power mode'");
}
}
@ -191,7 +193,7 @@ static void
clipboard_paste(struct controller *controller) {
char *text = SDL_GetClipboardText();
if (!text) {
LOGW("Cannot get clipboard text: %s", SDL_GetError());
LOGW("Could not get clipboard text: %s", SDL_GetError());
return;
}
if (!*text) {
@ -205,34 +207,57 @@ clipboard_paste(struct controller *controller) {
msg.inject_text.text = text;
if (!controller_push_msg(controller, &msg)) {
SDL_free(text);
LOGW("Cannot request 'paste clipboard'");
LOGW("Could not request 'paste clipboard'");
}
}
void
input_manager_process_text_input(struct input_manager *input_manager,
input_manager_process_text_input(struct input_manager *im,
const SDL_TextInputEvent *event) {
char c = event->text[0];
if (isalpha(c) || c == ' ') {
SDL_assert(event->text[1] == '\0');
// letters and space are handled as raw key event
return;
if (!im->prefer_text) {
char c = event->text[0];
if (isalpha(c) || c == ' ') {
SDL_assert(event->text[1] == '\0');
// letters and space are handled as raw key event
return;
}
}
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
msg.inject_text.text = SDL_strdup(event->text);
if (!msg.inject_text.text) {
LOGW("Cannot strdup input text");
LOGW("Could not strdup input text");
return;
}
if (!controller_push_msg(input_manager->controller, &msg)) {
if (!controller_push_msg(im->controller, &msg)) {
SDL_free(msg.inject_text.text);
LOGW("Cannot 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
input_manager_process_key(struct input_manager *input_manager,
input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event,
bool control) {
// control: indicates the state of the command-line option --no-control
@ -242,16 +267,27 @@ input_manager_process_key(struct input_manager *input_manager,
bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI);
// use Cmd on macOS, Ctrl on other platforms
#ifdef __APPLE__
bool cmd = !ctrl && meta;
#else
if (meta) {
// no shortcuts involve Meta on platforms other than macOS, and it must
// not be forwarded to the device
return;
}
bool cmd = ctrl; // && !meta, already guaranteed
#endif
if (alt) {
// no shortcut involves Alt or Meta, and they should not be forwarded
// to the device
// no shortcuts involve Alt, and it must not be forwarded to the device
return;
}
struct controller *controller = input_manager->controller;
struct controller *controller = im->controller;
// capture all Ctrl events
if (ctrl | meta) {
if (ctrl || cmd) {
SDL_Keycode keycode = event->keysym.sym;
bool down = event->type == SDL_KEYDOWN;
int action = down ? ACTION_DOWN : ACTION_UP;
@ -259,63 +295,59 @@ input_manager_process_key(struct input_manager *input_manager,
bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
switch (keycode) {
case SDLK_h:
// Ctrl+h on all platform, since Cmd+h is already captured by
// the system on macOS to hide the window
if (control && ctrl && !meta && !shift && !repeat) {
action_home(controller, action);
}
return;
case SDLK_b: // fall-through
case SDLK_BACKSPACE:
if (control && ctrl && !meta && !shift && !repeat) {
if (control && cmd && !shift && !repeat) {
action_back(controller, action);
}
return;
case SDLK_s:
if (control && ctrl && !meta && !shift && !repeat) {
if (control && cmd && !shift && !repeat) {
action_app_switch(controller, action);
}
return;
case SDLK_m:
// Ctrl+m on all platform, since Cmd+m is already captured by
// the system on macOS to minimize the window
if (control && ctrl && !meta && !shift && !repeat) {
action_menu(controller, action);
}
return;
case SDLK_p:
if (control && ctrl && !meta && !shift && !repeat) {
if (control && cmd && !shift && !repeat) {
action_power(controller, action);
}
return;
case SDLK_o:
if (control && ctrl && !shift && !meta && down) {
if (control && cmd && !shift && down) {
set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF);
}
return;
case SDLK_DOWN:
#ifdef __APPLE__
if (control && !ctrl && meta && !shift) {
#else
if (control && ctrl && !meta && !shift) {
#endif
if (control && cmd && !shift) {
// forward repeated events
action_volume_down(controller, action);
}
return;
case SDLK_UP:
#ifdef __APPLE__
if (control && !ctrl && meta && !shift) {
#else
if (control && ctrl && !meta && !shift) {
#endif
if (control && cmd && !shift) {
// forward repeated events
action_volume_up(controller, action);
}
return;
case SDLK_c:
if (control && ctrl && !meta && !shift && !repeat && down) {
if (control && cmd && !shift && !repeat && down) {
request_device_clipboard(controller);
}
return;
case SDLK_v:
if (control && ctrl && !meta && !repeat && down) {
if (control && cmd && !repeat && down) {
if (shift) {
// store the text in the device clipboard
set_device_clipboard(controller);
@ -326,29 +358,29 @@ input_manager_process_key(struct input_manager *input_manager,
}
return;
case SDLK_f:
if (ctrl && !meta && !shift && !repeat && down) {
screen_switch_fullscreen(input_manager->screen);
if (!shift && cmd && !repeat && down) {
screen_switch_fullscreen(im->screen);
}
return;
case SDLK_x:
if (ctrl && !meta && !shift && !repeat && down) {
screen_resize_to_fit(input_manager->screen);
if (!shift && cmd && !repeat && down) {
screen_resize_to_fit(im->screen);
}
return;
case SDLK_g:
if (ctrl && !meta && !shift && !repeat && down) {
screen_resize_to_pixel_perfect(input_manager->screen);
if (!shift && cmd && !repeat && down) {
screen_resize_to_pixel_perfect(im->screen);
}
return;
case SDLK_i:
if (ctrl && !meta && !shift && !repeat && down) {
if (!shift && cmd && !repeat && down) {
struct fps_counter *fps_counter =
input_manager->video_buffer->fps_counter;
im->video_buffer->fps_counter;
switch_fps_counter_state(fps_counter);
}
return;
case SDLK_n:
if (control && ctrl && !meta && !repeat && down) {
if (control && cmd && !repeat && down) {
if (shift) {
collapse_notification_panel(controller);
} else {
@ -366,56 +398,129 @@ input_manager_process_key(struct input_manager *input_manager,
}
struct control_msg msg;
if (input_key_from_sdl_to_android(event, &msg)) {
if (convert_input_key(event, &msg, im->prefer_text)) {
if (!controller_push_msg(controller, &msg)) {
LOGW("Cannot request 'inject keycode'");
}
}
}
void
input_manager_process_mouse_motion(struct input_manager *input_manager,
const SDL_MouseMotionEvent *event) {
if (!event->state) {
// do not send motion events when no button is pressed
return;
}
struct control_msg msg;
if (mouse_motion_from_sdl_to_android(event,
input_manager->screen->frame_size,
&msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) {
LOGW("Cannot request 'inject mouse motion event'");
LOGW("Could not request 'inject keycode'");
}
}
}
static bool
is_outside_device_screen(struct input_manager *input_manager, int x, int y)
{
return x < 0 || x >= input_manager->screen->frame_size.width ||
y < 0 || y >= input_manager->screen->frame_size.height;
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
input_manager_process_mouse_button(struct input_manager *input_manager,
input_manager_process_mouse_motion(struct input_manager *im,
const SDL_MouseMotionEvent *event) {
if (!event->state) {
// do not send motion events when no button is pressed
return;
}
if (event->which == SDL_TOUCH_MOUSEID) {
// simulated from touch events, so it's a duplicate
return;
}
struct control_msg msg;
if (convert_mouse_motion(event, im->screen, &msg)) {
if (!controller_push_msg(im->controller, &msg)) {
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
input_manager_process_touch(struct input_manager *im,
const SDL_TouchFingerEvent *event) {
struct control_msg msg;
if (convert_touch(event, im->screen, &msg)) {
if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject touch event'");
}
}
}
static bool
is_outside_device_screen(struct input_manager *im, int x, int y)
{
return x < 0 || x >= im->screen->frame_size.width ||
y < 0 || y >= im->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
input_manager_process_mouse_button(struct input_manager *im,
const SDL_MouseButtonEvent *event,
bool control) {
if (event->which == SDL_TOUCH_MOUSEID) {
// simulated from touch events, so it's a duplicate
return;
}
if (event->type == SDL_MOUSEBUTTONDOWN) {
if (control && event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(input_manager->controller);
press_back_or_turn_screen_on(im->controller);
return;
}
if (control && event->button == SDL_BUTTON_MIDDLE) {
action_home(input_manager->controller, ACTION_DOWN | ACTION_UP);
action_home(im->controller, ACTION_DOWN | ACTION_UP);
return;
}
// double-click on black borders resize to fit the device screen
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
bool outside =
is_outside_device_screen(input_manager, event->x, event->y);
is_outside_device_screen(im, event->x, event->y);
if (outside) {
screen_resize_to_fit(input_manager->screen);
screen_resize_to_fit(im->screen);
return;
}
}
@ -427,26 +532,42 @@ input_manager_process_mouse_button(struct input_manager *input_manager,
}
struct control_msg msg;
if (mouse_button_from_sdl_to_android(event,
input_manager->screen->frame_size,
&msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) {
LOGW("Cannot request 'inject mouse button event'");
if (convert_mouse_button(event, im->screen, &msg)) {
if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject mouse button event'");
}
}
}
void
input_manager_process_mouse_wheel(struct input_manager *input_manager,
const SDL_MouseWheelEvent *event) {
static bool
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
struct control_msg *to) {
struct position position = {
.screen_size = input_manager->screen->frame_size,
.point = get_mouse_point(input_manager->screen),
.screen_size = screen->frame_size,
.point = get_mouse_point(screen),
};
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;
}
void
input_manager_process_mouse_wheel(struct input_manager *im,
const SDL_MouseWheelEvent *event) {
struct control_msg msg;
if (mouse_wheel_from_sdl_to_android(event, position, &msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) {
LOGW("Cannot request 'inject mouse wheel event'");
if (convert_mouse_wheel(event, im->screen, &msg)) {
if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject mouse wheel event'");
}
}
}

View file

@ -3,6 +3,7 @@
#include <stdbool.h>
#include "config.h"
#include "common.h"
#include "controller.h"
#include "fps_counter.h"
@ -13,28 +14,33 @@ struct input_manager {
struct controller *controller;
struct video_buffer *video_buffer;
struct screen *screen;
bool prefer_text;
};
void
input_manager_process_text_input(struct input_manager *input_manager,
input_manager_process_text_input(struct input_manager *im,
const SDL_TextInputEvent *event);
void
input_manager_process_key(struct input_manager *input_manager,
input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event,
bool control);
void
input_manager_process_mouse_motion(struct input_manager *input_manager,
input_manager_process_mouse_motion(struct input_manager *im,
const SDL_MouseMotionEvent *event);
void
input_manager_process_mouse_button(struct input_manager *input_manager,
input_manager_process_touch(struct input_manager *im,
const SDL_TouchFingerEvent *event);
void
input_manager_process_mouse_button(struct input_manager *im,
const SDL_MouseButtonEvent *event,
bool control);
void
input_manager_process_mouse_wheel(struct input_manager *input_manager,
input_manager_process_mouse_wheel(struct input_manager *im,
const SDL_MouseWheelEvent *event);
#endif

View file

@ -4,6 +4,7 @@
#include <stdint.h>
#include <SDL2/SDL_mutex.h>
#include "config.h"
#include "log.h"
static inline void

View file

@ -8,42 +8,37 @@
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
#include <SDL2/SDL.h>
#include "compat.h"
#include "config.h"
#include "compat.h"
#include "log.h"
#include "recorder.h"
struct args {
const char *serial;
const char *crop;
const char *record_filename;
enum recorder_format record_format;
bool fullscreen;
bool no_control;
bool no_display;
struct scrcpy_options opts;
bool help;
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) {
#ifdef __APPLE__
# define CTRL_OR_CMD "Cmd"
#else
# define CTRL_OR_CMD "Ctrl"
#endif
fprintf(stderr,
"Usage: %s [options]\n"
"\n"
"Options:\n"
"\n"
" --always-on-top\n"
" Make scrcpy window always on top (above other windows).\n"
"\n"
" -b, --bit-rate value\n"
" Encode the video at the given bit-rate, expressed in bits/s.\n"
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
" Default is %d.\n"
"\n"
" -c, --crop width:height:x:y\n"
" --crop width:height:x:y\n"
" Crop the device screen on the server.\n"
" The values are expressed in the device natural orientation\n"
" (typically, portrait for a phone, landscape for a tablet).\n"
@ -52,12 +47,13 @@ static void usage(const char *arg0) {
" -f, --fullscreen\n"
" Start in fullscreen.\n"
"\n"
" -F, --record-format\n"
" Force recording format (either mp4 or mkv).\n"
"\n"
" -h, --help\n"
" Print this help.\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"
" Limit both the width and height of the video to value. The\n"
" other dimension is computed so that the device aspect-ratio\n"
@ -75,18 +71,33 @@ static void usage(const char *arg0) {
" Set the TCP port the client listens on.\n"
" Default is %d.\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"
" Set the target directory for pushing files to the device by\n"
" drag & drop. It is passed as-is to \"adb push\".\n"
" Default is \"/sdcard/\".\n"
"\n"
" -r, --record file.mp4\n"
" Record screen to file.\n"
" The format is determined by the -F/--record-format option if\n"
" The format is determined by the --record-format option if\n"
" set, or by the file extension (.mp4 or .mkv).\n"
"\n"
" --record-format format\n"
" Force recording format (either mp4 or mkv).\n"
"\n"
" --render-expired-frames\n"
" By default, to minimize latency, scrcpy always renders the\n"
" last available decoded frame, and drops any previous ones.\n"
" This flag forces to render all frames, at a cost of a\n"
" possible increased latency.\n"
"\n"
" -s, --serial\n"
" -s, --serial serial\n"
" The device serial number. Mandatory only if several devices\n"
" are connected to adb.\n"
"\n"
@ -97,21 +108,40 @@ static void usage(const char *arg0) {
" Enable \"show touches\" on start, disable on quit.\n"
" It only shows physical touches (not clicks from scrcpy).\n"
"\n"
" -T, --always-on-top\n"
" Make scrcpy window always on top (above other windows).\n"
"\n"
" -v, --version\n"
" Print the version of scrcpy.\n"
"\n"
" --window-borderless\n"
" Disable window decorations (display borderless window).\n"
"\n"
" --window-title text\n"
" Set a custom window title.\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"
"\n"
" Ctrl+f\n"
" " CTRL_OR_CMD "+f\n"
" switch fullscreen mode\n"
"\n"
" Ctrl+g\n"
" " CTRL_OR_CMD "+g\n"
" resize window to 1:1 (pixel-perfect)\n"
"\n"
" Ctrl+x\n"
" " CTRL_OR_CMD "+x\n"
" Double-click on black borders\n"
" resize window to remove black borders\n"
"\n"
@ -119,48 +149,48 @@ static void usage(const char *arg0) {
" Middle-click\n"
" click on HOME\n"
"\n"
" Ctrl+b\n"
" Ctrl+Backspace\n"
" " CTRL_OR_CMD "+b\n"
" " CTRL_OR_CMD "+Backspace\n"
" Right-click (when screen is on)\n"
" click on BACK\n"
"\n"
" Ctrl+s\n"
" " CTRL_OR_CMD "+s\n"
" click on APP_SWITCH\n"
"\n"
" Ctrl+m\n"
" click on MENU\n"
"\n"
" Ctrl+Up\n"
" " CTRL_OR_CMD "+Up\n"
" click on VOLUME_UP\n"
"\n"
" Ctrl+Down\n"
" " CTRL_OR_CMD "+Down\n"
" click on VOLUME_DOWN\n"
"\n"
" Ctrl+p\n"
" " CTRL_OR_CMD "+p\n"
" click on POWER (turn screen on/off)\n"
"\n"
" Right-click (when screen is off)\n"
" power on\n"
"\n"
" Ctrl+o\n"
" " CTRL_OR_CMD "+o\n"
" turn device screen off (keep mirroring)\n"
"\n"
" Ctrl+n\n"
" " CTRL_OR_CMD "+n\n"
" expand notification panel\n"
"\n"
" Ctrl+Shift+n\n"
" " CTRL_OR_CMD "+Shift+n\n"
" collapse notification panel\n"
"\n"
" Ctrl+c\n"
" " CTRL_OR_CMD "+c\n"
" copy device clipboard to computer\n"
"\n"
" Ctrl+v\n"
" " CTRL_OR_CMD "+v\n"
" paste computer clipboard to device\n"
"\n"
" Ctrl+Shift+v\n"
" " CTRL_OR_CMD "+Shift+v\n"
" copy computer clipboard to device\n"
"\n"
" Ctrl+i\n"
" " CTRL_OR_CMD "+i\n"
" enable/disable FPS counter (print frames/second in logs)\n"
"\n"
" Drag & drop APK file\n"
@ -243,11 +273,75 @@ parse_max_size(char *optarg, uint16_t *max_size) {
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
parse_port(char *optarg, uint16_t *port) {
char *endptr;
if (*optarg == '\0') {
LOGE("Invalid port parameter is empty");
LOGE("Port parameter is empty");
return false;
}
long value = strtol(optarg, &endptr, 0);
@ -295,88 +389,159 @@ guess_record_format(const char *filename) {
}
#define OPT_RENDER_EXPIRED_FRAMES 1000
#define OPT_WINDOW_TITLE 1001
#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
parse_args(struct args *args, int argc, char *argv[]) {
static const struct option long_options[] = {
{"always-on-top", no_argument, NULL, 'T'},
{"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
{"bit-rate", required_argument, NULL, 'b'},
{"crop", required_argument, NULL, 'c'},
{"crop", required_argument, NULL, OPT_CROP},
{"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'},
{"max-fps", required_argument, NULL, OPT_MAX_FPS},
{"max-size", required_argument, NULL, 'm'},
{"no-control", no_argument, NULL, 'n'},
{"no-display", no_argument, NULL, 'N'},
{"port", required_argument, NULL, 'p'},
{"push-target", required_argument, NULL, OPT_PUSH_TARGET},
{"record", required_argument, NULL, 'r'},
{"record-format", required_argument, NULL, 'f'},
{"record-format", required_argument, NULL, OPT_RECORD_FORMAT},
{"render-expired-frames", no_argument, NULL,
OPT_RENDER_EXPIRED_FRAMES},
OPT_RENDER_EXPIRED_FRAMES},
{"serial", required_argument, NULL, 's'},
{"show-touches", no_argument, NULL, 't'},
{"turn-screen-off", no_argument, NULL, 'S'},
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
{"version", no_argument, NULL, 'v'},
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
{"window-x", required_argument, NULL, OPT_WINDOW_X},
{"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 },
};
struct scrcpy_options *opts = &args->opts;
int c;
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options,
NULL)) != -1) {
switch (c) {
case 'b':
if (!parse_bit_rate(optarg, &args->bit_rate)) {
if (!parse_bit_rate(optarg, &opts->bit_rate)) {
return false;
}
break;
case 'c':
args->crop = optarg;
LOGW("Deprecated option -c. Use --crop instead.");
// fall through
case OPT_CROP:
opts->crop = optarg;
break;
case 'f':
args->fullscreen = true;
opts->fullscreen = true;
break;
case 'F':
if (!parse_record_format(optarg, &args->record_format)) {
LOGW("Deprecated option -F. Use --record-format instead.");
// fall through
case OPT_RECORD_FORMAT:
if (!parse_record_format(optarg, &opts->record_format)) {
return false;
}
break;
case 'h':
args->help = true;
break;
case OPT_MAX_FPS:
if (!parse_max_fps(optarg, &opts->max_fps)) {
return false;
}
break;
case 'm':
if (!parse_max_size(optarg, &args->max_size)) {
if (!parse_max_size(optarg, &opts->max_size)) {
return false;
}
break;
case 'n':
args->no_control = true;
opts->control = false;
break;
case 'N':
args->no_display = true;
opts->display = false;
break;
case 'p':
if (!parse_port(optarg, &args->port)) {
if (!parse_port(optarg, &opts->port)) {
return false;
}
break;
case 'r':
args->record_filename = optarg;
opts->record_filename = optarg;
break;
case 's':
args->serial = optarg;
opts->serial = optarg;
break;
case 'S':
args->turn_screen_off = true;
opts->turn_screen_off = true;
break;
case 't':
args->show_touches = true;
opts->show_touches = true;
break;
case 'T':
args->always_on_top = true;
LOGW("Deprecated option -T. Use --always-on-top instead.");
// fall through
case OPT_ALWAYS_ON_TOP:
opts->always_on_top = true;
break;
case 'v':
args->version = true;
break;
case OPT_RENDER_EXPIRED_FRAMES:
args->render_expired_frames = true;
opts->render_expired_frames = true;
break;
case OPT_WINDOW_TITLE:
opts->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;
case OPT_PUSH_TARGET:
opts->push_target = optarg;
break;
case OPT_PREFER_TEXT:
opts->prefer_text = true;
break;
default:
// getopt prints the error message on stderr
@ -384,12 +549,12 @@ parse_args(struct args *args, int argc, char *argv[]) {
}
}
if (args->no_display && !args->record_filename) {
if (!opts->display && !opts->record_filename) {
LOGE("-N/--no-display requires screen recording (-r/--record)");
return false;
}
if (args->no_display && args->fullscreen) {
if (!opts->display && opts->fullscreen) {
LOGE("-f/--fullscreen-window is incompatible with -N/--no-display");
return false;
}
@ -400,20 +565,25 @@ parse_args(struct args *args, int argc, char *argv[]) {
return false;
}
if (args->record_format && !args->record_filename) {
if (opts->record_format && !opts->record_filename) {
LOGE("Record format specified without recording");
return false;
}
if (args->record_filename && !args->record_format) {
args->record_format = guess_record_format(args->record_filename);
if (!args->record_format) {
if (opts->record_filename && !opts->record_format) {
opts->record_format = guess_record_format(opts->record_filename);
if (!opts->record_format) {
LOGE("No format specified for \"%s\" (try with -F mkv)",
args->record_filename);
opts->record_filename);
return false;
}
}
if (!opts->control && opts->turn_screen_off) {
LOGE("Could not request to turn screen off if control is disabled");
return false;
}
return true;
}
@ -426,22 +596,11 @@ main(int argc, char *argv[]) {
setbuf(stderr, NULL);
#endif
struct args args = {
.serial = NULL,
.crop = NULL,
.record_filename = NULL,
.record_format = 0,
.opts = SCRCPY_OPTIONS_DEFAULT,
.help = 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)) {
return 1;
}
@ -456,6 +615,8 @@ main(int argc, char *argv[]) {
return 0;
}
LOGI("scrcpy " SCRCPY_VERSION " <https://github.com/Genymobile/scrcpy>");
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
av_register_all();
#endif
@ -468,23 +629,7 @@ main(int argc, char *argv[]) {
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
#endif
struct scrcpy_options options = {
.serial = args.serial,
.crop = args.crop,
.port = args.port,
.record_filename = args.record_filename,
.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;
int res = scrcpy(&args.opts) ? 0 : 1;
avformat_network_deinit(); // ignore failure

View file

@ -2,6 +2,7 @@
#include <stdio.h>
#include "config.h"
#include "log.h"
#ifdef __WINDOWS__

View file

@ -17,6 +17,8 @@
typedef int socket_t;
#endif
#include "config.h"
bool
net_init(void);

77
app/src/queue.h Normal file
View file

@ -0,0 +1,77 @@
// generic intrusive FIFO queue
#ifndef QUEUE_H
#define QUEUE_H
#include <stdbool.h>
#include <stddef.h>
#include <SDL2/SDL_assert.h>
#include "config.h"
// To define a queue type of "struct foo":
// struct queue_foo QUEUE(struct foo);
#define QUEUE(TYPE) { \
TYPE *first; \
TYPE *last; \
}
#define queue_init(PQ) \
(void) ((PQ)->first = (PQ)->last = NULL)
#define queue_is_empty(PQ) \
!(PQ)->first
// NEXTFIELD is the field in the ITEM type used for intrusive linked-list
//
// For example:
// struct foo {
// int value;
// struct foo *next;
// };
//
// // define the type "struct my_queue"
// struct my_queue QUEUE(struct foo);
//
// struct my_queue queue;
// queue_init(&queue);
//
// struct foo v1 = { .value = 42 };
// struct foo v2 = { .value = 27 };
//
// queue_push(&queue, next, v1);
// queue_push(&queue, next, v2);
//
// struct foo *foo;
// queue_take(&queue, next, &foo);
// assert(foo->value == 42);
// queue_take(&queue, next, &foo);
// assert(foo->value == 27);
// assert(queue_is_empty(&queue));
//
// push a new item into the queue
#define queue_push(PQ, NEXTFIELD, ITEM) \
(void) ({ \
(ITEM)->NEXTFIELD = NULL; \
if (queue_is_empty(PQ)) { \
(PQ)->first = (PQ)->last = (ITEM); \
} else { \
(PQ)->last->NEXTFIELD = (ITEM); \
(PQ)->last = (ITEM); \
} \
})
// take the next item and remove it from the queue (the queue must not be empty)
// the result is stored in *(PITEM)
// (without typeof(), we could not store a local variable having the correct
// type so that we can "return" it)
#define queue_take(PQ, NEXTFIELD, PITEM) \
(void) ({ \
SDL_assert(!queue_is_empty(PQ)); \
*(PITEM) = (PQ)->first; \
(PQ)->first = (PQ)->first->NEXTFIELD; \
})
// no need to update (PQ)->last if the queue is left empty:
// (PQ)->last is undefined if !(PQ)->first anyway
#endif

View file

@ -5,6 +5,7 @@
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "config.h"
#include "net.h"
// receive events from the device

View file

@ -3,8 +3,9 @@
#include <libavutil/time.h>
#include <SDL2/SDL_assert.h>
#include "compat.h"
#include "config.h"
#include "compat.h"
#include "lock_util.h"
#include "log.h"
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
@ -26,6 +27,39 @@ find_muxer(const char *name) {
return oformat;
}
static struct record_packet *
record_packet_new(const AVPacket *packet) {
struct record_packet *rec = SDL_malloc(sizeof(*rec));
if (!rec) {
return NULL;
}
// av_packet_ref() does not initialize all fields in old FFmpeg versions
// See <https://github.com/Genymobile/scrcpy/issues/707>
av_init_packet(&rec->packet);
if (av_packet_ref(&rec->packet, packet)) {
SDL_free(rec);
return NULL;
}
return rec;
}
static void
record_packet_delete(struct record_packet *rec) {
av_packet_unref(&rec->packet);
SDL_free(rec);
}
static void
recorder_queue_clear(struct recorder_queue *queue) {
while (!queue_is_empty(queue)) {
struct record_packet *rec;
queue_take(queue, next, &rec);
record_packet_delete(rec);
}
}
bool
recorder_init(struct recorder *recorder,
const char *filename,
@ -33,19 +67,40 @@ recorder_init(struct recorder *recorder,
struct size declared_frame_size) {
recorder->filename = SDL_strdup(filename);
if (!recorder->filename) {
LOGE("Cannot strdup filename");
LOGE("Could not strdup filename");
return false;
}
recorder->mutex = SDL_CreateMutex();
if (!recorder->mutex) {
LOGC("Could not create mutex");
SDL_free(recorder->filename);
return false;
}
recorder->queue_cond = SDL_CreateCond();
if (!recorder->queue_cond) {
LOGC("Could not create cond");
SDL_DestroyMutex(recorder->mutex);
SDL_free(recorder->filename);
return false;
}
queue_init(&recorder->queue);
recorder->stopped = false;
recorder->failed = false;
recorder->format = format;
recorder->declared_frame_size = declared_frame_size;
recorder->header_written = false;
recorder->previous = NULL;
return true;
}
void
recorder_destroy(struct recorder *recorder) {
SDL_DestroyCond(recorder->queue_cond);
SDL_DestroyMutex(recorder->mutex);
SDL_free(recorder->filename);
}
@ -80,6 +135,9 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
// <https://github.com/FFmpeg/FFmpeg/commit/0694d8702421e7aff1340038559c438b61bb30dd>
recorder->ctx->oformat = (AVOutputFormat *) format;
av_dict_set(&recorder->ctx->metadata, "comment",
"Recorded by scrcpy " SCRCPY_VERSION, 0);
AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec);
if (!ostream) {
avformat_free_context(recorder->ctx);
@ -116,15 +174,25 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
void
recorder_close(struct recorder *recorder) {
int ret = av_write_trailer(recorder->ctx);
if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
if (recorder->header_written) {
int ret = av_write_trailer(recorder->ctx);
if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
recorder->failed = true;
}
} else {
// the recorded file is empty
recorder->failed = true;
}
avio_close(recorder->ctx->pb);
avformat_free_context(recorder->ctx);
const char *format_name = recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
if (recorder->failed) {
LOGE("Recording failed to %s", recorder->filename);
} else {
const char *format_name = recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
}
}
static bool
@ -133,7 +201,7 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
if (!extradata) {
LOGC("Cannot allocate extradata");
LOGC("Could not allocate extradata");
return false;
}
@ -151,9 +219,6 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
int ret = avformat_write_header(recorder->ctx, NULL);
if (ret < 0) {
LOGE("Failed to write header to %s", recorder->filename);
SDL_free(extradata);
avio_closep(&recorder->ctx->pb);
avformat_free_context(recorder->ctx);
return false;
}
@ -169,13 +234,145 @@ recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) {
bool
recorder_write(struct recorder *recorder, AVPacket *packet) {
if (!recorder->header_written) {
if (packet->pts != AV_NOPTS_VALUE) {
LOGE("The first packet is not a config packet");
return false;
}
bool ok = recorder_write_header(recorder, packet);
if (!ok) {
return false;
}
recorder->header_written = true;
return true;
}
if (packet->pts == AV_NOPTS_VALUE) {
// ignore config packets
return true;
}
recorder_rescale_packet(recorder, packet);
return av_write_frame(recorder->ctx, packet) >= 0;
}
static int
run_recorder(void *data) {
struct recorder *recorder = data;
for (;;) {
mutex_lock(recorder->mutex);
while (!recorder->stopped && queue_is_empty(&recorder->queue)) {
cond_wait(recorder->queue_cond, recorder->mutex);
}
// if stopped is set, continue to process the remaining events (to
// finish the recording) before actually stopping
if (recorder->stopped && queue_is_empty(&recorder->queue)) {
mutex_unlock(recorder->mutex);
struct record_packet *last = recorder->previous;
if (last) {
// assign an arbitrary duration to the last packet
last->packet.duration = 100000;
bool ok = recorder_write(recorder, &last->packet);
if (!ok) {
// failing to write the last frame is not very serious, no
// future frame may depend on it, so the resulting file
// will still be valid
LOGW("Could not record last packet");
}
record_packet_delete(last);
}
break;
}
struct record_packet *rec;
queue_take(&recorder->queue, next, &rec);
mutex_unlock(recorder->mutex);
// recorder->previous is only written from this thread, no need to lock
struct record_packet *previous = recorder->previous;
recorder->previous = rec;
if (!previous) {
// we just received the first packet
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
previous->packet.duration = rec->packet.pts - previous->packet.pts;
}
bool ok = recorder_write(recorder, &previous->packet);
record_packet_delete(previous);
if (!ok) {
LOGE("Could not record packet");
mutex_lock(recorder->mutex);
recorder->failed = true;
// discard pending packets
recorder_queue_clear(&recorder->queue);
mutex_unlock(recorder->mutex);
break;
}
}
LOGD("Recorder thread ended");
return 0;
}
bool
recorder_start(struct recorder *recorder) {
LOGD("Starting recorder thread");
recorder->thread = SDL_CreateThread(run_recorder, "recorder", recorder);
if (!recorder->thread) {
LOGC("Could not start recorder thread");
return false;
}
return true;
}
void
recorder_stop(struct recorder *recorder) {
mutex_lock(recorder->mutex);
recorder->stopped = true;
cond_signal(recorder->queue_cond);
mutex_unlock(recorder->mutex);
}
void
recorder_join(struct recorder *recorder) {
SDL_WaitThread(recorder->thread, NULL);
}
bool
recorder_push(struct recorder *recorder, const AVPacket *packet) {
mutex_lock(recorder->mutex);
SDL_assert(!recorder->stopped);
if (recorder->failed) {
// reject any new packet (this will stop the stream)
return false;
}
struct record_packet *rec = record_packet_new(packet);
if (!rec) {
LOGC("Could not allocate record packet");
return false;
}
queue_push(&recorder->queue, next, rec);
cond_signal(recorder->queue_cond);
mutex_unlock(recorder->mutex);
return true;
}

View file

@ -3,20 +3,45 @@
#include <stdbool.h>
#include <libavformat/avformat.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "config.h"
#include "common.h"
#include "queue.h"
enum recorder_format {
RECORDER_FORMAT_MP4 = 1,
RECORDER_FORMAT_AUTO,
RECORDER_FORMAT_MP4,
RECORDER_FORMAT_MKV,
};
struct record_packet {
AVPacket packet;
struct record_packet *next;
};
struct recorder_queue QUEUE(struct record_packet);
struct recorder {
char *filename;
enum recorder_format format;
AVFormatContext *ctx;
struct size declared_frame_size;
bool header_written;
SDL_Thread *thread;
SDL_mutex *mutex;
SDL_cond *queue_cond;
bool stopped; // set on recorder_stop() by the stream reader
bool failed; // set on packet write failure
struct recorder_queue queue;
// we can write a packet only once we received the next one so that we can
// set its duration (next_pts - current_pts)
// "previous" is only accessed from the recorder thread, so it does not
// need to be protected by the mutex
struct record_packet *previous;
};
bool
@ -33,6 +58,15 @@ void
recorder_close(struct recorder *recorder);
bool
recorder_write(struct recorder *recorder, AVPacket *packet);
recorder_start(struct recorder *recorder);
void
recorder_stop(struct recorder *recorder);
void
recorder_join(struct recorder *recorder);
bool
recorder_push(struct recorder *recorder, const AVPacket *packet);
#endif

View file

@ -7,6 +7,7 @@
#include <sys/time.h>
#include <SDL2/SDL.h>
#include "config.h"
#include "command.h"
#include "common.h"
#include "compat.h"
@ -41,6 +42,7 @@ static struct input_manager input_manager = {
.controller = &controller,
.video_buffer = &video_buffer,
.screen = &screen,
.prefer_text = false, // initialized later
};
// init SDL and set appropriate hints
@ -142,12 +144,7 @@ handle_event(SDL_Event *event, bool control) {
}
break;
case SDL_WINDOWEVENT:
switch (event->window.event) {
case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
screen_render(&screen);
break;
}
screen_handle_window_event(&screen, &event->window);
break;
case SDL_TEXTINPUT:
if (!control) {
@ -180,6 +177,11 @@ handle_event(SDL_Event *event, bool control) {
input_manager_process_mouse_button(&input_manager, &event->button,
control);
break;
case SDL_FINGERMOTION:
case SDL_FINGERDOWN:
case SDL_FINGERUP:
input_manager_process_touch(&input_manager, &event->tfinger);
break;
case SDL_DROPFILE: {
if (!control) {
break;
@ -211,6 +213,7 @@ event_loop(bool display, bool control) {
case EVENT_RESULT_STOPPED_BY_USER:
return true;
case EVENT_RESULT_STOPPED_BY_EOS:
LOGW("Device disconnected");
return false;
case EVENT_RESULT_CONTINUE:
break;
@ -259,7 +262,7 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
}
char *local_fmt = SDL_malloc(strlen(fmt) + 10);
if (!local_fmt) {
LOGC("Cannot allocate string");
LOGC("Could not allocate string");
return;
}
// strcpy is safe here, the destination is large enough
@ -277,7 +280,7 @@ scrcpy(const struct scrcpy_options *options) {
.local_port = options->port,
.max_size = options->max_size,
.bit_rate = options->bit_rate,
.send_frame_meta = record,
.max_fps = options->max_fps,
.control = options->control,
};
if (!server_start(&server, options->serial, &params)) {
@ -334,7 +337,8 @@ scrcpy(const struct scrcpy_options *options) {
video_buffer_initialized = true;
if (options->control) {
if (!file_handler_init(&file_handler, server.serial)) {
if (!file_handler_init(&file_handler, server.serial,
options->push_target)) {
goto end;
}
file_handler_initialized = true;
@ -380,8 +384,14 @@ scrcpy(const struct scrcpy_options *options) {
controller_started = true;
}
if (!screen_init_rendering(&screen, device_name, frame_size,
options->always_on_top)) {
const char *window_title =
options->window_title ? options->window_title : device_name;
if (!screen_init_rendering(&screen, window_title, frame_size,
options->always_on_top, options->window_x,
options->window_y, options->window_width,
options->window_height,
options->window_borderless)) {
goto end;
}
@ -391,7 +401,7 @@ scrcpy(const struct scrcpy_options *options) {
msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF;
if (!controller_push_msg(&controller, &msg)) {
LOGW("Cannot request 'set screen power mode'");
LOGW("Could not request 'set screen power mode'");
}
}
@ -405,6 +415,8 @@ scrcpy(const struct scrcpy_options *options) {
show_touches_waited = true;
}
input_manager.prefer_text = options->prefer_text;
ret = event_loop(options->display, options->control);
LOGD("quit...");

View file

@ -3,16 +3,26 @@
#include <stdbool.h>
#include <stdint.h>
#include <recorder.h>
#include "config.h"
#include "input_manager.h"
#include "recorder.h"
struct scrcpy_options {
const char *serial;
const char *crop;
const char *record_filename;
const char *window_title;
const char *push_target;
enum recorder_format record_format;
uint16_t port;
uint16_t max_size;
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 fullscreen;
bool always_on_top;
@ -20,8 +30,36 @@ struct scrcpy_options {
bool display;
bool turn_screen_off;
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
scrcpy(const struct scrcpy_options *options);

View file

@ -3,6 +3,7 @@
#include <string.h>
#include <SDL2/SDL.h>
#include "config.h"
#include "common.h"
#include "compat.h"
#include "icon.xpm"
@ -15,7 +16,7 @@
// get the window size in a struct size
static struct size
get_native_window_size(SDL_Window *window) {
get_window_size(SDL_Window *window) {
int width;
int height;
SDL_GetWindowSize(window, &width, &height);
@ -28,11 +29,20 @@ get_native_window_size(SDL_Window *window) {
// get the windowed window size
static struct size
get_window_size(const struct screen *screen) {
if (screen->fullscreen) {
get_windowed_window_size(const struct screen *screen) {
if (screen->fullscreen || screen->maximized) {
return screen->windowed_window_size;
}
return get_native_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
@ -40,12 +50,8 @@ static void
set_window_size(struct screen *screen, struct size new_size) {
// setting the window size during fullscreen is implementation defined,
// 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;
} else {
SDL_SetWindowSize(screen->window, new_size.width, new_size.height);
}
screen->windowed_window_size = new_size;
apply_windowed_size(screen);
}
// get the preferred display bounds (i.e. the screen bounds with some margins)
@ -85,7 +91,7 @@ get_optimal_size(struct size current_size, struct size frame_size) {
uint32_t h;
if (!get_preferred_display_bounds(&display_size)) {
// cannot get display bounds, do not constraint the size
// could not get display bounds, do not constraint the size
w = current_size.width;
h = current_size.height;
} else {
@ -111,14 +117,35 @@ get_optimal_size(struct size current_size, struct size frame_size) {
// same as get_optimal_size(), but read the current size from the window
static inline struct size
get_optimal_window_size(const struct screen *screen, struct size frame_size) {
struct size current_size = get_window_size(screen);
return get_optimal_size(current_size, frame_size);
struct size windowed_size = get_windowed_window_size(screen);
return get_optimal_size(windowed_size, frame_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
get_initial_optimal_size(struct size frame_size) {
return get_optimal_size(frame_size, frame_size);
get_initial_optimal_size(struct size frame_size, uint16_t req_width,
uint16_t req_height) {
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
@ -134,11 +161,14 @@ create_texture(SDL_Renderer *renderer, struct size frame_size) {
}
bool
screen_init_rendering(struct screen *screen, const char *device_name,
struct size frame_size, bool always_on_top) {
screen_init_rendering(struct screen *screen, const char *window_title,
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;
struct size window_size = get_initial_optimal_size(frame_size);
struct size window_size =
get_initial_optimal_size(frame_size, window_width, window_height);
uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
#ifdef HIDPI_SUPPORT
window_flags |= SDL_WINDOW_ALLOW_HIGHDPI;
@ -151,9 +181,13 @@ screen_init_rendering(struct screen *screen, const char *device_name,
"(compile with SDL >= 2.0.5 to enable it)");
#endif
}
if (window_borderless) {
window_flags |= SDL_WINDOW_BORDERLESS;
}
screen->window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
int x = window_x != -1 ? window_x : SDL_WINDOWPOS_UNDEFINED;
int y = window_y != -1 ? window_y : SDL_WINDOWPOS_UNDEFINED;
screen->window = SDL_CreateWindow(window_title, x, y,
window_size.width, window_size.height,
window_flags);
if (!screen->window) {
@ -193,6 +227,8 @@ screen_init_rendering(struct screen *screen, const char *device_name,
return false;
}
screen->windowed_window_size = window_size;
return true;
}
@ -228,11 +264,11 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) {
// frame dimension changed, destroy texture
SDL_DestroyTexture(screen->texture);
struct size current_size = get_window_size(screen);
struct size windowed_size = get_windowed_window_size(screen);
struct size target_size = {
(uint32_t) current_size.width * new_frame_size.width
(uint32_t) windowed_size.width * new_frame_size.width
/ screen->frame_size.width,
(uint32_t) current_size.height * new_frame_size.height
(uint32_t) windowed_size.height * new_frame_size.height
/ screen->frame_size.height,
};
target_size = get_optimal_size(target_size, new_frame_size);
@ -286,10 +322,6 @@ screen_render(struct screen *screen) {
void
screen_switch_fullscreen(struct screen *screen) {
if (!screen->fullscreen) {
// going to fullscreen, store the current windowed window size
screen->windowed_window_size = get_native_window_size(screen->window);
}
uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
if (SDL_SetWindowFullscreen(screen->window, new_mode)) {
LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
@ -297,11 +329,7 @@ screen_switch_fullscreen(struct screen *screen) {
}
screen->fullscreen = !screen->fullscreen;
if (!screen->fullscreen) {
// fullscreen disabled, restore expected windowed window size
SDL_SetWindowSize(screen->window, screen->windowed_window_size.width,
screen->windowed_window_size.height);
}
apply_windowed_size(screen);
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
screen_render(screen);
@ -309,20 +337,75 @@ screen_switch_fullscreen(struct screen *screen) {
void
screen_resize_to_fit(struct screen *screen) {
if (!screen->fullscreen) {
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");
if (screen->fullscreen) {
return;
}
if (screen->maximized) {
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");
}
void
screen_resize_to_pixel_perfect(struct screen *screen) {
if (!screen->fullscreen) {
SDL_SetWindowSize(screen->window, screen->frame_size.width,
screen->frame_size.height);
LOGD("Resized to pixel-perfect");
if (screen->fullscreen) {
return;
}
if (screen->maximized) {
SDL_RestoreWindow(screen->window);
screen->maximized = false;
}
SDL_SetWindowSize(screen->window, screen->frame_size.width,
screen->frame_size.height);
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.
SDL_assert(screen->windowed_window_size_backup.width);
SDL_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

@ -5,6 +5,7 @@
#include <SDL2/SDL.h>
#include <libavformat/avformat.h>
#include "config.h"
#include "common.h"
struct video_buffer;
@ -14,28 +15,37 @@ struct screen {
SDL_Renderer *renderer;
SDL_Texture *texture;
struct size frame_size;
//used only in fullscreen mode to know the windowed window size
// The window size the last time it was not maximized or fullscreen.
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 fullscreen;
bool maximized;
bool no_window;
};
#define SCREEN_INITIALIZER { \
.window = NULL, \
.renderer = NULL, \
.texture = NULL, \
.frame_size = { \
.width = 0, \
.height = 0, \
}, \
#define SCREEN_INITIALIZER { \
.window = NULL, \
.renderer = NULL, \
.texture = NULL, \
.frame_size = { \
.width = 0, \
.height = 0, \
}, \
.windowed_window_size = { \
.width = 0, \
.height = 0, \
}, \
.has_frame = false, \
.fullscreen = false, \
.no_window = false, \
.width = 0, \
.height = 0, \
}, \
.windowed_window_size_backup = { \
.width = 0, \
.height = 0, \
}, \
.has_frame = false, \
.fullscreen = false, \
.maximized = false, \
.no_window = false, \
}
// initialize default values
@ -44,8 +54,10 @@ screen_init(struct screen *screen);
// initialize screen, create window, renderer and texture (window is hidden)
bool
screen_init_rendering(struct screen *screen, const char *device_name,
struct size frame_size, bool always_on_top);
screen_init_rendering(struct screen *screen, const char *window_title,
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
void
@ -75,4 +87,8 @@ screen_resize_to_fit(struct screen *screen);
void
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

View file

@ -13,9 +13,9 @@
#include "net.h"
#define SOCKET_NAME "scrcpy"
#define SERVER_FILENAME "scrcpy-server.jar"
#define SERVER_FILENAME "scrcpy-server"
#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FLENAME
#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME
#define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME
static const char *
@ -32,10 +32,10 @@ get_server_path(void) {
// the absolute path is hardcoded
return DEFAULT_SERVER_PATH;
#else
// use scrcpy-server.jar in the same directory as the executable
// use scrcpy-server in the same directory as the executable
char *executable_path = get_executable_path();
if (!executable_path) {
LOGE("Cannot get executable path, "
LOGE("Could not get executable path, "
"using " SERVER_FILENAME " from current directory");
// not found, use current directory
return SERVER_FILENAME;
@ -47,7 +47,7 @@ get_server_path(void) {
size_t len = dirlen + 1 + sizeof(SERVER_FILENAME);
char *server_path = SDL_malloc(len);
if (!server_path) {
LOGE("Cannot alloc server path string, "
LOGE("Could not alloc server path string, "
"using " SERVER_FILENAME " from current directory");
SDL_free(executable_path);
return SERVER_FILENAME;
@ -118,21 +118,41 @@ static process_t
execute_server(struct server *server, const struct server_params *params) {
char max_size_string[6];
char bit_rate_string[11];
char max_fps_string[6];
sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
sprintf(max_fps_string, "%"PRIu16, params->max_fps);
const char *const cmd[] = {
"shell",
"CLASSPATH=/data/local/tmp/" SERVER_FILENAME,
"app_process",
#ifdef SERVER_DEBUGGER
# define SERVER_DEBUGGER_PORT "5005"
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
SERVER_DEBUGGER_PORT,
#endif
"/", // unused
"com.genymobile.scrcpy.Server",
SCRCPY_VERSION,
max_size_string,
bit_rate_string,
max_fps_string,
server->tunnel_forward ? "true" : "false",
params->crop ? params->crop : "-",
params->send_frame_meta ? "true" : "false",
"true", // always send frame meta (packet boundaries + timestamp)
params->control ? "true" : "false",
};
#ifdef SERVER_DEBUGGER
LOGI("Server debugger waiting for a client on device port "
SERVER_DEBUGGER_PORT "...");
// From the computer, run
// adb forward tcp:5005 tcp:5005
// Then, from Android Studio: Run > Debug > Edit configurations...
// On the left, click on '+', "Remote", with:
// Host: localhost
// Port: 5005
// Then click on "Debug"
#endif
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
}
@ -155,6 +175,7 @@ connect_and_read_byte(uint16_t port) {
// is not listening, so read one byte to detect a working connection
if (net_recv(socket, &byte, 1) != 1) {
// the server is not listening yet behind the adb tunnel
net_close(socket);
return INVALID_SOCKET;
}
return socket;
@ -181,7 +202,7 @@ close_socket(socket_t *socket) {
SDL_assert(*socket != INVALID_SOCKET);
net_shutdown(*socket, SHUT_RDWR);
if (!net_close(*socket)) {
LOGW("Cannot close socket");
LOGW("Could not close socket");
return;
}
*socket = INVALID_SOCKET;
@ -260,7 +281,7 @@ server_connect_to(struct server *server) {
server->control_socket = net_accept(server->server_socket);
if (server->control_socket == INVALID_SOCKET) {
// the video_socket will be clean up on destroy
// the video_socket will be cleaned up on destroy
return false;
}
@ -305,7 +326,7 @@ server_stop(struct server *server) {
SDL_assert(server->process != PROCESS_NONE);
if (!cmd_terminate(server->process)) {
LOGW("Cannot terminate server");
LOGW("Could not terminate server");
}
cmd_simple_wait(server->process, NULL); // ignore exit code

View file

@ -4,6 +4,7 @@
#include <stdbool.h>
#include <stdint.h>
#include "config.h"
#include "command.h"
#include "net.h"
@ -34,7 +35,7 @@ struct server_params {
uint16_t local_port;
uint16_t max_size;
uint32_t bit_rate;
bool send_frame_meta;
uint16_t max_fps;
bool control;
};

View file

@ -10,6 +10,8 @@
#include <SDL2/SDL_stdinc.h>
#include "config.h"
size_t
xstrncpy(char *dest, const char *src, size_t n) {
size_t i;

View file

@ -3,6 +3,8 @@
#include <stddef.h>
#include "config.h"
// like strncpy, except:
// - it copies at most n-1 chars
// - the dest string is nul-terminated

View file

@ -8,8 +8,8 @@
#include <SDL2/SDL_thread.h>
#include <unistd.h>
#include "compat.h"
#include "config.h"
#include "compat.h"
#include "buffer_util.h"
#include "decoder.h"
#include "events.h"
@ -22,54 +22,8 @@
#define HEADER_SIZE 12
#define NO_PTS UINT64_C(-1)
static struct frame_meta *
frame_meta_new(uint64_t pts) {
struct frame_meta *meta = SDL_malloc(sizeof(*meta));
if (!meta) {
return meta;
}
meta->pts = pts;
meta->next = NULL;
return meta;
}
static void
frame_meta_delete(struct frame_meta *frame_meta) {
SDL_free(frame_meta);
}
static bool
receiver_state_push_meta(struct receiver_state *state, uint64_t pts) {
struct frame_meta *frame_meta = frame_meta_new(pts);
if (!frame_meta) {
return false;
}
// append to the list
// (iterate to find the last item, in practice the list should be tiny)
struct frame_meta **p = &state->frame_meta_queue;
while (*p) {
p = &(*p)->next;
}
*p = frame_meta;
return true;
}
static uint64_t
receiver_state_take_meta(struct receiver_state *state) {
struct frame_meta *frame_meta = state->frame_meta_queue; // first item
SDL_assert(frame_meta); // must not be empty
uint64_t pts = frame_meta->pts;
state->frame_meta_queue = frame_meta->next; // remove the item
frame_meta_delete(frame_meta);
return pts;
}
static int
read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) {
struct stream *stream = opaque;
struct receiver_state *state = &stream->receiver_state;
stream_recv_packet(struct stream *stream, AVPacket *packet) {
// The video stream contains raw packets, without time information. When we
// record, we retrieve the timestamps separately, from a "meta" header
// added by the server before each raw packet.
@ -82,60 +36,30 @@ read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) {
//
// It is followed by <packet_size> bytes containing the packet/frame.
if (!state->remaining) {
#define HEADER_SIZE 12
uint8_t header[HEADER_SIZE];
ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE);
if (r == -1) {
return AVERROR(errno);
}
if (r == 0) {
return AVERROR_EOF;
}
// no partial read (net_recv_all())
SDL_assert_release(r == HEADER_SIZE);
uint64_t pts = buffer_read64be(header);
state->remaining = buffer_read32be(&header[8]);
if (pts != NO_PTS && !receiver_state_push_meta(state, pts)) {
LOGE("Could not store PTS for recording");
// we cannot save the PTS, the recording would be broken
return AVERROR(ENOMEM);
}
uint8_t header[HEADER_SIZE];
ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE);
if (r < HEADER_SIZE) {
return false;
}
SDL_assert(state->remaining);
uint64_t pts = buffer_read64be(header);
uint32_t len = buffer_read32be(&header[8]);
SDL_assert(len);
if (buf_size > state->remaining) {
buf_size = state->remaining;
if (av_new_packet(packet, len)) {
LOGE("Could not allocate packet");
return false;
}
ssize_t r = net_recv(stream->socket, buf, buf_size);
if (r == -1) {
return errno ? AVERROR(errno) : AVERROR_EOF;
}
if (r == 0) {
return AVERROR_EOF;
r = net_recv_all(stream->socket, packet->data, len);
if (r < len) {
av_packet_unref(packet);
return false;
}
SDL_assert(state->remaining >= r);
state->remaining -= r;
packet->pts = pts != NO_PTS ? pts : AV_NOPTS_VALUE;
return r;
}
static int
read_raw_packet(void *opaque, uint8_t *buf, int buf_size) {
struct stream *stream = opaque;
ssize_t r = net_recv(stream->socket, buf, buf_size);
if (r == -1) {
return errno ? AVERROR(errno) : AVERROR_EOF;
}
if (r == 0) {
return AVERROR_EOF;
}
return r;
return true;
}
static void
@ -145,116 +69,199 @@ notify_stopped(void) {
SDL_PushEvent(&stop_event);
}
static bool
process_config_packet(struct stream *stream, AVPacket *packet) {
if (stream->recorder && !recorder_push(stream->recorder, packet)) {
LOGE("Could not send config packet to recorder");
return false;
}
return true;
}
static bool
process_frame(struct stream *stream, AVPacket *packet) {
if (stream->decoder && !decoder_push(stream->decoder, packet)) {
return false;
}
if (stream->recorder) {
packet->dts = packet->pts;
if (!recorder_push(stream->recorder, packet)) {
LOGE("Could not send packet to recorder");
return false;
}
}
return true;
}
static bool
stream_parse(struct stream *stream, AVPacket *packet) {
uint8_t *in_data = packet->data;
int in_len = packet->size;
uint8_t *out_data = NULL;
int out_len = 0;
int r = av_parser_parse2(stream->parser, stream->codec_ctx,
&out_data, &out_len, in_data, in_len,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1);
// PARSER_FLAG_COMPLETE_FRAMES is set
SDL_assert(r == in_len);
SDL_assert(out_len == in_len);
if (stream->parser->key_frame == 1) {
packet->flags |= AV_PKT_FLAG_KEY;
}
bool ok = process_frame(stream, packet);
if (!ok) {
LOGE("Could not process frame");
return false;
}
return true;
}
static bool
stream_push_packet(struct stream *stream, AVPacket *packet) {
bool is_config = packet->pts == AV_NOPTS_VALUE;
// A config packet must not be decoded immetiately (it contains no
// frame); instead, it must be concatenated with the future data packet.
if (stream->has_pending || is_config) {
size_t offset;
if (stream->has_pending) {
offset = stream->pending.size;
if (av_grow_packet(&stream->pending, packet->size)) {
LOGE("Could not grow packet");
return false;
}
} else {
offset = 0;
if (av_new_packet(&stream->pending, packet->size)) {
LOGE("Could not create packet");
return false;
}
stream->has_pending = true;
}
memcpy(stream->pending.data + offset, packet->data, packet->size);
if (!is_config) {
// prepare the concat packet to send to the decoder
stream->pending.pts = packet->pts;
stream->pending.dts = packet->dts;
stream->pending.flags = packet->flags;
packet = &stream->pending;
}
}
if (is_config) {
// config packet
bool ok = process_config_packet(stream, packet);
if (!ok) {
return false;
}
} else {
// data packet
bool ok = stream_parse(stream, packet);
if (stream->has_pending) {
// the pending packet must be discarded (consumed or error)
stream->has_pending = false;
av_packet_unref(&stream->pending);
}
if (!ok) {
return false;
}
}
return true;
}
static int
run_stream(void *data) {
struct stream *stream = data;
AVFormatContext *format_ctx = avformat_alloc_context();
if (!format_ctx) {
LOGC("Could not allocate format context");
goto end;
}
unsigned char *buffer = av_malloc(BUFSIZE);
if (!buffer) {
LOGC("Could not allocate buffer");
goto finally_free_format_ctx;
}
// initialize the receiver state
stream->receiver_state.frame_meta_queue = NULL;
stream->receiver_state.remaining = 0;
// if recording is enabled, a "header" is sent between raw packets
int (*read_packet)(void *, uint8_t *, int) =
stream->recorder ? read_packet_with_meta : read_raw_packet;
AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, stream,
read_packet, NULL, NULL);
if (!avio_ctx) {
LOGC("Could not allocate avio context");
// avformat_open_input takes ownership of 'buffer'
// so only free the buffer before avformat_open_input()
av_free(buffer);
goto finally_free_format_ctx;
}
format_ctx->pb = avio_ctx;
if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) {
LOGE("Could not open video stream");
goto finally_free_avio_ctx;
}
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
LOGE("H.264 decoder not found");
goto end;
}
stream->codec_ctx = avcodec_alloc_context3(codec);
if (!stream->codec_ctx) {
LOGC("Could not allocate codec context");
goto end;
}
if (stream->decoder && !decoder_open(stream->decoder, codec)) {
LOGE("Could not open decoder");
goto finally_close_input;
goto finally_free_codec_ctx;
}
if (stream->recorder && !recorder_open(stream->recorder, codec)) {
LOGE("Could not open recorder");
goto finally_close_input;
if (stream->recorder) {
if (!recorder_open(stream->recorder, codec)) {
LOGE("Could not open recorder");
goto finally_close_decoder;
}
if (!recorder_start(stream->recorder)) {
LOGE("Could not start recorder");
goto finally_close_recorder;
}
}
AVPacket packet;
av_init_packet(&packet);
packet.data = NULL;
packet.size = 0;
stream->parser = av_parser_init(AV_CODEC_ID_H264);
if (!stream->parser) {
LOGE("Could not initialize parser");
goto finally_stop_and_join_recorder;
}
while (!av_read_frame(format_ctx, &packet)) {
if (SDL_AtomicGet(&stream->stopped)) {
// if the stream is stopped, the socket had been shutdown, so the
// last packet is probably corrupted (but not detected as such by
// FFmpeg) and will not be decoded correctly
av_packet_unref(&packet);
goto quit;
}
if (stream->decoder && !decoder_push(stream->decoder, &packet)) {
av_packet_unref(&packet);
goto quit;
}
if (stream->recorder) {
// we retrieve the PTS in order they were received, so they will
// be assigned to the correct frame
uint64_t pts = receiver_state_take_meta(&stream->receiver_state);
packet.pts = pts;
packet.dts = pts;
// no need to rescale with av_packet_rescale_ts(), the timestamps
// are in microseconds both in input and output
if (!recorder_write(stream->recorder, &packet)) {
LOGE("Could not write frame to output file");
av_packet_unref(&packet);
goto quit;
}
// We must only pass complete frames to av_parser_parse2()!
// It's more complicated, but this allows to reduce the latency by 1 frame!
stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
for (;;) {
AVPacket packet;
bool ok = stream_recv_packet(stream, &packet);
if (!ok) {
// end of stream
break;
}
ok = stream_push_packet(stream, &packet);
av_packet_unref(&packet);
if (avio_ctx->eof_reached) {
if (!ok) {
// cannot process packet (error already logged)
break;
}
}
LOGD("End of frames");
quit:
if (stream->has_pending) {
av_packet_unref(&stream->pending);
}
av_parser_close(stream->parser);
finally_stop_and_join_recorder:
if (stream->recorder) {
recorder_stop(stream->recorder);
LOGI("Finishing recording...");
recorder_join(stream->recorder);
}
finally_close_recorder:
if (stream->recorder) {
recorder_close(stream->recorder);
}
finally_close_input:
avformat_close_input(&format_ctx);
finally_free_avio_ctx:
av_free(avio_ctx->buffer);
av_free(avio_ctx);
finally_free_format_ctx:
avformat_free_context(format_ctx);
finally_close_decoder:
if (stream->decoder) {
decoder_close(stream->decoder);
}
finally_free_codec_ctx:
avcodec_free_context(&stream->codec_ctx);
end:
notify_stopped();
return 0;
@ -266,7 +273,7 @@ stream_init(struct stream *stream, socket_t socket,
stream->socket = socket;
stream->decoder = decoder,
stream->recorder = recorder;
SDL_AtomicSet(&stream->stopped, 0);
stream->has_pending = false;
}
bool
@ -283,7 +290,6 @@ stream_start(struct stream *stream) {
void
stream_stop(struct stream *stream) {
SDL_AtomicSet(&stream->stopped, 1);
if (stream->decoder) {
decoder_interrupt(stream->decoder);
}

View file

@ -3,30 +3,27 @@
#include <stdbool.h>
#include <stdint.h>
#include <libavformat/avformat.h>
#include <SDL2/SDL_atomic.h>
#include <SDL2/SDL_thread.h>
#include "config.h"
#include "net.h"
struct video_buffer;
struct frame_meta {
uint64_t pts;
struct frame_meta *next;
};
struct stream {
socket_t socket;
struct video_buffer *video_buffer;
SDL_Thread *thread;
SDL_atomic_t stopped;
struct decoder *decoder;
struct recorder *recorder;
struct receiver_state {
// meta (in order) for frames not consumed yet
struct frame_meta *frame_meta_queue;
size_t remaining; // remaining bytes to receive for the current frame
} receiver_state;
AVCodecContext *codec_ctx;
AVCodecParserContext *parser;
// successive packets may need to be concatenated, until a non-config
// packet is available
bool has_pending;
AVPacket pending;
};
void

View file

@ -7,6 +7,8 @@
#include "command.h"
#include "config.h"
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
@ -94,7 +96,7 @@ cmd_simple_wait(pid_t pid, int *exit_code) {
int status;
int code;
if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) {
// cannot wait, or exited unexpectedly, probably by a signal
// could not wait, or exited unexpectedly, probably by a signal
code = -1;
} else {
code = WEXITSTATUS(status);

View file

@ -2,6 +2,8 @@
#include <unistd.h>
#include "config.h"
bool
net_init(void) {
// do nothing

View file

@ -33,7 +33,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
wchar_t *wide = utf8_to_wide_char(cmd);
if (!wide) {
LOGC("Cannot allocate wide char string");
LOGC("Could not allocate wide char string");
return PROCESS_ERROR_GENERIC;
}
@ -67,7 +67,7 @@ cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
DWORD code;
if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0
|| !GetExitCodeProcess(handle, &code)) {
// cannot wait or retrieve the exit code
// could not wait or retrieve the exit code
code = -1; // max value, it's unsigned
}
if (exit_code) {

View file

@ -1,5 +1,6 @@
#include "net.h"
#include "config.h"
#include "log.h"
bool

View file

@ -5,6 +5,7 @@
#include <stdio.h>
#include <stdlib.h>
#include "config.h"
#include "log.h"
struct index {

View file

@ -3,6 +3,8 @@
#include <SDL2/SDL.h>
#include "config.h"
SDL_Surface *
read_xpm(char *xpm[]);

View file

@ -4,6 +4,7 @@
#include <stdbool.h>
#include <SDL2/SDL_mutex.h>
#include "config.h"
#include "fps_counter.h"
// forward declarations

View file

@ -67,35 +67,39 @@ static void test_serialize_inject_text_long(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_inject_mouse_event(void) {
static void test_serialize_inject_touch_event(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT,
.inject_mouse_event = {
.type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
.inject_touch_event = {
.action = AMOTION_EVENT_ACTION_DOWN,
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
.pointer_id = 0x1234567887654321L,
.position = {
.point = {
.x = 260,
.y = 1026,
.x = 100,
.y = 200,
},
.screen_size = {
.width = 1080,
.height = 1920,
},
},
.pressure = 1.0f,
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
},
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 18);
assert(size == 28);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT,
CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
0x00, // AKEY_EVENT_ACTION_DOWN
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21, // pointer id
0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200
0x04, 0x38, 0x07, 0x80, // 1080 1920
0xff, 0xff, // pressure
0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
@ -236,7 +240,7 @@ int main(void) {
test_serialize_inject_keycode();
test_serialize_inject_text();
test_serialize_inject_text_long();
test_serialize_inject_mouse_event();
test_serialize_inject_touch_event();
test_serialize_inject_scroll_event();
test_serialize_back_or_screen_on();
test_serialize_expand_notification_panel();

38
app/tests/test_queue.c Normal file
View file

@ -0,0 +1,38 @@
#include <assert.h>
#include <queue.h>
struct foo {
int value;
struct foo *next;
};
static void test_queue(void) {
struct my_queue QUEUE(struct foo) queue;
queue_init(&queue);
assert(queue_is_empty(&queue));
struct foo v1 = { .value = 42 };
struct foo v2 = { .value = 27 };
queue_push(&queue, next, &v1);
queue_push(&queue, next, &v2);
struct foo *foo;
assert(!queue_is_empty(&queue));
queue_take(&queue, next, &foo);
assert(foo->value == 42);
assert(!queue_is_empty(&queue));
queue_take(&queue, next, &foo);
assert(foo->value == 27);
assert(queue_is_empty(&queue));
}
int main(void) {
test_queue();
return 0;
}

View file

@ -7,7 +7,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.0'
classpath 'com.android.tools.build:gradle:3.4.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View file

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

View file

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

View file

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

Binary file not shown.

View file

@ -1,6 +1,5 @@
#Thu Apr 18 11:45:59 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip

116
gradlew vendored
View file

@ -1,4 +1,20 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
@ -6,42 +22,6 @@
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
@ -60,6 +40,46 @@ cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@ -85,7 +105,7 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@ -150,11 +170,19 @@ if $cygwin ; then
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
APP_ARGS=$(save "$@")
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

30
gradlew.bat vendored
View file

@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@ -8,14 +24,14 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@ -46,10 +62,9 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
@ -60,11 +75,6 @@ set _SKIP=2
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line

View file

@ -1,13 +1,13 @@
project('scrcpy', 'c',
version: '1.9',
version: '1.11',
meson_version: '>= 0.37',
default_options: 'c_std=c11')
if get_option('build_app')
if get_option('compile_app')
subdir('app')
endif
if get_option('build_server')
if get_option('compile_server')
subdir('server')
endif

View file

@ -1,8 +1,8 @@
option('build_app', type: 'boolean', value: true, description: 'Build the client')
option('build_server', type: 'boolean', value: true, description: 'Build the server')
option('compile_app', type: 'boolean', value: true, description: 'Build the client')
option('compile_server', type: 'boolean', value: true, description: 'Build the server')
option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux')
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('portable', type: 'boolean', description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable')
option('skip_frames', type: 'boolean', value: true, description: 'Always display the most recent frame')
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')

View file

@ -10,31 +10,31 @@ 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-ffmpeg-shared-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1.3-win32-shared.zip \
8ea472d673370d5e87517a75587abfa6f189ee4f82e8da21fdbc49d0db0c1a89 \
ffmpeg-4.1.3-win32-shared
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.1-win32-shared.zip \
9208255f409410d95147151d7e829b5699bf8d91bfe1e81c3f470f47c2fa66d2 \
ffmpeg-4.2.1-win32-shared
prepare-ffmpeg-dev-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.3-win32-dev.zip \
e16d3150b6ccf0b71908f5b964cb8c051d79053c8f5cd6d777d617ab4f03613a \
ffmpeg-4.1.3-win32-dev
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.1-win32-dev.zip \
c3469e6c5f031cbcc8cba88dee92d6548c5c6b6ff14f4097f18f72a92d0d70c4 \
ffmpeg-4.2.1-win32-dev
prepare-ffmpeg-shared-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.3-win64-shared.zip \
0b974578e07d974c4bafb36c7ed0b46e46b001d38b149455089c13b57ddefe5d \
ffmpeg-4.1.3-win64-shared
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.1-win64-shared.zip \
55063d3cf750a75485c7bf196031773d81a1b25d0980c7db48ecfc7701a42331 \
ffmpeg-4.2.1-win64-shared
prepare-ffmpeg-dev-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.3-win64-dev.zip \
334b473467db096a5b74242743592a73e120a137232794508e4fc55593696a5b \
ffmpeg-4.1.3-win64-dev
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.1-win64-dev.zip \
5af393be5f25c0a71aa29efce768e477c35347f7f8e0d9696767d5b9d405b74e \
ffmpeg-4.2.1-win64-dev
prepare-sdl2:
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.8-mingw.tar.gz \
ffff7305d634aff5e1df5b7bb935435c3a02c8b03ad94a1a2be9169a558a7961 \
SDL2-2.0.8
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \
a90a7cddaec4996f4d7be6d80c57ec69b062e132bffc513965f99217f603274a \
SDL2-2.0.10
prepare-adb:
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.1-windows.zip \
2334f92cf571fd2d9bf6ff7c637765bee5d8323e0bd8e051e15927d87b54b4e8 \
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \
2df06160056ec9a84c7334af2a1e42740befbb1a2e34370e7af544a2cc78152c \
platform-tools

View file

@ -23,21 +23,21 @@ cd -
make -f Makefile.CrossWindows
# the generated server must be the same everywhere
cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win32/scrcpy-server.jar
cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win64/scrcpy-server.jar
cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win32/scrcpy-server
cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win64/scrcpy-server
# get version name
TAG=$(git describe --tags --always)
# create release directory
mkdir -p "release-$TAG"
cp "$BUILDDIR/server/scrcpy-server.jar" "release-$TAG/scrcpy-server-$TAG.jar"
cp "$BUILDDIR/server/scrcpy-server" "release-$TAG/scrcpy-server-$TAG"
cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/"
cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/"
# generate checksums
cd "release-$TAG"
sha256sum "scrcpy-server-$TAG.jar" \
sha256sum "scrcpy-server-$TAG" \
"scrcpy-win32-$TAG.zip" \
"scrcpy-win64-$TAG.zip" > SHA256SUMS.txt

2
run
View file

@ -20,4 +20,4 @@ then
exit 1
fi
SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server.jar" "$BUILDDIR/app/scrcpy" "$@"
SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" "$BUILDDIR/app/scrcpy" "$@"

View file

@ -1,2 +1,2 @@
#!/bin/bash
SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server.jar" "$MESON_BUILD_ROOT/app/scrcpy"
SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy"

View file

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

66
server/build_without_gradle.sh Executable file
View file

@ -0,0 +1,66 @@
#!/bin/bash
#
# This script generates the scrcpy binary "manually" (without gradle).
#
# Adapt Android platform and build tools versions (via ANDROID_PLATFORM and
# ANDROID_BUILD_TOOLS environment variables).
#
# Then execute:
#
# BUILD_DIR=my_build_dir ./build_without_gradle.sh
set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=1.11
PLATFORM=${ANDROID_PLATFORM:-29}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
CLASSES_DIR="$BUILD_DIR/classes"
SERVER_DIR=$(dirname "$0")
SERVER_BINARY=scrcpy-server
echo "Platform: android-$PLATFORM"
echo "Build-tools: $BUILD_TOOLS"
echo "Build dir: $BUILD_DIR"
rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy"
<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java"
package com.genymobile.scrcpy;
public final class BuildConfig {
public static final boolean DEBUG = $SCRCPY_DEBUG;
public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME";
}
EOF
echo "Generating java from aidl..."
cd "$SERVER_DIR/src/main/aidl"
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o "$CLASSES_DIR" \
android/view/IRotationWatcher.aidl
echo "Compiling java sources..."
cd ../java
javac -bootclasspath "$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" \
-cp "$CLASSES_DIR" -d "$CLASSES_DIR" -source 1.8 -target 1.8 \
com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/wrappers/*.java
echo "Dexing..."
cd "$CLASSES_DIR"
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \
--output "$BUILD_DIR/classes.dex" \
android/view/*.class \
com/genymobile/scrcpy/*.class \
com/genymobile/scrcpy/wrappers/*.class
echo "Archiving..."
cd "$BUILD_DIR"
jar cvf "$SERVER_BINARY" classes.dex
rm -rf classes.dex classes
echo "Server generated in $BUILD_DIR/$SERVER_BINARY"

View file

@ -4,8 +4,9 @@ prebuilt_server = get_option('prebuilt_server')
if prebuilt_server == ''
custom_target('scrcpy-server',
build_always: true, # gradle is responsible for tracking source changes
output: 'scrcpy-server.jar',
output: 'scrcpy-server',
command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')],
console: true,
install: true,
install_dir: 'share/scrcpy')
else
@ -15,7 +16,7 @@ else
endif
custom_target('scrcpy-server-prebuilt',
input: prebuilt_server,
output: 'scrcpy-server.jar',
output: 'scrcpy-server',
command: ['cp', '@INPUT@', '@OUTPUT@'],
install: true,
install_dir: 'share/scrcpy')

View file

@ -7,7 +7,7 @@ public final class ControlMessage {
public static final int TYPE_INJECT_KEYCODE = 0;
public static final int TYPE_INJECT_TEXT = 1;
public static final int TYPE_INJECT_MOUSE_EVENT = 2;
public static final int TYPE_INJECT_TOUCH_EVENT = 2;
public static final int TYPE_INJECT_SCROLL_EVENT = 3;
public static final int TYPE_BACK_OR_SCREEN_ON = 4;
public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5;
@ -22,6 +22,8 @@ public final class ControlMessage {
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
private int keycode; // KeyEvent.KEYCODE_*
private int buttons; // MotionEvent.BUTTON_*
private long pointerId;
private float pressure;
private Position position;
private int hScroll;
private int vScroll;
@ -30,60 +32,63 @@ public final class ControlMessage {
}
public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_KEYCODE;
event.action = action;
event.keycode = keycode;
event.metaState = metaState;
return event;
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_KEYCODE;
msg.action = action;
msg.keycode = keycode;
msg.metaState = metaState;
return msg;
}
public static ControlMessage createInjectText(String text) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_TEXT;
event.text = text;
return event;
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_TEXT;
msg.text = text;
return msg;
}
public static ControlMessage createInjectMouseEvent(int action, int buttons, Position position) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_MOUSE_EVENT;
event.action = action;
event.buttons = buttons;
event.position = position;
return event;
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure,
int buttons) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_TOUCH_EVENT;
msg.action = action;
msg.pointerId = pointerId;
msg.pressure = pressure;
msg.position = position;
msg.buttons = buttons;
return msg;
}
public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_SCROLL_EVENT;
event.position = position;
event.hScroll = hScroll;
event.vScroll = vScroll;
return event;
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_SCROLL_EVENT;
msg.position = position;
msg.hScroll = hScroll;
msg.vScroll = vScroll;
return msg;
}
public static ControlMessage createSetClipboard(String text) {
ControlMessage event = new ControlMessage();
event.type = TYPE_SET_CLIPBOARD;
event.text = text;
return event;
ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_CLIPBOARD;
msg.text = text;
return msg;
}
/**
* @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants
*/
public static ControlMessage createSetScreenPowerMode(int mode) {
ControlMessage event = new ControlMessage();
event.type = TYPE_SET_SCREEN_POWER_MODE;
event.action = mode;
return event;
ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_SCREEN_POWER_MODE;
msg.action = mode;
return msg;
}
public static ControlMessage createEmpty(int type) {
ControlMessage event = new ControlMessage();
event.type = type;
return event;
ControlMessage msg = new ControlMessage();
msg.type = type;
return msg;
}
public int getType() {
@ -110,6 +115,14 @@ public final class ControlMessage {
return buttons;
}
public long getPointerId() {
return pointerId;
}
public float getPressure() {
return pressure;
}
public Position getPosition() {
return position;
}

View file

@ -10,6 +10,7 @@ public class ControlMessageReader {
private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9;
private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17;
private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 21;
private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
@ -59,8 +60,8 @@ public class ControlMessageReader {
case ControlMessage.TYPE_INJECT_TEXT:
msg = parseInjectText();
break;
case ControlMessage.TYPE_INJECT_MOUSE_EVENT:
msg = parseInjectMouseEvent();
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
msg = parseInjectTouchEvent();
break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
msg = parseInjectScrollEvent();
@ -120,14 +121,20 @@ public class ControlMessageReader {
return ControlMessage.createInjectText(text);
}
private ControlMessage parseInjectMouseEvent() {
if (buffer.remaining() < INJECT_MOUSE_EVENT_PAYLOAD_LENGTH) {
@SuppressWarnings("checkstyle:MagicNumber")
private ControlMessage parseInjectTouchEvent() {
if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
int buttons = buffer.getInt();
long pointerId = buffer.getLong();
Position position = readPosition(buffer);
return ControlMessage.createInjectMouseEvent(action, buttons, position);
// 16 bits fixed-point
int pressureInt = toUnsigned(buffer.getShort());
// convert it to a float between 0 and 1 (0x1p16f is 2^16 as float)
float pressure = pressureInt == 0xffff ? 1f : (pressureInt / 0x1p16f);
int buttons = buffer.getInt();
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
}
private ControlMessage parseInjectScrollEvent() {

View file

@ -19,38 +19,32 @@ public class Controller {
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
private long lastMouseDown;
private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()};
private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()};
private long lastTouchDown;
private final PointersState pointersState = new PointersState();
private final MotionEvent.PointerProperties[] pointerProperties =
new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
private final MotionEvent.PointerCoords[] pointerCoords =
new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
public Controller(Device device, DesktopConnection connection) {
this.device = device;
this.connection = connection;
initPointer();
initPointers();
sender = new DeviceMessageSender(connection);
}
private void initPointer() {
MotionEvent.PointerProperties props = pointerProperties[0];
props.id = 0;
props.toolType = MotionEvent.TOOL_TYPE_FINGER;
private void initPointers() {
for (int i = 0; i < PointersState.MAX_POINTERS; ++i) {
MotionEvent.PointerProperties props = new MotionEvent.PointerProperties();
props.toolType = MotionEvent.TOOL_TYPE_FINGER;
MotionEvent.PointerCoords coords = pointerCoords[0];
coords.orientation = 0;
coords.pressure = 1;
coords.size = 1;
}
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
coords.orientation = 0;
coords.size = 1;
private void setPointerCoords(Point point) {
MotionEvent.PointerCoords coords = pointerCoords[0];
coords.x = point.getX();
coords.y = point.getY();
}
private void setScroll(int hScroll, int vScroll) {
MotionEvent.PointerCoords coords = pointerCoords[0];
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
pointerProperties[i] = props;
pointerCoords[i] = coords;
}
}
@SuppressWarnings("checkstyle:MagicNumber")
@ -87,8 +81,8 @@ public class Controller {
case ControlMessage.TYPE_INJECT_TEXT:
injectText(msg.getText());
break;
case ControlMessage.TYPE_INJECT_MOUSE_EVENT:
injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition());
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
@ -148,19 +142,42 @@ public class Controller {
return successCount;
}
private boolean injectMouse(int action, int buttons, Position position) {
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
long now = SystemClock.uptimeMillis();
if (action == MotionEvent.ACTION_DOWN) {
lastMouseDown = now;
}
Point point = device.getPhysicalPoint(position);
if (point == null) {
// ignore event
return false;
}
setPointerCoords(point);
MotionEvent event = MotionEvent.obtain(lastMouseDown, now, action, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, 0, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0);
int pointerIndex = pointersState.getPointerIndex(pointerId);
if (pointerIndex == -1) {
Ln.w("Too many pointers for touch event");
return false;
}
Pointer pointer = pointersState.get(pointerIndex);
pointer.setPoint(point);
pointer.setPressure(pressure);
pointer.setUp(action == MotionEvent.ACTION_UP);
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
if (pointerCount == 1) {
if (action == MotionEvent.ACTION_DOWN) {
lastTouchDown = now;
}
} else {
// secondary pointers must use ACTION_POINTER_* ORed with the pointerIndex
if (action == MotionEvent.ACTION_UP) {
action = MotionEvent.ACTION_POINTER_UP | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
} else if (action == MotionEvent.ACTION_DOWN) {
action = MotionEvent.ACTION_POINTER_DOWN | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
}
}
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
return injectEvent(event);
}
@ -171,23 +188,30 @@ public class Controller {
// ignore event
return false;
}
setPointerCoords(point);
setScroll(hScroll, vScroll);
MotionEvent event = MotionEvent.obtain(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, 0,
0, InputDevice.SOURCE_MOUSE, 0);
MotionEvent.PointerProperties props = pointerProperties[0];
props.id = 0;
MotionEvent.PointerCoords coords = pointerCoords[0];
coords.x = point.getX();
coords.y = point.getY();
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0);
return injectEvent(event);
}
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
long now = SystemClock.uptimeMillis();
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
InputDevice.SOURCE_KEYBOARD);
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD,
0, 0, InputDevice.SOURCE_KEYBOARD);
return injectEvent(event);
}
private boolean injectKeycode(int keyCode) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0)
&& injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
}
private boolean injectEvent(InputEvent event) {

View file

@ -161,7 +161,11 @@ public final class Device {
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants
*/
public void setScreenPowerMode(int mode) {
IBinder d = SurfaceControl.getBuiltInDisplay(0);
IBinder d = SurfaceControl.getBuiltInDisplay();
if (d == null) {
Ln.e("Could not get built-in display");
return;
}
SurfaceControl.setDisplayPowerMode(d, mode);
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
}

View file

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

View file

@ -0,0 +1,55 @@
package com.genymobile.scrcpy;
public class Pointer {
/**
* Pointer id as received from the client.
*/
private final long id;
/**
* Local pointer id, using the lowest possible values to fill the {@link android.view.MotionEvent.PointerProperties PointerProperties}.
*/
private final int localId;
private Point point;
private float pressure;
private boolean up;
public Pointer(long id, int localId) {
this.id = id;
this.localId = localId;
}
public long getId() {
return id;
}
public int getLocalId() {
return localId;
}
public Point getPoint() {
return point;
}
public void setPoint(Point point) {
this.point = point;
}
public float getPressure() {
return pressure;
}
public void setPressure(float pressure) {
this.pressure = pressure;
}
public boolean isUp() {
return up;
}
public void setUp(boolean up) {
this.up = up;
}
}

View file

@ -0,0 +1,103 @@
package com.genymobile.scrcpy;
import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.List;
public class PointersState {
public static final int MAX_POINTERS = 10;
private final List<Pointer> pointers = new ArrayList<>();
private int indexOf(long id) {
for (int i = 0; i < pointers.size(); ++i) {
Pointer pointer = pointers.get(i);
if (pointer.getId() == id) {
return i;
}
}
return -1;
}
private boolean isLocalIdAvailable(int localId) {
for (int i = 0; i < pointers.size(); ++i) {
Pointer pointer = pointers.get(i);
if (pointer.getLocalId() == localId) {
return false;
}
}
return true;
}
private int nextUnusedLocalId() {
for (int localId = 0; localId < MAX_POINTERS; ++localId) {
if (isLocalIdAvailable(localId)) {
return localId;
}
}
return -1;
}
public Pointer get(int index) {
return pointers.get(index);
}
public int getPointerIndex(long id) {
int index = indexOf(id);
if (index != -1) {
// already exists, return it
return index;
}
if (pointers.size() >= MAX_POINTERS) {
// it's full
return -1;
}
// id 0 is reserved for mouse events
int localId = nextUnusedLocalId();
if (localId == -1) {
throw new AssertionError("pointers.size() < maxFingers implies that a local id is available");
}
Pointer pointer = new Pointer(id, localId);
pointers.add(pointer);
// return the index of the pointer
return pointers.size() - 1;
}
/**
* Initialize the motion event parameters.
*
* @param props the pointer properties
* @param coords the pointer coordinates
* @return The number of items initialized (the number of pointers).
*/
public int update(MotionEvent.PointerProperties[] props, MotionEvent.PointerCoords[] coords) {
int count = pointers.size();
for (int i = 0; i < count; ++i) {
Pointer pointer = pointers.get(i);
// id 0 is reserved for mouse events
props[i].id = pointer.getLocalId();
Point point = pointer.getPoint();
coords[i].x = point.getX();
coords[i].y = point.getY();
coords[i].pressure = pointer.getPressure();
}
cleanUp();
return count;
}
/**
* Remove all pointers which are UP.
*/
private void cleanUp() {
for (int i = pointers.size() - 1; i >= 0; --i) {
Pointer pointer = pointers.get(i);
if (pointer.isUp()) {
pointers.remove(i);
}
}
}
}

View file

@ -6,6 +6,7 @@ import android.graphics.Rect;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Build;
import android.os.IBinder;
import android.view.Surface;
@ -16,32 +17,29 @@ import java.util.concurrent.atomic.AtomicBoolean;
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 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 final AtomicBoolean rotationChanged = new AtomicBoolean();
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
private int bitRate;
private int frameRate;
private int maxFps;
private int iFrameInterval;
private boolean sendFrameMeta;
private long ptsOrigin;
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int frameRate, int iFrameInterval) {
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) {
this.sendFrameMeta = sendFrameMeta;
this.bitRate = bitRate;
this.frameRate = frameRate;
this.maxFps = maxFps;
this.iFrameInterval = iFrameInterval;
}
public ScreenEncoder(boolean sendFrameMeta, int bitRate) {
this(sendFrameMeta, bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) {
this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL);
}
@Override
@ -54,7 +52,10 @@ public class ScreenEncoder implements Device.RotationListener {
}
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval);
Workarounds.prepareMainLooper();
Workarounds.fillAppInfo();
MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval);
device.setRotationListener(this);
boolean alive;
try {
@ -137,15 +138,24 @@ public class ScreenEncoder implements Device.RotationListener {
return MediaCodec.createEncoderByType("video/avc");
}
private static MediaFormat createFormat(int bitRate, int frameRate, int iFrameInterval) throws IOException {
@SuppressWarnings("checkstyle:MagicNumber")
private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, "video/avc");
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
// display the very first frame, and recover from bad quality when no new frames
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate); // µs
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µ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;
}

View file

@ -7,7 +7,7 @@ import java.io.IOException;
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() {
// not instantiable
@ -17,7 +17,7 @@ public final class Server {
final Device device = new Device(options);
boolean tunnelForward = options.isTunnelForward();
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate());
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps());
if (options.getControl()) {
Controller controller = new Controller(device, connection);
@ -67,29 +67,42 @@ public final class Server {
@SuppressWarnings("checkstyle:MagicNumber")
private static Options createOptions(String... args) {
if (args.length != 6) {
throw new IllegalArgumentException("Expecting 5 parameters");
if (args.length < 1) {
throw new IllegalArgumentException("Missing client version");
}
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();
int maxSize = Integer.parseInt(args[0]) & ~7; // multiple of 8
int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8
options.setMaxSize(maxSize);
int bitRate = Integer.parseInt(args[1]);
int bitRate = Integer.parseInt(args[2]);
options.setBitRate(bitRate);
int maxFps = Integer.parseInt(args[3]);
options.setMaxFps(maxFps);
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(args[2]);
boolean tunnelForward = Boolean.parseBoolean(args[4]);
options.setTunnelForward(tunnelForward);
Rect crop = parseCrop(args[3]);
Rect crop = parseCrop(args[5]);
options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[4]);
boolean sendFrameMeta = Boolean.parseBoolean(args[6]);
options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[5]);
boolean control = Boolean.parseBoolean(args[7]);
options.setControl(control);
return options;
@ -116,7 +129,7 @@ public final class Server {
try {
new File(SERVER_PATH).delete();
} catch (Exception e) {
Ln.e("Cannot unlink server", e);
Ln.e("Could not unlink server", e);
}
}

View file

@ -0,0 +1,64 @@
package com.genymobile.scrcpy;
import android.annotation.SuppressLint;
import android.content.pm.ApplicationInfo;
import android.os.Looper;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
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 appInfo = appBindDataClass.getDeclaredField("appInfo");
appInfo.setAccessible(true);
appInfo.set(appBindData, applicationInfo);
// activityThread.mBoundApplication = appBindData;
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
mBoundApplicationField.setAccessible(true);
mBoundApplicationField.set(activityThread, appBindData);
} catch (Throwable throwable) {
// this is a workaround, so failing is not an error
Ln.w("Could not fill app info: " + throwable.getMessage());
}
}
}

View file

@ -1,44 +1,102 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.content.ClipData;
import android.os.Build;
import android.os.IInterface;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ClipboardManager {
private static final String PACKAGE_NAME = "com.android.shell";
private static final int USER_ID = 0;
private final IInterface manager;
private final Method getPrimaryClipMethod;
private final Method setPrimaryClipMethod;
private Method getPrimaryClipMethod;
private Method setPrimaryClipMethod;
public ClipboardManager(IInterface manager) {
this.manager = manager;
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
} catch (NoSuchMethodException e) {
throw new AssertionError(e);
}
private Method getGetPrimaryClipMethod() {
if (getPrimaryClipMethod == null) {
try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
} else {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
}
} catch (NoSuchMethodException e) {
Ln.e("Could not find method", e);
}
}
return getPrimaryClipMethod;
}
private Method getSetPrimaryClipMethod() {
if (setPrimaryClipMethod == null) {
try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
} else {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class,
String.class, int.class);
}
} catch (NoSuchMethodException e) {
Ln.e("Could not find method", e);
}
}
return setPrimaryClipMethod;
}
private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException,
IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return (ClipData) method.invoke(manager, PACKAGE_NAME);
}
return (ClipData) method.invoke(manager, PACKAGE_NAME, USER_ID);
}
private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) throws InvocationTargetException,
IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, clipData, PACKAGE_NAME);
} else {
method.invoke(manager, clipData, PACKAGE_NAME, USER_ID);
}
}
public CharSequence getText() {
Method method = getGetPrimaryClipMethod();
if (method == null) {
return null;
}
try {
ClipData clipData = (ClipData) getPrimaryClipMethod.invoke(manager, "com.android.shell");
ClipData clipData = getPrimaryClip(method, manager);
if (clipData == null || clipData.getItemCount() == 0) {
return null;
}
return clipData.getItemAt(0).getText();
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
Ln.e("Could not invoke " + method.getName(), e);
return null;
}
}
public void setText(CharSequence text) {
Method method = getSetPrimaryClipMethod();
if (method == null) {
return;
}
ClipData clipData = ClipData.newPlainText(null, text);
try {
setPrimaryClipMethod.invoke(manager, clipData, "com.android.shell");
setPrimaryClip(method, manager, clipData);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
Ln.e("Could not invoke " + method.getName(), e);
}
}
}

View file

@ -1,5 +1,7 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.os.IInterface;
import android.view.InputEvent;
@ -13,22 +15,33 @@ public final class InputManager {
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
private final IInterface manager;
private final Method injectInputEventMethod;
private Method injectInputEventMethod;
public InputManager(IInterface manager) {
this.manager = manager;
try {
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
} catch (NoSuchMethodException e) {
throw new AssertionError(e);
}
private Method getInjectInputEventMethod() {
if (injectInputEventMethod == null) {
try {
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
} catch (NoSuchMethodException e) {
Ln.e("Could not find method", e);
}
}
return injectInputEventMethod;
}
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
Method method = getInjectInputEventMethod();
if (method == null) {
return false;
}
try {
return (Boolean) injectInputEventMethod.invoke(manager, inputEvent, mode);
return (Boolean) method.invoke(manager, inputEvent, mode);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
Ln.e("Could not invoke " + method.getName(), e);
return false;
}
}
}

View file

@ -1,5 +1,7 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.IInterface;
@ -9,24 +11,35 @@ import java.lang.reflect.Method;
public final class PowerManager {
private final IInterface manager;
private final Method isScreenOnMethod;
private Method isScreenOnMethod;
public PowerManager(IInterface manager) {
this.manager = manager;
try {
@SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future
String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
isScreenOnMethod = manager.getClass().getMethod(methodName);
} catch (NoSuchMethodException e) {
throw new AssertionError(e);
}
private Method getIsScreenOnMethod() {
if (isScreenOnMethod == null) {
try {
@SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future
String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
isScreenOnMethod = manager.getClass().getMethod(methodName);
} catch (NoSuchMethodException e) {
Ln.e("Could not find method", e);
}
}
return isScreenOnMethod;
}
public boolean isScreenOn() {
Method method = getIsScreenOnMethod();
if (method == null) {
return false;
}
try {
return (Boolean) isScreenOnMethod.invoke(manager);
return (Boolean) method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
Ln.e("Could not invoke " + method.getName(), e);
return false;
}
}
}

View file

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

View file

@ -1,10 +1,16 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.annotation.SuppressLint;
import android.graphics.Rect;
import android.os.Build;
import android.os.IBinder;
import android.view.Surface;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@SuppressLint("PrivateApi")
public final class SurfaceControl {
@ -22,6 +28,9 @@ public final class SurfaceControl {
}
}
private static Method getBuiltInDisplayMethod;
private static Method setDisplayPowerModeMethod;
private SurfaceControl() {
// only static methods
}
@ -75,19 +84,62 @@ public final class SurfaceControl {
}
}
public static IBinder getBuiltInDisplay(int builtInDisplayId) {
private static Method getGetBuiltInDisplayMethod() {
if (getBuiltInDisplayMethod == null) {
try {
// the method signature has changed in Android Q
// <https://github.com/Genymobile/scrcpy/issues/586>
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class);
} else {
getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken");
}
} catch (NoSuchMethodException e) {
Ln.e("Could not find method", e);
}
}
return getBuiltInDisplayMethod;
}
public static IBinder getBuiltInDisplay() {
Method method = getGetBuiltInDisplayMethod();
if (method == null) {
return null;
}
try {
return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId);
} catch (Exception e) {
throw new AssertionError(e);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// call getBuiltInDisplay(0)
return (IBinder) method.invoke(null, 0);
}
// call getInternalDisplayToken()
return (IBinder) method.invoke(null);
} catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke " + method.getName(), e);
return null;
}
}
private static Method getSetDisplayPowerModeMethod() {
if (setDisplayPowerModeMethod == null) {
try {
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);
} catch (NoSuchMethodException e) {
Ln.e("Could not find method", e);
}
}
return setDisplayPowerModeMethod;
}
public static void setDisplayPowerMode(IBinder displayToken, int mode) {
Method method = getSetDisplayPowerModeMethod();
if (method == null) {
return;
}
try {
CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class).invoke(null, displayToken, mode);
} catch (Exception e) {
throw new AssertionError(e);
method.invoke(null, displayToken, mode);
} catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke " + method.getName(), e);
}
}

View file

@ -14,7 +14,7 @@ public final class WindowManager {
try {
Class<?> cls = manager.getClass();
try {
return (Integer) manager.getClass().getMethod("getRotation").invoke(manager);
return (Integer) cls.getMethod("getRotation").invoke(manager);
} catch (NoSuchMethodException e) {
// method changed since this commit:
// https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2

View file

@ -77,24 +77,36 @@ public class ControlMessageReaderTest {
}
@Test
public void testParseMouseEvent() throws IOException {
@SuppressWarnings("checkstyle:MagicNumber")
public void testParseTouchEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(ControlMessage.TYPE_INJECT_TOUCH_EVENT);
dos.writeByte(MotionEvent.ACTION_DOWN);
dos.writeLong(-42); // pointerId
dos.writeInt(100);
dos.writeInt(200);
dos.writeShort(1080);
dos.writeShort(1920);
dos.writeShort(0xffff); // pressure
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(KeyEvent.META_CTRL_ON);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(ControlMessage.TYPE_INJECT_TOUCH_EVENT, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
Assert.assertEquals(-42, event.getPointerId());
Assert.assertEquals(100, event.getPosition().getPoint().getX());
Assert.assertEquals(200, event.getPosition().getPoint().getY());
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons());
}
@Test

View file

@ -0,0 +1,35 @@
package com.genymobile.scrcpy;
import org.junit.Assert;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class DeviceMessageWriterTest {
@Test
public void testSerializeClipboard() throws IOException {
DeviceMessageWriter writer = new DeviceMessageWriter();
String text = "aéûoç";
byte[] data = text.getBytes(StandardCharsets.UTF_8);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(DeviceMessage.TYPE_CLIPBOARD);
dos.writeShort(data.length);
dos.write(data);
byte[] expected = bos.toByteArray();
DeviceMessage msg = DeviceMessage.createClipboard(text);
bos = new ByteArrayOutputStream();
writer.writeTo(msg, bos);
byte[] actual = bos.toByteArray();
Assert.assertArrayEquals(expected, actual);
}
}