diff --git a/BUILD.md b/BUILD.md index bfe9173b..8801e5fc 100644 --- a/BUILD.md +++ b/BUILD.md @@ -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 ``` diff --git a/DEVELOP.md b/DEVELOP.md index dea8137d..92c3ce87 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -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_. diff --git a/FAQ.md b/FAQ.md index 4b04d228..49382471 100644 --- a/FAQ.md +++ b/FAQ.md @@ -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 +``` diff --git a/LICENSE b/LICENSE index cea43741..3d6840b1 100644 --- a/LICENSE +++ b/LICENSE @@ -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. diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 7955c544..2b30dcb5 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -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)"; \ diff --git a/README.md b/README.md index 273d380e..677e7a1c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/app/meson.build b/app/meson.build index 02d24a34..159ae695 100644 --- a/app/meson.build +++ b/app/meson.build @@ -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' diff --git a/app/scrcpy.1 b/app/scrcpy.1 new file mode 100644 index 00000000..6cb062b5 --- /dev/null +++ b/app/scrcpy.1 @@ -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 diff --git a/app/src/buffer_util.h b/app/src/buffer_util.h index a79014b1..262df1dc 100644 --- a/app/src/buffer_util.h +++ b/app/src/buffer_util.h @@ -4,6 +4,8 @@ #include #include +#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]; diff --git a/app/src/cbuf.h b/app/src/cbuf.h index 5d9fe4ae..c18e4680 100644 --- a/app/src/cbuf.h +++ b/app/src/cbuf.h @@ -5,8 +5,10 @@ #include #include +#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) \ ({ \ diff --git a/app/src/command.c b/app/src/command.c index 4cb2e408..d914e6ab 100644 --- a/app/src/command.c +++ b/app/src/command.c @@ -5,6 +5,7 @@ #include #include +#include "config.h" #include "common.h" #include "log.h" #include "str_util.h" diff --git a/app/src/command.h b/app/src/command.h index db6358da..d119c9bb 100644 --- a/app/src/command.h +++ b/app/src/command.h @@ -33,6 +33,8 @@ #endif +#include "config.h" + # define NO_EXIT_CODE -1 enum process_result { diff --git a/app/src/common.h b/app/src/common.h index 8963f058..e5cbe953 100644 --- a/app/src/common.h +++ b/app/src/common.h @@ -3,6 +3,8 @@ #include +#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) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 9c3d9849..e042dc5a 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -1,7 +1,9 @@ #include "control_msg.h" #include +#include +#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], diff --git a/app/src/control_msg.h b/app/src/control_msg.h index e7fdfc4c..2f319d9d 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -5,6 +5,7 @@ #include #include +#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; diff --git a/app/src/controller.c b/app/src/controller.c index 4b1f4c8b..7f90d787 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -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; } } diff --git a/app/src/controller.h b/app/src/controller.h index ae13e39f..1b0d005b 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -5,6 +5,7 @@ #include #include +#include "config.h" #include "cbuf.h" #include "control_msg.h" #include "net.h" diff --git a/app/src/convert.h b/app/src/convert.h deleted file mode 100644 index 5989e163..00000000 --- a/app/src/convert.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef CONVERT_H -#define CONVERT_H - -#include -#include - -#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 diff --git a/app/src/decoder.c b/app/src/decoder.c index 8fa218f4..cad19913 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -8,8 +8,8 @@ #include #include -#include "compat.h" #include "config.h" +#include "compat.h" #include "buffer_util.h" #include "events.h" #include "lock_util.h" diff --git a/app/src/decoder.h b/app/src/decoder.h index 76fee80e..f243812c 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -4,6 +4,8 @@ #include #include +#include "config.h" + struct video_buffer; struct decoder { diff --git a/app/src/device.c b/app/src/device.c index 8027ccbb..4f50ab48 100644 --- a/app/src/device.c +++ b/app/src/device.c @@ -1,4 +1,6 @@ #include "device.h" + +#include "config.h" #include "log.h" bool diff --git a/app/src/device.h b/app/src/device.h index f3449e5e..34a5f17f 100644 --- a/app/src/device.h +++ b/app/src/device.h @@ -3,11 +3,11 @@ #include +#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 diff --git a/app/src/device_msg.c b/app/src/device_msg.c index a90d78dd..2fc90ae4 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -3,6 +3,7 @@ #include #include +#include "config.h" #include "buffer_util.h" #include "log.h" diff --git a/app/src/device_msg.h b/app/src/device_msg.h index fd4a7eb1..04723597 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -5,6 +5,8 @@ #include #include +#include "config.h" + #define DEVICE_MSG_TEXT_MAX_LENGTH 4093 #define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH) diff --git a/app/src/convert.c b/app/src/event_converter.c similarity index 66% rename from app/src/convert.c rename to app/src/event_converter.c index adf6d400..80ead615 100644 --- a/app/src/convert.c +++ b/app/src/event_converter.c @@ -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 - // - to->inject_scroll_event.hscroll = -mul * from->x; - to->inject_scroll_event.vscroll = mul * from->y; - - return true; } diff --git a/app/src/event_converter.h b/app/src/event_converter.h new file mode 100644 index 00000000..c41887e1 --- /dev/null +++ b/app/src/event_converter.h @@ -0,0 +1,29 @@ +#ifndef CONVERT_H +#define CONVERT_H + +#include +#include + +#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 diff --git a/app/src/file_handler.c b/app/src/file_handler.c index 051db897..e02ca2a9 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -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; diff --git a/app/src/file_handler.h b/app/src/file_handler.h index 22245105..4c158296 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -5,6 +5,7 @@ #include #include +#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); diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index daece470..2a9478f6 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -3,6 +3,7 @@ #include #include +#include "config.h" #include "lock_util.h" #include "log.h" diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index 6b560a35..1c56bb01 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -7,6 +7,8 @@ #include #include +#include "config.h" + struct fps_counter { SDL_Thread *thread; SDL_mutex *mutex; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index fb8ef8f0..7d333c1b 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -1,7 +1,9 @@ #include "input_manager.h" #include -#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 + // + 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'"); } } } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 83cb7405..43fc0eeb 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -3,6 +3,7 @@ #include +#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 diff --git a/app/src/lock_util.h b/app/src/lock_util.h index d1ca7336..260d2c12 100644 --- a/app/src/lock_util.h +++ b/app/src/lock_util.h @@ -4,6 +4,7 @@ #include #include +#include "config.h" #include "log.h" static inline void diff --git a/app/src/main.c b/app/src/main.c index bf3b7a50..8a835bf1 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -8,42 +8,37 @@ #define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem #include -#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 " "); + #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 diff --git a/app/src/net.c b/app/src/net.c index a0bc38f2..bf4389dd 100644 --- a/app/src/net.c +++ b/app/src/net.c @@ -2,6 +2,7 @@ #include +#include "config.h" #include "log.h" #ifdef __WINDOWS__ diff --git a/app/src/net.h b/app/src/net.h index dd82c083..ffd5dd89 100644 --- a/app/src/net.h +++ b/app/src/net.h @@ -17,6 +17,8 @@ typedef int socket_t; #endif +#include "config.h" + bool net_init(void); diff --git a/app/src/queue.h b/app/src/queue.h new file mode 100644 index 00000000..6cf7aba6 --- /dev/null +++ b/app/src/queue.h @@ -0,0 +1,77 @@ +// generic intrusive FIFO queue +#ifndef QUEUE_H +#define QUEUE_H + +#include +#include +#include + +#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 diff --git a/app/src/receiver.h b/app/src/receiver.h index c119b827..6108e545 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -5,6 +5,7 @@ #include #include +#include "config.h" #include "net.h" // receive events from the device diff --git a/app/src/recorder.c b/app/src/recorder.c index 321a17ee..f6f6fd96 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -3,8 +3,9 @@ #include #include -#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 + 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) { // 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; +} diff --git a/app/src/recorder.h b/app/src/recorder.h index 8a8e3310..4ad77197 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -3,20 +3,45 @@ #include #include +#include +#include +#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 diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 761edb69..67f1de16 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -7,6 +7,7 @@ #include #include +#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, ¶ms)) { @@ -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..."); diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index d705d2db..8723f29f 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -3,16 +3,26 @@ #include #include -#include + +#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); diff --git a/app/src/screen.c b/app/src/screen.c index 67b268c5..ab4d434e 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -3,6 +3,7 @@ #include #include +#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; } } diff --git a/app/src/screen.h b/app/src/screen.h index 5734fdc2..2346ff15 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -5,6 +5,7 @@ #include #include +#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 diff --git a/app/src/server.c b/app/src/server.c index d0599bef..b37b39d0 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -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 diff --git a/app/src/server.h b/app/src/server.h index 74a6cac8..f46ced19 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -4,6 +4,7 @@ #include #include +#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; }; diff --git a/app/src/str_util.c b/app/src/str_util.c index 7d46a1a0..15378d8a 100644 --- a/app/src/str_util.c +++ b/app/src/str_util.c @@ -10,6 +10,8 @@ #include +#include "config.h" + size_t xstrncpy(char *dest, const char *src, size_t n) { size_t i; diff --git a/app/src/str_util.h b/app/src/str_util.h index 0b7a571a..56490190 100644 --- a/app/src/str_util.h +++ b/app/src/str_util.h @@ -3,6 +3,8 @@ #include +#include "config.h" + // like strncpy, except: // - it copies at most n-1 chars // - the dest string is nul-terminated diff --git a/app/src/stream.c b/app/src/stream.c index 4f38cecf..6c3192f2 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -8,8 +8,8 @@ #include #include -#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 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); } diff --git a/app/src/stream.h b/app/src/stream.h index 1ebff1a0..cb50468e 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -3,30 +3,27 @@ #include #include +#include #include #include +#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 diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index 55aea5e8..6a3a5a47 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -7,6 +7,8 @@ #include "command.h" +#include "config.h" + #include #include #include @@ -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); diff --git a/app/src/sys/unix/net.c b/app/src/sys/unix/net.c index 199cd7c2..d940f3bb 100644 --- a/app/src/sys/unix/net.c +++ b/app/src/sys/unix/net.c @@ -2,6 +2,8 @@ #include +#include "config.h" + bool net_init(void) { // do nothing diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c index 484ce9f0..f23730a0 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/command.c @@ -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) { diff --git a/app/src/sys/win/net.c b/app/src/sys/win/net.c index dc483682..55519782 100644 --- a/app/src/sys/win/net.c +++ b/app/src/sys/win/net.c @@ -1,5 +1,6 @@ #include "net.h" +#include "config.h" #include "log.h" bool diff --git a/app/src/tiny_xpm.c b/app/src/tiny_xpm.c index 0fb410f3..5ea89078 100644 --- a/app/src/tiny_xpm.c +++ b/app/src/tiny_xpm.c @@ -5,6 +5,7 @@ #include #include +#include "config.h" #include "log.h" struct index { diff --git a/app/src/tiny_xpm.h b/app/src/tiny_xpm.h index 85dea5c2..6e6f8035 100644 --- a/app/src/tiny_xpm.h +++ b/app/src/tiny_xpm.h @@ -3,6 +3,8 @@ #include +#include "config.h" + SDL_Surface * read_xpm(char *xpm[]); diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 26a6fa1f..303b3fc2 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -4,6 +4,7 @@ #include #include +#include "config.h" #include "fps_counter.h" // forward declarations diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index c0c501f2..83ab011f 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -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(); diff --git a/app/tests/test_queue.c b/app/tests/test_queue.c new file mode 100644 index 00000000..bcbced2b --- /dev/null +++ b/app/tests/test_queue.c @@ -0,0 +1,38 @@ +#include + +#include + +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; +} diff --git a/build.gradle b/build.gradle index 1b6f5aef..b6ec625d 100644 --- a/build.gradle +++ b/build.gradle @@ -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 diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 63ee315a..798814d9 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -54,7 +54,7 @@ page at http://checkstyle.sourceforge.net/config.html --> - + @@ -99,7 +99,7 @@ page at http://checkstyle.sourceforge.net/config.html --> - + diff --git a/cross_win32.txt b/cross_win32.txt index 2db35fe0..d13af0e2 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -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' diff --git a/cross_win64.txt b/cross_win64.txt index 79181653..09f387e1 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -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' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 13372aef..5c2d1cf0 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 33997651..430dfabc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew index 9d82f789..8e25e6c1 100755 --- a/gradlew +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec99730..24467a14 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -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 diff --git a/meson.build b/meson.build index 053d8c94..ba19d7ee 100644 --- a/meson.build +++ b/meson.build @@ -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 diff --git a/meson_options.txt b/meson_options.txt index a443ccb2..4cf4a8bf 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -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') diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 04f8b779..892af6c7 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -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 diff --git a/release.sh b/release.sh index fbd1eb54..4c5afbf1 100755 --- a/release.sh +++ b/release.sh @@ -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 diff --git a/run b/run index 7abeca05..bfb499ae 100755 --- a/run +++ b/run @@ -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" "$@" diff --git a/scripts/run-scrcpy.sh b/scripts/run-scrcpy.sh index fa6d7c8f..f3130ee9 100755 --- a/scripts/run-scrcpy.sh +++ b/scripts/run-scrcpy.sh @@ -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" diff --git a/server/build.gradle b/server/build.gradle index d5c1fb00..0804a8bd 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -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 { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh new file mode 100755 index 00000000..fcd6233e --- /dev/null +++ b/server/build_without_gradle.sh @@ -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" diff --git a/server/meson.build b/server/meson.build index d96373a0..4ba481d5 100644 --- a/server/meson.build +++ b/server/meson.build @@ -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') diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 0de4bc3c..30c05a3b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -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; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 8ced049d..2f8b5177 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -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() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 263fc2fc..ce02e333 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -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) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 538135d4..708b9516 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -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")); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index af6b2ee1..5b993f30 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -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; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Pointer.java b/server/src/main/java/com/genymobile/scrcpy/Pointer.java new file mode 100644 index 00000000..b89cc256 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Pointer.java @@ -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; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/PointersState.java b/server/src/main/java/com/genymobile/scrcpy/PointersState.java new file mode 100644 index 00000000..d8daaff2 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/PointersState.java @@ -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 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); + } + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 8357b061..c9a37f84 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -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; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 1e4d10d6..ad14e5d8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -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); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java new file mode 100644 index 00000000..f45d82a4 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -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()" + // + // + // 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" + // + 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()); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index a058a8bb..27dcb443 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -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); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 1fc78c27..788a04c7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -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; } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index a730d1b1..66acdba8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -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; } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 74003b64..670de952 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -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); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index bed21b3c..bef6e5d9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -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 + // + 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); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index 56330f9d..52096461 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -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 diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index df1db1a6..ede759dc 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -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 diff --git a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java new file mode 100644 index 00000000..df12f647 --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java @@ -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); + } +}