├── .gitignore ├── BUILD.md ├── DEVELOP.md ├── FAQ.md ├── LICENSE ├── Makefile.CrossWindows ├── README.md ├── app ├── meson.build ├── src │ ├── android │ │ ├── input.h │ │ └── keycodes.h │ ├── buffer_util.h │ ├── cbuf.h │ ├── command.c │ ├── command.h │ ├── common.h │ ├── compat.h │ ├── control_msg.c │ ├── control_msg.h │ ├── controller.c │ ├── controller.h │ ├── convert.c │ ├── convert.h │ ├── decoder.c │ ├── decoder.h │ ├── device.c │ ├── device.h │ ├── device_msg.c │ ├── device_msg.h │ ├── events.h │ ├── file_handler.c │ ├── file_handler.h │ ├── fps_counter.c │ ├── fps_counter.h │ ├── icon.xpm │ ├── input_manager.c │ ├── input_manager.h │ ├── lock_util.h │ ├── log.h │ ├── main.c │ ├── net.c │ ├── net.h │ ├── queue.h │ ├── receiver.c │ ├── receiver.h │ ├── recorder.c │ ├── recorder.h │ ├── scrcpy.c │ ├── scrcpy.h │ ├── screen.c │ ├── screen.h │ ├── server.c │ ├── server.h │ ├── str_util.c │ ├── str_util.h │ ├── stream.c │ ├── stream.h │ ├── sys │ │ ├── unix │ │ │ ├── command.c │ │ │ └── net.c │ │ └── win │ │ │ ├── command.c │ │ │ └── net.c │ ├── tiny_xpm.c │ ├── tiny_xpm.h │ ├── video_buffer.c │ └── video_buffer.h └── tests │ ├── test_cbuf.c │ ├── test_control_msg_serialize.c │ ├── test_device_msg_deserialize.c │ ├── test_queue.c │ └── test_strutil.c ├── assets └── screenshot-debian-600.jpg ├── build.gradle ├── config ├── android-checkstyle.gradle └── checkstyle │ └── checkstyle.xml ├── cross_win32.txt ├── cross_win64.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── meson.build ├── meson_options.txt ├── prebuilt-deps ├── .gitignore ├── Makefile └── prepare-dep ├── release.sh ├── run ├── scripts └── run-scrcpy.sh ├── server ├── .gitignore ├── build.gradle ├── meson.build ├── proguard-rules.pro ├── scripts │ └── build-wrapper.sh └── src │ ├── main │ ├── AndroidManifest.xml │ ├── aidl │ │ └── android │ │ │ └── view │ │ │ └── IRotationWatcher.aidl │ └── java │ │ └── com │ │ └── genymobile │ │ └── scrcpy │ │ ├── ControlMessage.java │ │ ├── ControlMessageReader.java │ │ ├── Controller.java │ │ ├── DesktopConnection.java │ │ ├── Device.java │ │ ├── DeviceControl.java │ │ ├── DeviceMessage.java │ │ ├── DeviceMessageSender.java │ │ ├── DeviceMessageWriter.java │ │ ├── DisplayInfo.java │ │ ├── IME.java │ │ ├── KeyComposition.java │ │ ├── Ln.java │ │ ├── Options.java │ │ ├── Point.java │ │ ├── Position.java │ │ ├── ScreenEncoder.java │ │ ├── ScreenInfo.java │ │ ├── Server.java │ │ ├── Size.java │ │ ├── StringUtils.java │ │ └── wrappers │ │ ├── ActivityManager.java │ │ ├── ClipboardManager.java │ │ ├── DisplayManager.java │ │ ├── InputManager.java │ │ ├── InputMethodManager.java │ │ ├── PackageManager.java │ │ ├── PowerManager.java │ │ ├── ServiceManager.java │ │ ├── Settings.java │ │ ├── StatusBarManager.java │ │ ├── SurfaceControl.java │ │ └── WindowManager.java │ └── test │ └── java │ └── com │ └── genymobile │ └── scrcpy │ ├── ControlMessageReaderTest.java │ ├── DeviceMessageWriterTest.java │ └── StringUtilsTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | /dist/ 3 | .idea/ 4 | .gradle/ 5 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # Build scrcpy 2 | 3 | Here are the instructions to build _scrcpy_ (client and server). 4 | 5 | You may want to build only the client: the server binary, which will be pushed 6 | to the Android device, does not depend on your system and architecture. In that 7 | case, use the [prebuilt server] (so you will not need Java or the Android SDK). 8 | 9 | [prebuilt server]: #prebuilt-server 10 | 11 | 12 | ## Requirements 13 | 14 | You need [adb]. It is available in the [Android SDK platform 15 | tools][platform-tools], or packaged in your distribution (`adb`). 16 | 17 | On Windows, download the [platform-tools][platform-tools-windows] and extract 18 | the following files to a directory accessible from your `PATH`: 19 | - `adb.exe` 20 | - `AdbWinApi.dll` 21 | - `AdbWinUsbApi.dll` 22 | 23 | The client requires [FFmpeg] and [LibSDL2]. Just follow the instructions. 24 | 25 | [adb]: https://developer.android.com/studio/command-line/adb.html 26 | [platform-tools]: https://developer.android.com/studio/releases/platform-tools.html 27 | [platform-tools-windows]: https://dl.google.com/android/repository/platform-tools-latest-windows.zip 28 | [ffmpeg]: https://en.wikipedia.org/wiki/FFmpeg 29 | [LibSDL2]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer 30 | 31 | 32 | 33 | ## System-specific steps 34 | 35 | ### Linux 36 | 37 | Install the required packages from your package manager. 38 | 39 | #### Debian/Ubuntu 40 | 41 | ```bash 42 | # runtime dependencies 43 | sudo apt install ffmpeg libsdl2-2.0-0 44 | 45 | # client build dependencies 46 | sudo apt install make gcc git pkg-config meson ninja-build \ 47 | libavcodec-dev libavformat-dev libavutil-dev \ 48 | libsdl2-dev 49 | 50 | # server build dependencies 51 | sudo apt install openjdk-8-jdk 52 | ``` 53 | 54 | On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install 55 | it from `pip3`: 56 | 57 | ```bash 58 | sudo apt install python3-pip 59 | pip3 install meson 60 | ``` 61 | 62 | 63 | #### Fedora 64 | 65 | ```bash 66 | # enable RPM fusion free 67 | sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm 68 | 69 | # client build dependencies 70 | sudo dnf install SDL2-devel ffms2-devel meson gcc make 71 | 72 | # server build dependencies 73 | sudo dnf install java-devel 74 | ``` 75 | 76 | 77 | 78 | ### Windows 79 | 80 | #### Cross-compile from Linux 81 | 82 | This is the preferred method (and the way the release is built). 83 | 84 | From _Debian_, install _mingw_: 85 | 86 | ```bash 87 | sudo apt install mingw-w64 mingw-w64-tools 88 | ``` 89 | 90 | You also need the JDK to build the server: 91 | 92 | ```bash 93 | sudo apt install openjdk-8-jdk 94 | ``` 95 | 96 | Then generate the releases: 97 | 98 | ```bash 99 | make -f Makefile.CrossWindows 100 | ``` 101 | 102 | It will generate win32 and win64 releases into `dist/`. 103 | 104 | 105 | #### In MSYS2 106 | 107 | From Windows, you need [MSYS2] to build the project. From an MSYS2 terminal, 108 | install the required packages: 109 | 110 | [MSYS2]: http://www.msys2.org/ 111 | 112 | ```bash 113 | # runtime dependencies 114 | pacman -S mingw-w64-x86_64-SDL2 \ 115 | mingw-w64-x86_64-ffmpeg 116 | 117 | # client build dependencies 118 | pacman -S mingw-w64-x86_64-make \ 119 | mingw-w64-x86_64-gcc \ 120 | mingw-w64-x86_64-pkg-config \ 121 | mingw-w64-x86_64-meson 122 | ``` 123 | 124 | For a 32 bits version, replace `x86_64` by `i686`: 125 | 126 | ```bash 127 | # runtime dependencies 128 | pacman -S mingw-w64-i686-SDL2 \ 129 | mingw-w64-i686-ffmpeg 130 | 131 | # client build dependencies 132 | pacman -S mingw-w64-i686-make \ 133 | mingw-w64-i686-gcc \ 134 | mingw-w64-i686-pkg-config \ 135 | mingw-w64-i686-meson 136 | ``` 137 | 138 | Java (>= 7) is not available in MSYS2, so if you plan to build the server, 139 | install it manually and make it available from the `PATH`: 140 | 141 | ```bash 142 | export PATH="$JAVA_HOME/bin:$PATH" 143 | ``` 144 | 145 | ### Mac OS 146 | 147 | Install the packages with [Homebrew]: 148 | 149 | [Homebrew]: https://brew.sh/ 150 | 151 | ```bash 152 | # runtime dependencies 153 | brew install sdl2 ffmpeg 154 | 155 | # client build dependencies 156 | brew install pkg-config meson 157 | ``` 158 | 159 | Additionally, if you want to build the server, install Java 8 from Caskroom, and 160 | make it avaliable from the `PATH`: 161 | 162 | ```bash 163 | brew tap caskroom/versions 164 | brew cask install java8 165 | export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)" 166 | export PATH="$JAVA_HOME/bin:$PATH" 167 | ``` 168 | 169 | ### Docker 170 | 171 | See [pierlon/scrcpy-docker](https://github.com/pierlon/scrcpy-docker). 172 | 173 | 174 | ## Common steps 175 | 176 | If you want to build the server, install the [Android SDK] (_Android Studio_), 177 | and set `ANDROID_HOME` to its directory. For example: 178 | 179 | [Android SDK]: https://developer.android.com/studio/index.html 180 | 181 | ```bash 182 | export ANDROID_HOME=~/android/sdk 183 | ``` 184 | 185 | If you don't want to build the server, use the [prebuilt server]. 186 | 187 | Clone the project: 188 | 189 | ```bash 190 | git clone https://github.com/Genymobile/scrcpy 191 | cd scrcpy 192 | ``` 193 | 194 | Then, build: 195 | 196 | ```bash 197 | meson x --buildtype release --strip -Db_lto=true 198 | cd x 199 | ninja 200 | ``` 201 | 202 | _Note: `ninja` [must][ninja-user] be run as a non-root user (only `ninja 203 | install` must be run as root)._ 204 | 205 | [ninja-user]: https://github.com/Genymobile/scrcpy/commit/4c49b27e9f6be02b8e63b508b60535426bd0291a 206 | 207 | 208 | ### Run 209 | 210 | To run without installing: 211 | 212 | ```bash 213 | ./run x [options] 214 | ``` 215 | 216 | 217 | ### Install 218 | 219 | After a successful build, you can install _scrcpy_ on the system: 220 | 221 | ```bash 222 | sudo ninja install # without sudo on Windows 223 | ``` 224 | 225 | This installs two files: 226 | 227 | - `/usr/local/bin/scrcpy` 228 | - `/usr/local/share/scrcpy/scrcpy-server.jar` 229 | 230 | Just remove them to "uninstall" the application. 231 | 232 | You can then [run](README.md#run) _scrcpy_. 233 | 234 | 235 | ## Prebuilt server 236 | 237 | - [`scrcpy-server-v1.10.jar`][direct-scrcpy-server] 238 | _(SHA-256: cbeb1a4e046f1392c1dc73c3ccffd7f86dec4636b505556ea20929687a119390)_ 239 | 240 | [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-server-v1.10.jar 241 | 242 | Download the prebuilt server somewhere, and specify its path during the Meson 243 | configuration: 244 | 245 | ```bash 246 | meson x --buildtype release --strip -Db_lto=true \ 247 | -Dprebuilt_server=/path/to/scrcpy-server.jar 248 | cd x 249 | ninja 250 | sudo ninja install 251 | ``` 252 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | Here are the common reported problems and their status. 4 | 5 | 6 | ### On Windows, my device is not detected 7 | 8 | The most common is your device not being detected by `adb`, or is unauthorized. 9 | Check everything is ok by calling: 10 | 11 | adb devices 12 | 13 | Windows may need some [drivers] to detect your device. 14 | 15 | [drivers]: https://developer.android.com/studio/run/oem-usb.html 16 | 17 | 18 | ### I can only mirror, I cannot interact with the device 19 | 20 | On some devices, you may need to enable an option to allow [simulating input]. 21 | In developer options, enable: 22 | 23 | > **USB debugging (Security settings)** 24 | > _Allow granting permissions and simulating input via USB debugging_ 25 | 26 | [simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 27 | 28 | 29 | ### Mouse clicks at wrong location 30 | 31 | On MacOS, with HiDPI support and multiple screens, input location are wrongly 32 | scaled. See [issue 15]. 33 | 34 | [issue 15]: https://github.com/Genymobile/scrcpy/issues/15 35 | 36 | A workaround is to build with HiDPI support disabled: 37 | 38 | ```bash 39 | meson x --buildtype release -Dhidpi_support=false 40 | ``` 41 | 42 | However, the video will be displayed at lower resolution. 43 | 44 | 45 | ### The quality is low on HiDPI display 46 | 47 | On Windows, you may need to configure the [scaling behavior]. 48 | 49 | > `scrcpy.exe` > Properties > Compatibility > Change high DPI settings > 50 | > Override high DPI scaling behavior > Scaling performed by: _Application_. 51 | 52 | [scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 53 | 54 | 55 | ### KWin compositor crashes 56 | 57 | On Plasma Desktop, compositor is disabled while _scrcpy_ is running. 58 | 59 | As a workaround, [disable "Block compositing"][kwin]. 60 | 61 | [kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 62 | 63 | 64 | ### I get an error "Could not open video stream" 65 | 66 | There may be many reasons. One common cause is that the hardware encoder of your 67 | device is not able to encode at the given definition: 68 | 69 | ``` 70 | ERROR: Exception on thread Thread[main,5,main] 71 | android.media.MediaCodec$CodecException: Error 0xfffffc0e 72 | ... 73 | Exit due to uncaughtException in main thread: 74 | ERROR: Could not open video stream 75 | INFO: Initial texture: 1080x2336 76 | ``` 77 | 78 | Just try with a lower definition: 79 | 80 | ``` 81 | scrcpy -m 1920 82 | scrcpy -m 1024 83 | scrcpy -m 800 84 | ``` 85 | -------------------------------------------------------------------------------- /Makefile.CrossWindows: -------------------------------------------------------------------------------- 1 | # This makefile provides recipes to build a "portable" version of scrcpy for 2 | # Windows. 3 | # 4 | # Here, "portable" means that the client and server binaries are expected to be 5 | # anywhere, but in the same directory, instead of well-defined separate 6 | # locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server.jar). 7 | # 8 | # In particular, this implies to change the location from where the client push 9 | # the server to the device. 10 | 11 | .PHONY: default clean \ 12 | build-server \ 13 | prepare-deps-win32 prepare-deps-win64 \ 14 | build-win32 build-win32-noconsole \ 15 | build-win64 build-win64-noconsole \ 16 | dist-win32 dist-win64 \ 17 | zip-win32 zip-win64 \ 18 | sums release 19 | 20 | GRADLE ?= ./gradlew 21 | 22 | SERVER_BUILD_DIR := build-server 23 | WIN32_BUILD_DIR := build-win32 24 | WIN32_NOCONSOLE_BUILD_DIR := build-win32-noconsole 25 | WIN64_BUILD_DIR := build-win64 26 | WIN64_NOCONSOLE_BUILD_DIR := build-win64-noconsole 27 | 28 | DIST := dist 29 | WIN32_TARGET_DIR := scrcpy-win32 30 | WIN64_TARGET_DIR := scrcpy-win64 31 | 32 | VERSION := $(shell git describe --tags --always) 33 | WIN32_TARGET := $(WIN32_TARGET_DIR)-$(VERSION).zip 34 | WIN64_TARGET := $(WIN64_TARGET_DIR)-$(VERSION).zip 35 | 36 | release: clean zip-win32 zip-win64 sums 37 | @echo "Windows archives generated in $(DIST)/" 38 | 39 | clean: 40 | $(GRADLE) clean 41 | rm -rf "$(SERVER_BUILD_DIR)" "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" \ 42 | "$(WIN32_NOCONSOLE_BUILD_DIR)" "$(WIN64_NOCONSOLE_BUILD_DIR)" "$(DIST)" 43 | 44 | build-server: 45 | [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ 46 | meson "$(SERVER_BUILD_DIR)" \ 47 | --buildtype release -Dcompile_app=false ) 48 | ninja -C "$(SERVER_BUILD_DIR)" 49 | 50 | prepare-deps-win32: 51 | -$(MAKE) -C prebuilt-deps prepare-win32 52 | 53 | build-win32: prepare-deps-win32 54 | [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ 55 | meson "$(WIN32_BUILD_DIR)" \ 56 | --cross-file cross_win32.txt \ 57 | --buildtype release --strip -Db_lto=true \ 58 | -Dcrossbuild_windows=true \ 59 | -Dcompile_server=false \ 60 | -Dportable=true ) 61 | ninja -C "$(WIN32_BUILD_DIR)" 62 | 63 | build-win32-noconsole: prepare-deps-win32 64 | [ -d "$(WIN32_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN32_NOCONSOLE_BUILD_DIR)" && \ 65 | meson "$(WIN32_NOCONSOLE_BUILD_DIR)" \ 66 | --cross-file cross_win32.txt \ 67 | --buildtype release --strip -Db_lto=true \ 68 | -Dcrossbuild_windows=true \ 69 | -Dcompile_server=false \ 70 | -Dwindows_noconsole=true \ 71 | -Dportable=true ) 72 | ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)" 73 | 74 | prepare-deps-win64: 75 | -$(MAKE) -C prebuilt-deps prepare-win64 76 | 77 | build-win64: prepare-deps-win64 78 | [ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \ 79 | meson "$(WIN64_BUILD_DIR)" \ 80 | --cross-file cross_win64.txt \ 81 | --buildtype release --strip -Db_lto=true \ 82 | -Dcrossbuild_windows=true \ 83 | -Dcompile_server=false \ 84 | -Dportable=true ) 85 | ninja -C "$(WIN64_BUILD_DIR)" 86 | 87 | build-win64-noconsole: prepare-deps-win64 88 | [ -d "$(WIN64_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN64_NOCONSOLE_BUILD_DIR)" && \ 89 | meson "$(WIN64_NOCONSOLE_BUILD_DIR)" \ 90 | --cross-file cross_win64.txt \ 91 | --buildtype release --strip -Db_lto=true \ 92 | -Dcrossbuild_windows=true \ 93 | -Dcompile_server=false \ 94 | -Dwindows_noconsole=true \ 95 | -Dportable=true ) 96 | ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)" 97 | 98 | dist-win32: build-server build-win32 build-win32-noconsole 99 | mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" 100 | cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/" 101 | cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" 102 | cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" 103 | cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" 104 | cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" 105 | cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" 106 | cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" 107 | cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" 108 | cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" 109 | cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" 110 | cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" 111 | cp prebuilt-deps/SDL2-2.0.10/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" 112 | 113 | dist-win64: build-server build-win64 build-win64-noconsole 114 | mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" 115 | cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN64_TARGET_DIR)/" 116 | cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" 117 | cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" 118 | cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" 119 | cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" 120 | cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" 121 | cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" 122 | cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" 123 | cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" 124 | cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" 125 | cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" 126 | cp prebuilt-deps/SDL2-2.0.10/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" 127 | 128 | zip-win32: dist-win32 129 | cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ 130 | zip -r "../$(WIN32_TARGET)" . 131 | 132 | zip-win64: dist-win64 133 | cd "$(DIST)/$(WIN64_TARGET_DIR)"; \ 134 | zip -r "../$(WIN64_TARGET)" . 135 | 136 | sums: 137 | cd "$(DIST)"; \ 138 | sha256sum *.zip > SHA256SUMS.txt 139 | -------------------------------------------------------------------------------- /app/meson.build: -------------------------------------------------------------------------------- 1 | src = [ 2 | 'src/main.c', 3 | 'src/command.c', 4 | 'src/control_msg.c', 5 | 'src/controller.c', 6 | 'src/convert.c', 7 | 'src/decoder.c', 8 | 'src/device.c', 9 | 'src/device_msg.c', 10 | 'src/file_handler.c', 11 | 'src/fps_counter.c', 12 | 'src/input_manager.c', 13 | 'src/net.c', 14 | 'src/receiver.c', 15 | 'src/recorder.c', 16 | 'src/scrcpy.c', 17 | 'src/screen.c', 18 | 'src/server.c', 19 | 'src/str_util.c', 20 | 'src/tiny_xpm.c', 21 | 'src/stream.c', 22 | 'src/video_buffer.c', 23 | ] 24 | 25 | if not get_option('crossbuild_windows') 26 | 27 | # native build 28 | dependencies = [ 29 | dependency('libavformat'), 30 | dependency('libavcodec'), 31 | dependency('libavutil'), 32 | dependency('sdl2'), 33 | ] 34 | 35 | else 36 | 37 | # cross-compile mingw32 build (from Linux to Windows) 38 | cc = meson.get_compiler('c') 39 | 40 | prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2') 41 | sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/bin' 42 | sdl2_lib_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/lib' 43 | sdl2_include_dir = '../prebuilt-deps/' + prebuilt_sdl2 + '/include' 44 | 45 | sdl2 = declare_dependency( 46 | dependencies: [ 47 | cc.find_library('SDL2', dirs: sdl2_bin_dir), 48 | cc.find_library('SDL2main', dirs: sdl2_lib_dir), 49 | ], 50 | include_directories: include_directories(sdl2_include_dir) 51 | ) 52 | 53 | prebuilt_ffmpeg_shared = meson.get_cross_property('prebuilt_ffmpeg_shared') 54 | prebuilt_ffmpeg_dev = meson.get_cross_property('prebuilt_ffmpeg_dev') 55 | ffmpeg_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_ffmpeg_shared + '/bin' 56 | ffmpeg_include_dir = '../prebuilt-deps/' + prebuilt_ffmpeg_dev + '/include' 57 | ffmpeg = declare_dependency( 58 | dependencies: [ 59 | cc.find_library('avcodec-58', dirs: ffmpeg_bin_dir), 60 | cc.find_library('avformat-58', dirs: ffmpeg_bin_dir), 61 | cc.find_library('avutil-56', dirs: ffmpeg_bin_dir), 62 | ], 63 | include_directories: include_directories(ffmpeg_include_dir) 64 | ) 65 | 66 | dependencies = [ 67 | ffmpeg, 68 | sdl2, 69 | cc.find_library('mingw32') 70 | ] 71 | 72 | endif 73 | 74 | cc = meson.get_compiler('c') 75 | 76 | if host_machine.system() == 'windows' 77 | src += [ 'src/sys/win/command.c' ] 78 | src += [ 'src/sys/win/net.c' ] 79 | dependencies += cc.find_library('ws2_32') 80 | else 81 | src += [ 'src/sys/unix/command.c' ] 82 | src += [ 'src/sys/unix/net.c' ] 83 | endif 84 | 85 | conf = configuration_data() 86 | 87 | # expose the build type 88 | conf.set('BUILD_DEBUG', get_option('buildtype') == 'debug') 89 | 90 | # the version, updated on release 91 | conf.set_quoted('SCRCPY_VERSION', meson.project_version()) 92 | 93 | # the prefix used during configuration (meson --prefix=PREFIX) 94 | conf.set_quoted('PREFIX', get_option('prefix')) 95 | 96 | # build a "portable" version (with scrcpy-server.jar accessible from the same 97 | # directory as the executable) 98 | conf.set('PORTABLE', get_option('portable')) 99 | 100 | # the default client TCP port for the "adb reverse" tunnel 101 | # overridden by option --port 102 | conf.set('DEFAULT_LOCAL_PORT', '27183') 103 | 104 | # the default max video size for both dimensions, in pixels 105 | # overridden by option --max-size 106 | conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited 107 | 108 | # the default video bitrate, in bits/second 109 | # overridden by option --bit-rate 110 | conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps 111 | 112 | # enable High DPI support 113 | conf.set('HIDPI_SUPPORT', get_option('hidpi_support')) 114 | 115 | # disable console on Windows 116 | conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole')) 117 | 118 | configure_file(configuration: conf, output: 'config.h') 119 | 120 | src_dir = include_directories('src') 121 | 122 | if get_option('windows_noconsole') 123 | c_args = [ '-mwindows' ] 124 | link_args = [ '-mwindows' ] 125 | else 126 | c_args = [] 127 | link_args = [] 128 | endif 129 | 130 | executable('scrcpy', src, 131 | dependencies: dependencies, 132 | include_directories: src_dir, 133 | install: true, 134 | c_args: c_args, 135 | link_args: link_args) 136 | 137 | 138 | ### TESTS 139 | 140 | tests = [ 141 | ['test_cbuf', [ 142 | 'tests/test_cbuf.c', 143 | ]], 144 | ['test_control_event_serialize', [ 145 | 'tests/test_control_msg_serialize.c', 146 | 'src/control_msg.c', 147 | 'src/str_util.c' 148 | ]], 149 | ['test_device_event_deserialize', [ 150 | 'tests/test_device_msg_deserialize.c', 151 | 'src/device_msg.c' 152 | ]], 153 | ['test_queue', [ 154 | 'tests/test_queue.c', 155 | ]], 156 | ['test_strutil', [ 157 | 'tests/test_strutil.c', 158 | 'src/str_util.c' 159 | ]], 160 | ] 161 | 162 | foreach t : tests 163 | exe = executable(t[0], t[1], 164 | include_directories: src_dir, 165 | dependencies: dependencies) 166 | test(t[0], exe) 167 | endforeach 168 | -------------------------------------------------------------------------------- /app/src/buffer_util.h: -------------------------------------------------------------------------------- 1 | #ifndef BUFFER_UTIL_H 2 | #define BUFFER_UTIL_H 3 | 4 | #include 5 | #include 6 | 7 | static inline void 8 | buffer_write16be(uint8_t *buf, uint16_t value) { 9 | buf[0] = value >> 8; 10 | buf[1] = value; 11 | } 12 | 13 | static inline void 14 | buffer_write32be(uint8_t *buf, uint32_t value) { 15 | buf[0] = value >> 24; 16 | buf[1] = value >> 16; 17 | buf[2] = value >> 8; 18 | buf[3] = value; 19 | } 20 | 21 | static inline uint16_t 22 | buffer_read16be(const uint8_t *buf) { 23 | return (buf[0] << 8) | buf[1]; 24 | } 25 | 26 | static inline uint32_t 27 | buffer_read32be(const uint8_t *buf) { 28 | return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; 29 | } 30 | 31 | static inline 32 | uint64_t buffer_read64be(const uint8_t *buf) { 33 | uint32_t msb = buffer_read32be(buf); 34 | uint32_t lsb = buffer_read32be(&buf[4]); 35 | return ((uint64_t) msb << 32) | lsb; 36 | } 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /app/src/cbuf.h: -------------------------------------------------------------------------------- 1 | // generic circular buffer (bounded queue) implementation 2 | #ifndef CBUF_H 3 | #define CBUF_H 4 | 5 | #include 6 | #include 7 | 8 | // To define a circular buffer type of 20 ints: 9 | // struct cbuf_int CBUF(int, 20); 10 | // 11 | // data has length CAP + 1 to distinguish empty vs full. 12 | #define CBUF(TYPE, CAP) { \ 13 | TYPE data[(CAP) + 1]; \ 14 | size_t head; \ 15 | size_t tail; \ 16 | } 17 | 18 | #define cbuf_size_(PCBUF) \ 19 | (sizeof((PCBUF)->data) / sizeof(*(PCBUF)->data)) 20 | 21 | #define cbuf_is_empty(PCBUF) \ 22 | ((PCBUF)->head == (PCBUF)->tail) 23 | 24 | #define cbuf_is_full(PCBUF) \ 25 | (((PCBUF)->head + 1) % cbuf_size_(PCBUF) == (PCBUF)->tail) 26 | 27 | #define cbuf_init(PCBUF) \ 28 | (void) ((PCBUF)->head = (PCBUF)->tail = 0) 29 | 30 | #define cbuf_push(PCBUF, ITEM) \ 31 | ({ \ 32 | bool ok = !cbuf_is_full(PCBUF); \ 33 | if (ok) { \ 34 | (PCBUF)->data[(PCBUF)->head] = (ITEM); \ 35 | (PCBUF)->head = ((PCBUF)->head + 1) % cbuf_size_(PCBUF); \ 36 | } \ 37 | ok; \ 38 | }) 39 | 40 | #define cbuf_take(PCBUF, PITEM) \ 41 | ({ \ 42 | bool ok = !cbuf_is_empty(PCBUF); \ 43 | if (ok) { \ 44 | *(PITEM) = (PCBUF)->data[(PCBUF)->tail]; \ 45 | (PCBUF)->tail = ((PCBUF)->tail + 1) % cbuf_size_(PCBUF); \ 46 | } \ 47 | ok; \ 48 | }) 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /app/src/command.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMAND_H 2 | #define COMMAND_H 3 | 4 | #include 5 | #include 6 | 7 | #ifdef _WIN32 8 | 9 | // not needed here, but winsock2.h must never be included AFTER windows.h 10 | # include 11 | # include 12 | # define PATH_SEPARATOR '\\' 13 | # define PRIexitcode "lu" 14 | // 15 | # ifdef _WIN64 16 | # define PRIsizet PRIu64 17 | # else 18 | # define PRIsizet PRIu32 19 | # endif 20 | # define PROCESS_NONE NULL 21 | typedef HANDLE process_t; 22 | typedef DWORD exit_code_t; 23 | 24 | #else 25 | 26 | # include 27 | # define PATH_SEPARATOR '/' 28 | # define PRIsizet "zu" 29 | # define PRIexitcode "d" 30 | # define PROCESS_NONE -1 31 | typedef pid_t process_t; 32 | typedef int exit_code_t; 33 | 34 | #endif 35 | 36 | # define NO_EXIT_CODE -1 37 | 38 | enum process_result { 39 | PROCESS_SUCCESS, 40 | PROCESS_ERROR_GENERIC, 41 | PROCESS_ERROR_MISSING_BINARY, 42 | }; 43 | 44 | enum process_result 45 | cmd_execute(const char *path, const char *const argv[], process_t *process); 46 | 47 | bool 48 | cmd_terminate(process_t pid); 49 | 50 | bool 51 | cmd_simple_wait(process_t pid, exit_code_t *exit_code); 52 | 53 | process_t 54 | adb_execute(const char *serial, const char *const adb_cmd[], size_t len); 55 | 56 | process_t 57 | adb_forward(const char *serial, uint16_t local_port, 58 | const char *device_socket_name); 59 | 60 | process_t 61 | adb_forward_remove(const char *serial, uint16_t local_port); 62 | 63 | process_t 64 | adb_reverse(const char *serial, const char *device_socket_name, 65 | uint16_t local_port); 66 | 67 | process_t 68 | adb_reverse_remove(const char *serial, const char *device_socket_name); 69 | 70 | bool 71 | adb_connect(const char *serial); 72 | 73 | bool 74 | adb_disconnect(const char *serial); 75 | 76 | process_t 77 | adb_push(const char *serial, const char *local, const char *remote); 78 | 79 | process_t 80 | adb_install(const char *serial, const char *local); 81 | 82 | // convenience function to wait for a successful process execution 83 | // automatically log process errors with the provided process name 84 | bool 85 | process_check_success(process_t proc, const char *name); 86 | 87 | // return the absolute path of the executable (the scrcpy binary) 88 | // may be NULL on error; to be freed by SDL_free 89 | char * 90 | get_executable_path(void); 91 | 92 | #endif 93 | -------------------------------------------------------------------------------- /app/src/common.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMON_H 2 | #define COMMON_H 3 | 4 | #include 5 | 6 | #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) 7 | #define MIN(X,Y) (X) < (Y) ? (X) : (Y) 8 | #define MAX(X,Y) (X) > (Y) ? (X) : (Y) 9 | 10 | struct size { 11 | uint16_t width; 12 | uint16_t height; 13 | }; 14 | 15 | struct point { 16 | int32_t x; 17 | int32_t y; 18 | }; 19 | 20 | struct position { 21 | // The video screen size may be different from the real device screen size, 22 | // so store to which size the absolute position apply, to scale it 23 | // accordingly. 24 | struct size screen_size; 25 | struct point point; 26 | }; 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /app/src/compat.h: -------------------------------------------------------------------------------- 1 | #ifndef COMPAT_H 2 | #define COMPAT_H 3 | 4 | #include 5 | #include 6 | 7 | // In ffmpeg/doc/APIchanges: 8 | // 2016-04-11 - 6f69f7a / 9200514 - lavf 57.33.100 / 57.5.0 - avformat.h 9 | // Add AVStream.codecpar, deprecate AVStream.codec. 10 | #if (LIBAVFORMAT_VERSION_MICRO >= 100 /* FFmpeg */ && \ 11 | LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 33, 100)) \ 12 | || (LIBAVFORMAT_VERSION_MICRO < 100 && /* Libav */ \ 13 | LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 5, 0)) 14 | # define SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API 15 | #endif 16 | 17 | // In ffmpeg/doc/APIchanges: 18 | // 2018-02-06 - 0694d87024 - lavf 58.9.100 - avformat.h 19 | // Deprecate use of av_register_input_format(), av_register_output_format(), 20 | // av_register_all(), av_iformat_next(), av_oformat_next(). 21 | // Add av_demuxer_iterate(), and av_muxer_iterate(). 22 | #if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100) 23 | # define SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API 24 | #else 25 | # define SCRCPY_LAVF_REQUIRES_REGISTER_ALL 26 | #endif 27 | 28 | // In ffmpeg/doc/APIchanges: 29 | // 2016-04-21 - 7fc329e - lavc 57.37.100 - avcodec.h 30 | // Add a new audio/video encoding and decoding API with decoupled input 31 | // and output -- avcodec_send_packet(), avcodec_receive_frame(), 32 | // avcodec_send_frame() and avcodec_receive_packet(). 33 | #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 100) 34 | # define SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API 35 | #endif 36 | 37 | #if SDL_VERSION_ATLEAST(2, 0, 5) 38 | // 39 | # define SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH 40 | // 41 | # define SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS 42 | // 43 | # define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP 44 | #endif 45 | 46 | #if SDL_VERSION_ATLEAST(2, 0, 8) 47 | // 48 | # define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR 49 | #endif 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /app/src/control_msg.c: -------------------------------------------------------------------------------- 1 | #include "control_msg.h" 2 | 3 | #include 4 | 5 | #include "buffer_util.h" 6 | #include "log.h" 7 | #include "str_util.h" 8 | 9 | static void 10 | write_position(uint8_t *buf, const struct position *position) { 11 | buffer_write32be(&buf[0], position->point.x); 12 | buffer_write32be(&buf[4], position->point.y); 13 | buffer_write16be(&buf[8], position->screen_size.width); 14 | buffer_write16be(&buf[10], position->screen_size.height); 15 | } 16 | 17 | // write length (2 bytes) + string (non nul-terminated) 18 | static size_t 19 | write_string(const char *utf8, size_t max_len, unsigned char *buf) { 20 | size_t len = utf8_truncation_index(utf8, max_len); 21 | buffer_write16be(buf, (uint16_t) len); 22 | memcpy(&buf[2], utf8, len); 23 | return 2 + len; 24 | } 25 | 26 | size_t 27 | control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { 28 | buf[0] = msg->type; 29 | switch (msg->type) { 30 | case CONTROL_MSG_TYPE_INJECT_KEYCODE: 31 | buf[1] = msg->inject_keycode.action; 32 | buffer_write32be(&buf[2], msg->inject_keycode.keycode); 33 | buffer_write32be(&buf[6], msg->inject_keycode.metastate); 34 | return 10; 35 | case CONTROL_MSG_TYPE_INJECT_TEXT: { 36 | size_t len = write_string(msg->inject_text.text, 37 | CONTROL_MSG_TEXT_MAX_LENGTH, &buf[1]); 38 | return 1 + len; 39 | } 40 | case CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT: 41 | buf[1] = msg->inject_mouse_event.action; 42 | buffer_write32be(&buf[2], msg->inject_mouse_event.buttons); 43 | write_position(&buf[6], &msg->inject_mouse_event.position); 44 | buffer_write32be(&buf[18], msg->timestamp); 45 | return 22; 46 | case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: 47 | buf[1] = msg->inject_touch_event.action; 48 | buffer_write32be(&buf[2], msg->inject_touch_event.touch_id); 49 | write_position(&buf[6], &msg->inject_touch_event.position); 50 | buffer_write32be(&buf[18], msg->timestamp); 51 | return 22; 52 | case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: 53 | write_position(&buf[1], &msg->inject_scroll_event.position); 54 | buffer_write32be(&buf[13], 55 | (uint32_t) msg->inject_scroll_event.hscroll); 56 | buffer_write32be(&buf[17], 57 | (uint32_t) msg->inject_scroll_event.vscroll); 58 | buffer_write32be(&buf[21], msg->timestamp); 59 | return 25; 60 | case CONTROL_MSG_TYPE_COMMAND: 61 | buf[1] = msg->command_event.action; 62 | buffer_write32be(&buf[2], msg->timestamp); 63 | return 6; 64 | case CONTROL_MSG_TYPE_SET_CLIPBOARD: { 65 | size_t len = write_string(msg->inject_text.text, 66 | CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, 67 | &buf[1]); 68 | return 1 + len; 69 | } 70 | case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: 71 | buf[1] = msg->set_screen_power_mode.mode; 72 | return 2; 73 | default: 74 | LOGW("Unknown message type: %u", (unsigned) msg->type); 75 | return 0; 76 | } 77 | } 78 | 79 | void 80 | control_msg_destroy(struct control_msg *msg) { 81 | switch (msg->type) { 82 | case CONTROL_MSG_TYPE_INJECT_TEXT: 83 | SDL_free(msg->inject_text.text); 84 | break; 85 | case CONTROL_MSG_TYPE_SET_CLIPBOARD: 86 | SDL_free(msg->set_clipboard.text); 87 | break; 88 | default: 89 | // do nothing 90 | break; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/control_msg.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTROLMSG_H 2 | #define CONTROLMSG_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "android/input.h" 9 | #include "android/keycodes.h" 10 | #include "common.h" 11 | 12 | #define CONTROL_MSG_TEXT_MAX_LENGTH 300 13 | #define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093 14 | #define CONTROL_MSG_SERIALIZED_MAX_SIZE \ 15 | (3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) 16 | 17 | enum control_msg_type { 18 | CONTROL_MSG_TYPE_INJECT_KEYCODE = 0, 19 | CONTROL_MSG_TYPE_INJECT_TEXT = 1, 20 | CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT = 2, 21 | CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT = 3, 22 | CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT = 4, 23 | CONTROL_MSG_TYPE_COMMAND = 5, 24 | CONTROL_MSG_TYPE_SET_CLIPBOARD = 6, 25 | CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE = 7, 26 | }; 27 | 28 | enum control_command { 29 | CONTROL_COMMAND_BACK_OR_SCREEN_ON = 0, 30 | CONTROL_COMMAND_EXPAND_NOTIFICATION_PANEL = 1, 31 | CONTROL_COMMAND_COLLAPSE_NOTIFICATION_PANEL = 2, 32 | CONTROL_COMMAND_QUIT = 3, 33 | CONTROL_COMMAND_PORTRAIT = 4, 34 | CONTROL_COMMAND_LANDSCAPE = 5, 35 | CONTROL_COMMAND_PING = 6, 36 | CONTROL_COMMAND_GET_CLIPBOARD = 7, 37 | }; 38 | 39 | enum screen_power_mode { 40 | // see 41 | SCREEN_POWER_MODE_OFF = 0, 42 | SCREEN_POWER_MODE_NORMAL = 2, 43 | }; 44 | 45 | struct control_msg { 46 | enum control_msg_type type; 47 | uint32_t timestamp; 48 | union { 49 | struct { 50 | enum android_keyevent_action action; 51 | enum android_keycode keycode; 52 | enum android_metastate metastate; 53 | } inject_keycode; 54 | struct { 55 | char *text; // owned, to be freed by SDL_free() 56 | } inject_text; 57 | struct { 58 | enum android_motionevent_action action; 59 | enum android_motionevent_buttons buttons; 60 | struct position position; 61 | } inject_mouse_event; 62 | struct { 63 | enum android_motionevent_action action; 64 | int32_t touch_id; 65 | struct position position; 66 | } inject_touch_event; 67 | struct { 68 | struct position position; 69 | int32_t hscroll; 70 | int32_t vscroll; 71 | } inject_scroll_event; 72 | struct { 73 | char *text; // owned, to be freed by SDL_free() 74 | } set_clipboard; 75 | struct { 76 | enum screen_power_mode mode; 77 | } set_screen_power_mode; 78 | struct { 79 | enum control_command action; 80 | } command_event; 81 | }; 82 | }; 83 | 84 | // buf size must be at least CONTROL_MSG_SERIALIZED_MAX_SIZE 85 | // return the number of bytes written 86 | size_t 87 | control_msg_serialize(const struct control_msg *msg, unsigned char *buf); 88 | 89 | void 90 | control_msg_destroy(struct control_msg *msg); 91 | 92 | #endif 93 | -------------------------------------------------------------------------------- /app/src/controller.c: -------------------------------------------------------------------------------- 1 | #include "controller.h" 2 | 3 | #include 4 | 5 | #include "config.h" 6 | #include "lock_util.h" 7 | #include "log.h" 8 | 9 | bool 10 | controller_init(struct controller *controller, socket_t control_socket) { 11 | cbuf_init(&controller->queue); 12 | 13 | if (!receiver_init(&controller->receiver, control_socket)) { 14 | return false; 15 | } 16 | 17 | if (!(controller->mutex = SDL_CreateMutex())) { 18 | receiver_destroy(&controller->receiver); 19 | return false; 20 | } 21 | 22 | if (!(controller->msg_cond = SDL_CreateCond())) { 23 | receiver_destroy(&controller->receiver); 24 | SDL_DestroyMutex(controller->mutex); 25 | return false; 26 | } 27 | 28 | controller->control_socket = control_socket; 29 | controller->stopped = false; 30 | 31 | return true; 32 | } 33 | 34 | void 35 | controller_destroy(struct controller *controller) { 36 | SDL_DestroyCond(controller->msg_cond); 37 | SDL_DestroyMutex(controller->mutex); 38 | 39 | struct control_msg msg; 40 | while (cbuf_take(&controller->queue, &msg)) { 41 | control_msg_destroy(&msg); 42 | } 43 | 44 | receiver_destroy(&controller->receiver); 45 | } 46 | 47 | bool 48 | controller_push_msg(struct controller *controller, 49 | const struct control_msg *msg) { 50 | mutex_lock(controller->mutex); 51 | bool was_empty = cbuf_is_empty(&controller->queue); 52 | bool res = cbuf_push(&controller->queue, *msg); 53 | if (was_empty) { 54 | cond_signal(controller->msg_cond); 55 | } 56 | mutex_unlock(controller->mutex); 57 | return res; 58 | } 59 | 60 | static bool 61 | process_msg(struct controller *controller, 62 | const struct control_msg *msg) { 63 | unsigned char serialized_msg[CONTROL_MSG_SERIALIZED_MAX_SIZE]; 64 | int length = control_msg_serialize(msg, serialized_msg); 65 | if (!length) { 66 | return false; 67 | } 68 | int w = net_send_all(controller->control_socket, serialized_msg, length); 69 | return w == length; 70 | } 71 | 72 | static int 73 | run_controller(void *data) { 74 | struct controller *controller = data; 75 | 76 | for (;;) { 77 | mutex_lock(controller->mutex); 78 | while (!controller->stopped && cbuf_is_empty(&controller->queue)) { 79 | cond_wait(controller->msg_cond, controller->mutex); 80 | } 81 | if (controller->stopped) { 82 | // stop immediately, do not process further msgs 83 | mutex_unlock(controller->mutex); 84 | break; 85 | } 86 | struct control_msg msg; 87 | bool non_empty = cbuf_take(&controller->queue, &msg); 88 | SDL_assert(non_empty); 89 | mutex_unlock(controller->mutex); 90 | 91 | bool ok = process_msg(controller, &msg); 92 | control_msg_destroy(&msg); 93 | if (!ok) { 94 | LOGD("Could not write msg to socket"); 95 | break; 96 | } 97 | } 98 | return 0; 99 | } 100 | 101 | bool 102 | controller_start(struct controller *controller) { 103 | LOGD("Starting controller thread"); 104 | 105 | controller->thread = SDL_CreateThread(run_controller, "controller", 106 | controller); 107 | if (!controller->thread) { 108 | LOGC("Could not start controller thread"); 109 | return false; 110 | } 111 | 112 | if (!receiver_start(&controller->receiver)) { 113 | controller_stop(controller); 114 | SDL_WaitThread(controller->thread, NULL); 115 | return false; 116 | } 117 | 118 | return true; 119 | } 120 | 121 | void 122 | controller_stop(struct controller *controller) { 123 | mutex_lock(controller->mutex); 124 | controller->stopped = true; 125 | cond_signal(controller->msg_cond); 126 | mutex_unlock(controller->mutex); 127 | } 128 | 129 | void 130 | controller_join(struct controller *controller) { 131 | SDL_WaitThread(controller->thread, NULL); 132 | receiver_join(&controller->receiver); 133 | } 134 | -------------------------------------------------------------------------------- /app/src/controller.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTROLLER_H 2 | #define CONTROLLER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "cbuf.h" 9 | #include "control_msg.h" 10 | #include "net.h" 11 | #include "receiver.h" 12 | 13 | struct control_msg_queue CBUF(struct control_msg, 64); 14 | 15 | struct controller { 16 | socket_t control_socket; 17 | SDL_Thread *thread; 18 | SDL_mutex *mutex; 19 | SDL_cond *msg_cond; 20 | bool stopped; 21 | struct control_msg_queue queue; 22 | struct receiver receiver; 23 | uint32_t reference_timestamp; 24 | }; 25 | 26 | bool 27 | controller_init(struct controller *controller, socket_t control_socket); 28 | 29 | void 30 | controller_destroy(struct controller *controller); 31 | 32 | bool 33 | controller_start(struct controller *controller); 34 | 35 | void 36 | controller_stop(struct controller *controller); 37 | 38 | void 39 | controller_join(struct controller *controller); 40 | 41 | bool 42 | controller_push_msg(struct controller *controller, 43 | const struct control_msg *msg); 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /app/src/convert.h: -------------------------------------------------------------------------------- 1 | #ifndef CONVERT_H 2 | #define CONVERT_H 3 | 4 | #include 5 | #include 6 | 7 | #include "control_msg.h" 8 | 9 | struct complete_mouse_motion_event { 10 | SDL_MouseMotionEvent *mouse_motion_event; 11 | struct size screen_size; 12 | }; 13 | 14 | struct complete_mouse_wheel_event { 15 | SDL_MouseWheelEvent *mouse_wheel_event; 16 | struct point position; 17 | }; 18 | 19 | bool 20 | input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, 21 | struct control_msg *to, bool useIME); 22 | 23 | bool 24 | mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, 25 | struct size screen_size, 26 | struct control_msg *to); 27 | 28 | // the video size may be different from the real device size, so we need the 29 | // size to which the absolute position apply, to scale it accordingly 30 | bool 31 | mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from, 32 | struct size screen_size, 33 | struct control_msg *to); 34 | 35 | bool 36 | finger_from_sdl_to_android(const SDL_TouchFingerEvent *from, 37 | struct size screen_size, 38 | struct control_msg *to); 39 | 40 | // on Android, a scroll event requires the current mouse position 41 | bool 42 | mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from, 43 | struct position position, 44 | struct control_msg *to); 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /app/src/decoder.c: -------------------------------------------------------------------------------- 1 | #include "decoder.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "compat.h" 12 | #include "config.h" 13 | #include "buffer_util.h" 14 | #include "events.h" 15 | #include "lock_util.h" 16 | #include "log.h" 17 | #include "recorder.h" 18 | #include "video_buffer.h" 19 | 20 | // set the decoded frame as ready for rendering, and notify 21 | static void 22 | push_frame(struct decoder *decoder) { 23 | bool previous_frame_skipped; 24 | video_buffer_offer_decoded_frame(decoder->video_buffer, 25 | &previous_frame_skipped); 26 | if (previous_frame_skipped) { 27 | // the previous EVENT_NEW_FRAME will consume this frame 28 | return; 29 | } 30 | static SDL_Event new_frame_event = { 31 | .type = EVENT_NEW_FRAME, 32 | }; 33 | SDL_PushEvent(&new_frame_event); 34 | } 35 | 36 | void 37 | decoder_init(struct decoder *decoder, struct video_buffer *vb) { 38 | decoder->video_buffer = vb; 39 | } 40 | 41 | bool 42 | decoder_open(struct decoder *decoder, const AVCodec *codec) { 43 | decoder->codec_ctx = avcodec_alloc_context3(codec); 44 | if (!decoder->codec_ctx) { 45 | LOGC("Could not allocate decoder context"); 46 | return false; 47 | } 48 | 49 | if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) { 50 | LOGE("Could not open codec"); 51 | avcodec_free_context(&decoder->codec_ctx); 52 | return false; 53 | } 54 | 55 | return true; 56 | } 57 | 58 | void 59 | decoder_close(struct decoder *decoder) { 60 | avcodec_close(decoder->codec_ctx); 61 | avcodec_free_context(&decoder->codec_ctx); 62 | } 63 | 64 | bool 65 | decoder_push(struct decoder *decoder, const AVPacket *packet) { 66 | // the new decoding/encoding API has been introduced by: 67 | // 68 | #ifdef SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API 69 | int ret; 70 | if ((ret = avcodec_send_packet(decoder->codec_ctx, packet)) < 0) { 71 | LOGE("Could not send video packet: %d", ret); 72 | return false; 73 | } 74 | ret = avcodec_receive_frame(decoder->codec_ctx, 75 | decoder->video_buffer->decoding_frame); 76 | if (!ret) { 77 | // a frame was received 78 | push_frame(decoder); 79 | } else if (ret != AVERROR(EAGAIN)) { 80 | LOGE("Could not receive video frame: %d", ret); 81 | return false; 82 | } 83 | #else 84 | int got_picture; 85 | int len = avcodec_decode_video2(decoder->codec_ctx, 86 | decoder->video_buffer->decoding_frame, 87 | &got_picture, 88 | packet); 89 | if (len < 0) { 90 | LOGE("Could not decode video packet: %d", len); 91 | return false; 92 | } 93 | if (got_picture) { 94 | push_frame(decoder); 95 | } 96 | #endif 97 | return true; 98 | } 99 | 100 | void 101 | decoder_interrupt(struct decoder *decoder) { 102 | video_buffer_interrupt(decoder->video_buffer); 103 | } 104 | -------------------------------------------------------------------------------- /app/src/decoder.h: -------------------------------------------------------------------------------- 1 | #ifndef DECODER_H 2 | #define DECODER_H 3 | 4 | #include 5 | #include 6 | 7 | struct video_buffer; 8 | 9 | struct decoder { 10 | struct video_buffer *video_buffer; 11 | AVCodecContext *codec_ctx; 12 | }; 13 | 14 | void 15 | decoder_init(struct decoder *decoder, struct video_buffer *vb); 16 | 17 | bool 18 | decoder_open(struct decoder *decoder, const AVCodec *codec); 19 | 20 | void 21 | decoder_close(struct decoder *decoder); 22 | 23 | bool 24 | decoder_push(struct decoder *decoder, const AVPacket *packet); 25 | 26 | void 27 | decoder_interrupt(struct decoder *decoder); 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /app/src/device.c: -------------------------------------------------------------------------------- 1 | #include "device.h" 2 | #include "log.h" 3 | 4 | bool 5 | device_read_info(socket_t device_socket, char *device_name, struct size *size) { 6 | unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4]; 7 | int r = net_recv_all(device_socket, buf, sizeof(buf)); 8 | if (r < DEVICE_NAME_FIELD_LENGTH + 4) { 9 | LOGE("Could not retrieve device information"); 10 | return false; 11 | } 12 | // in case the client sends garbage 13 | buf[DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; 14 | // strcpy is safe here, since name contains at least 15 | // DEVICE_NAME_FIELD_LENGTH bytes and strlen(buf) < DEVICE_NAME_FIELD_LENGTH 16 | strcpy(device_name, (char *) buf); 17 | size->width = (buf[DEVICE_NAME_FIELD_LENGTH] << 8) 18 | | buf[DEVICE_NAME_FIELD_LENGTH + 1]; 19 | size->height = (buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8) 20 | | buf[DEVICE_NAME_FIELD_LENGTH + 3]; 21 | return true; 22 | } 23 | -------------------------------------------------------------------------------- /app/src/device.h: -------------------------------------------------------------------------------- 1 | #ifndef DEVICE_H 2 | #define DEVICE_H 3 | 4 | #include 5 | 6 | #include "common.h" 7 | #include "net.h" 8 | 9 | #define DEVICE_NAME_FIELD_LENGTH 64 10 | 11 | // name must be at least DEVICE_NAME_FIELD_LENGTH bytes 12 | bool 13 | device_read_info(socket_t device_socket, char *device_name, struct size *size); 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /app/src/device_msg.c: -------------------------------------------------------------------------------- 1 | #include "device_msg.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "buffer_util.h" 7 | #include "log.h" 8 | 9 | ssize_t 10 | device_msg_deserialize(const unsigned char *buf, size_t len, 11 | struct device_msg *msg) { 12 | if (len < 3) { 13 | // at least type + empty string length 14 | return 0; // not available 15 | } 16 | 17 | msg->type = buf[0]; 18 | switch (msg->type) { 19 | case DEVICE_MSG_TYPE_CLIPBOARD: { 20 | uint16_t clipboard_len = buffer_read16be(&buf[1]); 21 | if (clipboard_len > len - 3) { 22 | return 0; // not available 23 | } 24 | char *text = SDL_malloc(clipboard_len + 1); 25 | if (!text) { 26 | LOGW("Could not allocate text for clipboard"); 27 | return -1; 28 | } 29 | if (clipboard_len) { 30 | memcpy(text, &buf[3], clipboard_len); 31 | } 32 | text[clipboard_len] = '\0'; 33 | 34 | msg->clipboard.text = text; 35 | return 3 + clipboard_len; 36 | } 37 | default: 38 | LOGW("Unknown device message type: %d", (int) msg->type); 39 | return -1; // error, we cannot recover 40 | } 41 | } 42 | 43 | void 44 | device_msg_destroy(struct device_msg *msg) { 45 | if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) { 46 | SDL_free(msg->clipboard.text); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/device_msg.h: -------------------------------------------------------------------------------- 1 | #ifndef DEVICEMSG_H 2 | #define DEVICEMSG_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #define DEVICE_MSG_TEXT_MAX_LENGTH 4093 9 | #define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH) 10 | 11 | enum device_msg_type { 12 | DEVICE_MSG_TYPE_CLIPBOARD, 13 | }; 14 | 15 | struct device_msg { 16 | enum device_msg_type type; 17 | union { 18 | struct { 19 | char *text; // owned, to be freed by SDL_free() 20 | } clipboard; 21 | }; 22 | }; 23 | 24 | // return the number of bytes consumed (0 for no msg available, -1 on error) 25 | ssize_t 26 | device_msg_deserialize(const unsigned char *buf, size_t len, 27 | struct device_msg *msg); 28 | 29 | void 30 | device_msg_destroy(struct device_msg *msg); 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /app/src/events.h: -------------------------------------------------------------------------------- 1 | #define EVENT_NEW_SESSION SDL_USEREVENT 2 | #define EVENT_NEW_FRAME (SDL_USEREVENT + 1) 3 | #define EVENT_STREAM_STOPPED (SDL_USEREVENT + 2) 4 | #define EVENT_TIMER (SDL_USEREVENT + 3) 5 | -------------------------------------------------------------------------------- /app/src/file_handler.c: -------------------------------------------------------------------------------- 1 | #include "file_handler.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "config.h" 7 | #include "command.h" 8 | #include "lock_util.h" 9 | #include "log.h" 10 | 11 | #define DEFAULT_PUSH_TARGET "/sdcard/" 12 | 13 | static void 14 | file_handler_request_destroy(struct file_handler_request *req) { 15 | SDL_free(req->file); 16 | } 17 | 18 | bool 19 | file_handler_init(struct file_handler *file_handler, const char *serial, 20 | const char *push_target) { 21 | 22 | cbuf_init(&file_handler->queue); 23 | 24 | if (!(file_handler->mutex = SDL_CreateMutex())) { 25 | return false; 26 | } 27 | 28 | if (!(file_handler->event_cond = SDL_CreateCond())) { 29 | SDL_DestroyMutex(file_handler->mutex); 30 | return false; 31 | } 32 | 33 | if (serial) { 34 | file_handler->serial = SDL_strdup(serial); 35 | if (!file_handler->serial) { 36 | LOGW("Could not strdup serial"); 37 | SDL_DestroyCond(file_handler->event_cond); 38 | SDL_DestroyMutex(file_handler->mutex); 39 | return false; 40 | } 41 | } else { 42 | file_handler->serial = NULL; 43 | } 44 | 45 | // lazy initialization 46 | file_handler->initialized = false; 47 | 48 | file_handler->stopped = false; 49 | file_handler->current_process = PROCESS_NONE; 50 | 51 | file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET; 52 | 53 | return true; 54 | } 55 | 56 | void 57 | file_handler_destroy(struct file_handler *file_handler) { 58 | SDL_DestroyCond(file_handler->event_cond); 59 | SDL_DestroyMutex(file_handler->mutex); 60 | SDL_free(file_handler->serial); 61 | 62 | struct file_handler_request req; 63 | while (cbuf_take(&file_handler->queue, &req)) { 64 | file_handler_request_destroy(&req); 65 | } 66 | } 67 | 68 | static process_t 69 | install_apk(const char *serial, const char *file) { 70 | return adb_install(serial, file); 71 | } 72 | 73 | static process_t 74 | push_file(const char *serial, const char *file, const char *push_target) { 75 | return adb_push(serial, file, push_target); 76 | } 77 | 78 | bool 79 | file_handler_request(struct file_handler *file_handler, 80 | file_handler_action_t action, char *file) { 81 | // start file_handler if it's used for the first time 82 | if (!file_handler->initialized) { 83 | if (!file_handler_start(file_handler)) { 84 | return false; 85 | } 86 | file_handler->initialized = true; 87 | } 88 | 89 | LOGI("Request to %s %s", action == ACTION_INSTALL_APK ? "install" : "push", 90 | file); 91 | struct file_handler_request req = { 92 | .action = action, 93 | .file = file, 94 | }; 95 | 96 | mutex_lock(file_handler->mutex); 97 | bool was_empty = cbuf_is_empty(&file_handler->queue); 98 | bool res = cbuf_push(&file_handler->queue, req); 99 | if (was_empty) { 100 | cond_signal(file_handler->event_cond); 101 | } 102 | mutex_unlock(file_handler->mutex); 103 | return res; 104 | } 105 | 106 | static int 107 | run_file_handler(void *data) { 108 | struct file_handler *file_handler = data; 109 | 110 | for (;;) { 111 | mutex_lock(file_handler->mutex); 112 | file_handler->current_process = PROCESS_NONE; 113 | while (!file_handler->stopped && cbuf_is_empty(&file_handler->queue)) { 114 | cond_wait(file_handler->event_cond, file_handler->mutex); 115 | } 116 | if (file_handler->stopped) { 117 | // stop immediately, do not process further events 118 | mutex_unlock(file_handler->mutex); 119 | break; 120 | } 121 | struct file_handler_request req; 122 | bool non_empty = cbuf_take(&file_handler->queue, &req); 123 | SDL_assert(non_empty); 124 | 125 | process_t process; 126 | if (req.action == ACTION_INSTALL_APK) { 127 | LOGI("Installing %s...", req.file); 128 | process = install_apk(file_handler->serial, req.file); 129 | } else { 130 | LOGI("Pushing %s...", req.file); 131 | process = push_file(file_handler->serial, req.file, 132 | file_handler->push_target); 133 | } 134 | file_handler->current_process = process; 135 | mutex_unlock(file_handler->mutex); 136 | 137 | if (req.action == ACTION_INSTALL_APK) { 138 | if (process_check_success(process, "adb install")) { 139 | LOGI("%s successfully installed", req.file); 140 | } else { 141 | LOGE("Failed to install %s", req.file); 142 | } 143 | } else { 144 | if (process_check_success(process, "adb push")) { 145 | LOGI("%s successfully pushed to %s", req.file, 146 | file_handler->push_target); 147 | } else { 148 | LOGE("Failed to push %s to %s", req.file, 149 | file_handler->push_target); 150 | } 151 | } 152 | 153 | file_handler_request_destroy(&req); 154 | } 155 | return 0; 156 | } 157 | 158 | bool 159 | file_handler_start(struct file_handler *file_handler) { 160 | LOGD("Starting file_handler thread"); 161 | 162 | file_handler->thread = SDL_CreateThread(run_file_handler, "file_handler", 163 | file_handler); 164 | if (!file_handler->thread) { 165 | LOGC("Could not start file_handler thread"); 166 | return false; 167 | } 168 | 169 | return true; 170 | } 171 | 172 | void 173 | file_handler_stop(struct file_handler *file_handler) { 174 | mutex_lock(file_handler->mutex); 175 | file_handler->stopped = true; 176 | cond_signal(file_handler->event_cond); 177 | if (file_handler->current_process != PROCESS_NONE) { 178 | if (!cmd_terminate(file_handler->current_process)) { 179 | LOGW("Could not terminate install process"); 180 | } 181 | cmd_simple_wait(file_handler->current_process, NULL); 182 | file_handler->current_process = PROCESS_NONE; 183 | } 184 | mutex_unlock(file_handler->mutex); 185 | } 186 | 187 | void 188 | file_handler_join(struct file_handler *file_handler) { 189 | SDL_WaitThread(file_handler->thread, NULL); 190 | } 191 | -------------------------------------------------------------------------------- /app/src/file_handler.h: -------------------------------------------------------------------------------- 1 | #ifndef FILE_HANDLER_H 2 | #define FILE_HANDLER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "cbuf.h" 9 | #include "command.h" 10 | 11 | typedef enum { 12 | ACTION_INSTALL_APK, 13 | ACTION_PUSH_FILE, 14 | } file_handler_action_t; 15 | 16 | struct file_handler_request { 17 | file_handler_action_t action; 18 | char *file; 19 | }; 20 | 21 | struct file_handler_request_queue CBUF(struct file_handler_request, 16); 22 | 23 | struct file_handler { 24 | char *serial; 25 | const char *push_target; 26 | SDL_Thread *thread; 27 | SDL_mutex *mutex; 28 | SDL_cond *event_cond; 29 | bool stopped; 30 | bool initialized; 31 | process_t current_process; 32 | struct file_handler_request_queue queue; 33 | }; 34 | 35 | bool 36 | file_handler_init(struct file_handler *file_handler, const char *serial, 37 | const char *push_target); 38 | 39 | void 40 | file_handler_destroy(struct file_handler *file_handler); 41 | 42 | bool 43 | file_handler_start(struct file_handler *file_handler); 44 | 45 | void 46 | file_handler_stop(struct file_handler *file_handler); 47 | 48 | void 49 | file_handler_join(struct file_handler *file_handler); 50 | 51 | // take ownership of file, and will SDL_free() it 52 | bool 53 | file_handler_request(struct file_handler *file_handler, 54 | file_handler_action_t action, 55 | char *file); 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /app/src/fps_counter.c: -------------------------------------------------------------------------------- 1 | #include "fps_counter.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "lock_util.h" 7 | #include "log.h" 8 | 9 | #define FPS_COUNTER_INTERVAL_MS 1000 10 | 11 | bool 12 | fps_counter_init(struct fps_counter *counter) { 13 | counter->mutex = SDL_CreateMutex(); 14 | if (!counter->mutex) { 15 | return false; 16 | } 17 | 18 | counter->state_cond = SDL_CreateCond(); 19 | if (!counter->state_cond) { 20 | SDL_DestroyMutex(counter->mutex); 21 | return false; 22 | } 23 | 24 | counter->thread = NULL; 25 | SDL_AtomicSet(&counter->started, 0); 26 | // no need to initialize the other fields, they are unused until started 27 | 28 | return true; 29 | } 30 | 31 | void 32 | fps_counter_destroy(struct fps_counter *counter) { 33 | SDL_DestroyCond(counter->state_cond); 34 | SDL_DestroyMutex(counter->mutex); 35 | } 36 | 37 | // must be called with mutex locked 38 | static void 39 | display_fps(struct fps_counter *counter) { 40 | unsigned rendered_per_second = 41 | counter->nr_rendered * 1000 / FPS_COUNTER_INTERVAL_MS; 42 | if (counter->nr_skipped) { 43 | LOGI("%u fps (+%u frames skipped)", rendered_per_second, 44 | counter->nr_skipped); 45 | } else { 46 | LOGI("%u fps", rendered_per_second); 47 | } 48 | } 49 | 50 | // must be called with mutex locked 51 | static void 52 | check_interval_expired(struct fps_counter *counter, uint32_t now) { 53 | if (now < counter->next_timestamp) { 54 | return; 55 | } 56 | 57 | display_fps(counter); 58 | counter->nr_rendered = 0; 59 | counter->nr_skipped = 0; 60 | // add a multiple of the interval 61 | uint32_t elapsed_slices = 62 | (now - counter->next_timestamp) / FPS_COUNTER_INTERVAL_MS + 1; 63 | counter->next_timestamp += FPS_COUNTER_INTERVAL_MS * elapsed_slices; 64 | } 65 | 66 | static int 67 | run_fps_counter(void *data) { 68 | struct fps_counter *counter = data; 69 | 70 | mutex_lock(counter->mutex); 71 | while (!counter->interrupted) { 72 | while (!counter->interrupted && !SDL_AtomicGet(&counter->started)) { 73 | cond_wait(counter->state_cond, counter->mutex); 74 | } 75 | while (!counter->interrupted && SDL_AtomicGet(&counter->started)) { 76 | uint32_t now = SDL_GetTicks(); 77 | check_interval_expired(counter, now); 78 | 79 | SDL_assert(counter->next_timestamp > now); 80 | uint32_t remaining = counter->next_timestamp - now; 81 | 82 | // ignore the reason (timeout or signaled), we just loop anyway 83 | cond_wait_timeout(counter->state_cond, counter->mutex, remaining); 84 | } 85 | } 86 | mutex_unlock(counter->mutex); 87 | return 0; 88 | } 89 | 90 | bool 91 | fps_counter_start(struct fps_counter *counter) { 92 | mutex_lock(counter->mutex); 93 | counter->next_timestamp = SDL_GetTicks() + FPS_COUNTER_INTERVAL_MS; 94 | counter->nr_rendered = 0; 95 | counter->nr_skipped = 0; 96 | mutex_unlock(counter->mutex); 97 | 98 | SDL_AtomicSet(&counter->started, 1); 99 | cond_signal(counter->state_cond); 100 | 101 | // counter->thread is always accessed from the same thread, no need to lock 102 | if (!counter->thread) { 103 | counter->thread = 104 | SDL_CreateThread(run_fps_counter, "fps counter", counter); 105 | if (!counter->thread) { 106 | LOGE("Could not start FPS counter thread"); 107 | return false; 108 | } 109 | } 110 | 111 | return true; 112 | } 113 | 114 | void 115 | fps_counter_stop(struct fps_counter *counter) { 116 | SDL_AtomicSet(&counter->started, 0); 117 | cond_signal(counter->state_cond); 118 | } 119 | 120 | bool 121 | fps_counter_is_started(struct fps_counter *counter) { 122 | return SDL_AtomicGet(&counter->started); 123 | } 124 | 125 | void 126 | fps_counter_interrupt(struct fps_counter *counter) { 127 | if (!counter->thread) { 128 | return; 129 | } 130 | 131 | mutex_lock(counter->mutex); 132 | counter->interrupted = true; 133 | mutex_unlock(counter->mutex); 134 | // wake up blocking wait 135 | cond_signal(counter->state_cond); 136 | } 137 | 138 | void 139 | fps_counter_join(struct fps_counter *counter) { 140 | if (counter->thread) { 141 | SDL_WaitThread(counter->thread, NULL); 142 | } 143 | } 144 | 145 | void 146 | fps_counter_add_rendered_frame(struct fps_counter *counter) { 147 | if (!SDL_AtomicGet(&counter->started)) { 148 | return; 149 | } 150 | 151 | mutex_lock(counter->mutex); 152 | uint32_t now = SDL_GetTicks(); 153 | check_interval_expired(counter, now); 154 | ++counter->nr_rendered; 155 | mutex_unlock(counter->mutex); 156 | } 157 | 158 | void 159 | fps_counter_add_skipped_frame(struct fps_counter *counter) { 160 | if (!SDL_AtomicGet(&counter->started)) { 161 | return; 162 | } 163 | 164 | mutex_lock(counter->mutex); 165 | uint32_t now = SDL_GetTicks(); 166 | check_interval_expired(counter, now); 167 | ++counter->nr_skipped; 168 | mutex_unlock(counter->mutex); 169 | } 170 | -------------------------------------------------------------------------------- /app/src/fps_counter.h: -------------------------------------------------------------------------------- 1 | #ifndef FPSCOUNTER_H 2 | #define FPSCOUNTER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | struct fps_counter { 11 | SDL_Thread *thread; 12 | SDL_mutex *mutex; 13 | SDL_cond *state_cond; 14 | 15 | // atomic so that we can check without locking the mutex 16 | // if the FPS counter is disabled, we don't want to lock unnecessarily 17 | SDL_atomic_t started; 18 | 19 | // the following fields are protected by the mutex 20 | bool interrupted; 21 | unsigned nr_rendered; 22 | unsigned nr_skipped; 23 | uint32_t next_timestamp; 24 | }; 25 | 26 | bool 27 | fps_counter_init(struct fps_counter *counter); 28 | 29 | void 30 | fps_counter_destroy(struct fps_counter *counter); 31 | 32 | bool 33 | fps_counter_start(struct fps_counter *counter); 34 | 35 | void 36 | fps_counter_stop(struct fps_counter *counter); 37 | 38 | bool 39 | fps_counter_is_started(struct fps_counter *counter); 40 | 41 | // request to stop the thread (on quit) 42 | // must be called before fps_counter_join() 43 | void 44 | fps_counter_interrupt(struct fps_counter *counter); 45 | 46 | void 47 | fps_counter_join(struct fps_counter *counter); 48 | 49 | void 50 | fps_counter_add_rendered_frame(struct fps_counter *counter); 51 | 52 | void 53 | fps_counter_add_skipped_frame(struct fps_counter *counter); 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /app/src/icon.xpm: -------------------------------------------------------------------------------- 1 | /* XPM */ 2 | static char * icon_xpm[] = { 3 | "48 48 2 1", 4 | " c None", 5 | ". c #96C13E", 6 | " .. .. ", 7 | " ... ... ", 8 | " ... ...... ... ", 9 | " ................ ", 10 | " .............. ", 11 | " ................ ", 12 | " .................. ", 13 | " .................... ", 14 | " ..... ........ ..... ", 15 | " ..... ........ ..... ", 16 | " ...................... ", 17 | " ........................ ", 18 | " ........................ ", 19 | " ........................ ", 20 | " ", 21 | " ", 22 | " .... ........................ .... ", 23 | " ...... ........................ ...... ", 24 | " ...... ........................ ...... ", 25 | " ...... ........................ ...... ", 26 | " ...... ........................ ...... ", 27 | " ...... ........................ ...... ", 28 | " ...... ........................ ...... ", 29 | " ...... ........................ ...... ", 30 | " ...... ........................ ...... ", 31 | " ...... ........................ ...... ", 32 | " ...... ........................ ...... ", 33 | " ...... ........................ ...... ", 34 | " ...... ........................ ...... ", 35 | " ...... ........................ ...... ", 36 | " ...... ........................ ...... ", 37 | " ...... ........................ ...... ", 38 | " ...... ........................ ...... ", 39 | " ...... ........................ ...... ", 40 | " ...... ........................ ...... ", 41 | " .... ........................ .... ", 42 | " ........................ ", 43 | " ...................... ", 44 | " ...... ...... ", 45 | " ...... ...... ", 46 | " ...... ...... ", 47 | " ...... ...... ", 48 | " ...... ...... ", 49 | " ...... ...... ", 50 | " ...... ...... ", 51 | " ...... ...... ", 52 | " ...... ...... ", 53 | " .... .... "}; 54 | -------------------------------------------------------------------------------- /app/src/input_manager.h: -------------------------------------------------------------------------------- 1 | #ifndef INPUTMANAGER_H 2 | #define INPUTMANAGER_H 3 | 4 | #include 5 | 6 | #include "common.h" 7 | #include "controller.h" 8 | #include "fps_counter.h" 9 | #include "video_buffer.h" 10 | #include "screen.h" 11 | 12 | struct input_manager { 13 | struct controller *controller; 14 | struct video_buffer *video_buffer; 15 | struct screen *screen; 16 | }; 17 | 18 | void 19 | input_manager_process_text_input(struct input_manager *input_manager, 20 | const SDL_TextInputEvent *event, bool useIME); 21 | 22 | bool 23 | input_manager_process_key(struct input_manager *input_manager, 24 | const SDL_KeyboardEvent *event, 25 | bool control, bool useIME); 26 | 27 | void 28 | input_manager_process_mouse_motion(struct input_manager *input_manager, 29 | const SDL_MouseMotionEvent *event); 30 | 31 | void 32 | input_manager_process_mouse_button(struct input_manager *input_manager, 33 | const SDL_MouseButtonEvent *event, 34 | bool control); 35 | 36 | void 37 | input_manager_process_mouse_wheel(struct input_manager *input_manager, 38 | const SDL_MouseWheelEvent *event); 39 | 40 | void 41 | input_manager_process_finger(struct input_manager *input_manager, 42 | const SDL_TouchFingerEvent *event); 43 | 44 | void 45 | input_manager_send_quit(struct input_manager *input_manager); 46 | 47 | void 48 | input_manager_send_ping(struct input_manager *input_manager); 49 | 50 | void 51 | input_manager_send_rotation(struct input_manager *input_manager); 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /app/src/lock_util.h: -------------------------------------------------------------------------------- 1 | #ifndef LOCKUTIL_H 2 | #define LOCKUTIL_H 3 | 4 | #include 5 | #include 6 | 7 | #include "log.h" 8 | 9 | static inline void 10 | mutex_lock(SDL_mutex *mutex) { 11 | if (SDL_LockMutex(mutex)) { 12 | LOGC("Could not lock mutex"); 13 | abort(); 14 | } 15 | } 16 | 17 | static inline void 18 | mutex_unlock(SDL_mutex *mutex) { 19 | if (SDL_UnlockMutex(mutex)) { 20 | LOGC("Could not unlock mutex"); 21 | abort(); 22 | } 23 | } 24 | 25 | static inline void 26 | cond_wait(SDL_cond *cond, SDL_mutex *mutex) { 27 | if (SDL_CondWait(cond, mutex)) { 28 | LOGC("Could not wait on condition"); 29 | abort(); 30 | } 31 | } 32 | 33 | static inline int 34 | cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) { 35 | int r = SDL_CondWaitTimeout(cond, mutex, ms); 36 | if (r < 0) { 37 | LOGC("Could not wait on condition with timeout"); 38 | abort(); 39 | } 40 | return r; 41 | } 42 | 43 | static inline void 44 | cond_signal(SDL_cond *cond) { 45 | if (SDL_CondSignal(cond)) { 46 | LOGC("Could not signal a condition"); 47 | abort(); 48 | } 49 | } 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /app/src/log.h: -------------------------------------------------------------------------------- 1 | #ifndef LOG_H 2 | #define LOG_H 3 | 4 | #include 5 | 6 | #define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) 7 | #define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) 8 | #define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) 9 | #define LOGW(...) SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) 10 | #define LOGE(...) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) 11 | #define LOGC(...) SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /app/src/net.c: -------------------------------------------------------------------------------- 1 | #include "net.h" 2 | 3 | #include 4 | 5 | #include "log.h" 6 | 7 | #ifdef __WINDOWS__ 8 | typedef int socklen_t; 9 | #else 10 | # include 11 | # include 12 | # include 13 | # include 14 | # include 15 | # include 16 | # define SOCKET_ERROR -1 17 | typedef struct sockaddr_in SOCKADDR_IN; 18 | typedef struct sockaddr SOCKADDR; 19 | typedef struct in_addr IN_ADDR; 20 | #endif 21 | 22 | socket_t 23 | net_connect(uint32_t addr, uint16_t port) { 24 | socket_t sock = socket(AF_INET, SOCK_STREAM, 0); 25 | if (sock == INVALID_SOCKET) { 26 | perror("socket"); 27 | return INVALID_SOCKET; 28 | } 29 | 30 | SOCKADDR_IN sin; 31 | sin.sin_family = AF_INET; 32 | sin.sin_addr.s_addr = htonl(addr); 33 | sin.sin_port = htons(port); 34 | 35 | if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { 36 | perror("connect"); 37 | net_close(sock); 38 | return INVALID_SOCKET; 39 | } 40 | /* 41 | https://en.wikipedia.org/wiki/Type_of_service 42 | */ 43 | int optval = 0B00111100; 44 | setsockopt(sock, IPPROTO_IP, IP_TOS, (const char*)&optval, sizeof(optval)); 45 | 46 | int one = 1; 47 | setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (const char*)&one, sizeof(one)); 48 | 49 | size_t size = 2*1024*1024; 50 | setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (const char*)&size, sizeof(size)); 51 | return sock; 52 | } 53 | 54 | socket_t 55 | net_listen(uint32_t addr, uint16_t port, int backlog) { 56 | socket_t sock = socket(AF_INET, SOCK_STREAM, 0); 57 | if (sock == INVALID_SOCKET) { 58 | perror("socket"); 59 | return INVALID_SOCKET; 60 | } 61 | 62 | int reuse = 1; 63 | if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, 64 | sizeof(reuse)) == -1) { 65 | perror("setsockopt(SO_REUSEADDR)"); 66 | } 67 | 68 | SOCKADDR_IN sin; 69 | sin.sin_family = AF_INET; 70 | sin.sin_addr.s_addr = htonl(addr); // htonl() harmless on INADDR_ANY 71 | sin.sin_port = htons(port); 72 | 73 | if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { 74 | perror("bind"); 75 | net_close(sock); 76 | return INVALID_SOCKET; 77 | } 78 | 79 | if (listen(sock, backlog) == SOCKET_ERROR) { 80 | perror("listen"); 81 | net_close(sock); 82 | return INVALID_SOCKET; 83 | } 84 | 85 | return sock; 86 | } 87 | 88 | socket_t 89 | net_accept(socket_t server_socket) { 90 | SOCKADDR_IN csin; 91 | socklen_t sinsize = sizeof(csin); 92 | return accept(server_socket, (SOCKADDR *) &csin, &sinsize); 93 | } 94 | 95 | ssize_t 96 | net_recv(socket_t socket, void *buf, size_t len) { 97 | return recv(socket, buf, len, 0); 98 | } 99 | 100 | ssize_t 101 | net_recv_all(socket_t socket, void *buf, size_t len) { 102 | return recv(socket, buf, len, MSG_WAITALL); 103 | } 104 | 105 | ssize_t 106 | net_send(socket_t socket, const void *buf, size_t len) { 107 | return send(socket, buf, len, 0); 108 | } 109 | 110 | ssize_t 111 | net_send_all(socket_t socket, const void *buf, size_t len) { 112 | ssize_t w = 0; 113 | while (len > 0) { 114 | w = send(socket, buf, len, 0); 115 | if (w == -1) { 116 | return -1; 117 | } 118 | len -= w; 119 | buf = (char *) buf + w; 120 | } 121 | return w; 122 | } 123 | 124 | bool 125 | net_shutdown(socket_t socket, int how) { 126 | return !shutdown(socket, how); 127 | } 128 | -------------------------------------------------------------------------------- /app/src/net.h: -------------------------------------------------------------------------------- 1 | #ifndef NET_H 2 | #define NET_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #ifdef __WINDOWS__ 9 | # include 10 | # include // for IP_TOS only 11 | #define SHUT_RD SD_RECEIVE 12 | #define SHUT_WR SD_SEND 13 | #define SHUT_RDWR SD_BOTH 14 | typedef SOCKET socket_t; 15 | #else 16 | # include 17 | # define INVALID_SOCKET -1 18 | typedef int socket_t; 19 | #endif 20 | 21 | bool 22 | net_init(void); 23 | 24 | void 25 | net_cleanup(void); 26 | 27 | socket_t 28 | net_connect(uint32_t addr, uint16_t port); 29 | 30 | socket_t 31 | net_listen(uint32_t addr, uint16_t port, int backlog); 32 | 33 | socket_t 34 | net_accept(socket_t server_socket); 35 | 36 | // the _all versions wait/retry until len bytes have been written/read 37 | ssize_t 38 | net_recv(socket_t socket, void *buf, size_t len); 39 | 40 | ssize_t 41 | net_recv_all(socket_t socket, void *buf, size_t len); 42 | 43 | ssize_t 44 | net_send(socket_t socket, const void *buf, size_t len); 45 | 46 | ssize_t 47 | net_send_all(socket_t socket, const void *buf, size_t len); 48 | 49 | // how is SHUT_RD (read), SHUT_WR (write) or SHUT_RDWR (both) 50 | bool 51 | net_shutdown(socket_t socket, int how); 52 | 53 | bool 54 | net_close(socket_t socket); 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /app/src/queue.h: -------------------------------------------------------------------------------- 1 | // generic intrusive FIFO queue 2 | #ifndef QUEUE_H 3 | #define QUEUE_H 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | // To define a queue type of "struct foo": 10 | // struct queue_foo QUEUE(struct foo); 11 | #define QUEUE(TYPE) { \ 12 | TYPE *first; \ 13 | TYPE *last; \ 14 | } 15 | 16 | #define queue_init(PQ) \ 17 | (void) ((PQ)->first = (PQ)->last = NULL) 18 | 19 | #define queue_is_empty(PQ) \ 20 | !(PQ)->first 21 | 22 | // NEXTFIELD is the field in the ITEM type used for intrusive linked-list 23 | // 24 | // For example: 25 | // struct foo { 26 | // int value; 27 | // struct foo *next; 28 | // }; 29 | // 30 | // // define the type "struct my_queue" 31 | // struct my_queue QUEUE(struct foo); 32 | // 33 | // struct my_queue queue; 34 | // queue_init(&queue); 35 | // 36 | // struct foo v1 = { .value = 42 }; 37 | // struct foo v2 = { .value = 27 }; 38 | // 39 | // queue_push(&queue, next, v1); 40 | // queue_push(&queue, next, v2); 41 | // 42 | // struct foo *foo; 43 | // queue_take(&queue, next, &foo); 44 | // assert(foo->value == 42); 45 | // queue_take(&queue, next, &foo); 46 | // assert(foo->value == 27); 47 | // assert(queue_is_empty(&queue)); 48 | // 49 | 50 | // push a new item into the queue 51 | #define queue_push(PQ, NEXTFIELD, ITEM) \ 52 | (void) ({ \ 53 | (ITEM)->NEXTFIELD = NULL; \ 54 | if (queue_is_empty(PQ)) { \ 55 | (PQ)->first = (PQ)->last = (ITEM); \ 56 | } else { \ 57 | (PQ)->last->NEXTFIELD = (ITEM); \ 58 | (PQ)->last = (ITEM); \ 59 | } \ 60 | }) 61 | 62 | // take the next item and remove it from the queue (the queue must not be empty) 63 | // the result is stored in *(PITEM) 64 | // (without typeof(), we could not store a local variable having the correct 65 | // type so that we can "return" it) 66 | #define queue_take(PQ, NEXTFIELD, PITEM) \ 67 | (void) ({ \ 68 | SDL_assert(!queue_is_empty(PQ)); \ 69 | *(PITEM) = (PQ)->first; \ 70 | (PQ)->first = (PQ)->first->NEXTFIELD; \ 71 | }) 72 | // no need to update (PQ)->last if the queue is left empty: 73 | // (PQ)->last is undefined if !(PQ)->first anyway 74 | 75 | #endif 76 | -------------------------------------------------------------------------------- /app/src/receiver.c: -------------------------------------------------------------------------------- 1 | #include "receiver.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "config.h" 7 | #include "device_msg.h" 8 | #include "lock_util.h" 9 | #include "log.h" 10 | 11 | bool 12 | receiver_init(struct receiver *receiver, socket_t control_socket) { 13 | if (!(receiver->mutex = SDL_CreateMutex())) { 14 | return false; 15 | } 16 | receiver->control_socket = control_socket; 17 | return true; 18 | } 19 | 20 | void 21 | receiver_destroy(struct receiver *receiver) { 22 | SDL_DestroyMutex(receiver->mutex); 23 | } 24 | 25 | static void 26 | process_msg(struct receiver *receiver, struct device_msg *msg) { 27 | switch (msg->type) { 28 | case DEVICE_MSG_TYPE_CLIPBOARD: 29 | LOGI("Device clipboard copied"); 30 | SDL_SetClipboardText(msg->clipboard.text); 31 | break; 32 | } 33 | } 34 | 35 | static ssize_t 36 | process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) { 37 | size_t head = 0; 38 | for (;;) { 39 | struct device_msg msg; 40 | ssize_t r = device_msg_deserialize(&buf[head], len - head, &msg); 41 | if (r == -1) { 42 | return -1; 43 | } 44 | if (r == 0) { 45 | return head; 46 | } 47 | 48 | process_msg(receiver, &msg); 49 | device_msg_destroy(&msg); 50 | 51 | head += r; 52 | SDL_assert(head <= len); 53 | if (head == len) { 54 | return head; 55 | } 56 | } 57 | } 58 | 59 | static int 60 | run_receiver(void *data) { 61 | struct receiver *receiver = data; 62 | 63 | unsigned char buf[DEVICE_MSG_SERIALIZED_MAX_SIZE]; 64 | size_t head = 0; 65 | 66 | for (;;) { 67 | SDL_assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE); 68 | ssize_t r = net_recv(receiver->control_socket, buf, 69 | DEVICE_MSG_SERIALIZED_MAX_SIZE - head); 70 | if (r <= 0) { 71 | LOGD("Receiver stopped"); 72 | break; 73 | } 74 | 75 | ssize_t consumed = process_msgs(receiver, buf, r); 76 | if (consumed == -1) { 77 | // an error occurred 78 | break; 79 | } 80 | 81 | if (consumed) { 82 | // shift the remaining data in the buffer 83 | memmove(buf, &buf[consumed], r - consumed); 84 | head = r - consumed; 85 | } 86 | } 87 | 88 | return 0; 89 | } 90 | 91 | bool 92 | receiver_start(struct receiver *receiver) { 93 | LOGD("Starting receiver thread"); 94 | 95 | receiver->thread = SDL_CreateThread(run_receiver, "receiver", receiver); 96 | if (!receiver->thread) { 97 | LOGC("Could not start receiver thread"); 98 | return false; 99 | } 100 | 101 | return true; 102 | } 103 | 104 | void 105 | receiver_join(struct receiver *receiver) { 106 | SDL_WaitThread(receiver->thread, NULL); 107 | } 108 | -------------------------------------------------------------------------------- /app/src/receiver.h: -------------------------------------------------------------------------------- 1 | #ifndef RECEIVER_H 2 | #define RECEIVER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "net.h" 9 | 10 | // receive events from the device 11 | // managed by the controller 12 | struct receiver { 13 | socket_t control_socket; 14 | SDL_Thread *thread; 15 | SDL_mutex *mutex; 16 | }; 17 | 18 | bool 19 | receiver_init(struct receiver *receiver, socket_t control_socket); 20 | 21 | void 22 | receiver_destroy(struct receiver *receiver); 23 | 24 | bool 25 | receiver_start(struct receiver *receiver); 26 | 27 | // no receiver_stop(), it will automatically stop on control_socket shutdown 28 | 29 | void 30 | receiver_join(struct receiver *receiver); 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /app/src/recorder.h: -------------------------------------------------------------------------------- 1 | #ifndef RECORDER_H 2 | #define RECORDER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "common.h" 10 | #include "queue.h" 11 | 12 | enum recorder_format { 13 | RECORDER_FORMAT_MP4 = 1, 14 | RECORDER_FORMAT_MKV, 15 | }; 16 | 17 | struct record_packet { 18 | AVPacket packet; 19 | struct record_packet *next; 20 | }; 21 | 22 | struct recorder_queue QUEUE(struct record_packet); 23 | 24 | struct recorder { 25 | char *filename; 26 | enum recorder_format format; 27 | AVFormatContext *ctx; 28 | struct size declared_frame_size; 29 | bool header_written; 30 | 31 | SDL_Thread *thread; 32 | SDL_mutex *mutex; 33 | SDL_cond *queue_cond; 34 | bool stopped; // set on recorder_stop() by the stream reader 35 | bool failed; // set on packet write failure 36 | struct recorder_queue queue; 37 | 38 | // we can write a packet only once we received the next one so that we can 39 | // set its duration (next_pts - current_pts) 40 | // "previous" is only accessed from the recorder thread, so it does not 41 | // need to be protected by the mutex 42 | struct record_packet *previous; 43 | }; 44 | 45 | bool 46 | recorder_init(struct recorder *recorder, const char *filename, 47 | enum recorder_format format, struct size declared_frame_size); 48 | 49 | void 50 | recorder_destroy(struct recorder *recorder); 51 | 52 | bool 53 | recorder_open(struct recorder *recorder, const AVCodec *input_codec); 54 | 55 | void 56 | recorder_close(struct recorder *recorder); 57 | 58 | bool 59 | recorder_start(struct recorder *recorder); 60 | 61 | void 62 | recorder_stop(struct recorder *recorder); 63 | 64 | void 65 | recorder_join(struct recorder *recorder); 66 | 67 | bool 68 | recorder_push(struct recorder *recorder, const AVPacket *packet); 69 | 70 | #endif 71 | -------------------------------------------------------------------------------- /app/src/scrcpy.h: -------------------------------------------------------------------------------- 1 | #ifndef SCRCPY_H 2 | #define SCRCPY_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | struct scrcpy_options { 9 | const char *serial; 10 | const char *crop; 11 | const char *record_filename; 12 | const char *window_title; 13 | const char *push_target; 14 | enum recorder_format record_format; 15 | uint16_t port; 16 | uint16_t max_size; 17 | uint32_t bit_rate; 18 | bool show_touches; 19 | bool fullscreen; 20 | bool always_on_top; 21 | bool control; 22 | bool display; 23 | bool turn_screen_off; 24 | bool render_expired_frames; 25 | uint16_t density; 26 | const char *size; 27 | bool tablet; 28 | bool useIME; 29 | bool disableHWOverlays; 30 | }; 31 | 32 | bool 33 | scrcpy(const struct scrcpy_options *options); 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /app/src/screen.h: -------------------------------------------------------------------------------- 1 | #ifndef SCREEN_H 2 | #define SCREEN_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "common.h" 9 | 10 | struct video_buffer; 11 | 12 | struct screen { 13 | SDL_Window *window; 14 | SDL_Renderer *renderer; 15 | SDL_Texture *texture; 16 | struct size frame_size; 17 | //used only in fullscreen mode to know the windowed window size 18 | struct size windowed_window_size; 19 | bool has_frame; 20 | bool fullscreen; 21 | bool no_window; 22 | bool frame_changed; 23 | }; 24 | 25 | #define SCREEN_INITIALIZER { \ 26 | .window = NULL, \ 27 | .renderer = NULL, \ 28 | .texture = NULL, \ 29 | .frame_size = { \ 30 | .width = 0, \ 31 | .height = 0, \ 32 | }, \ 33 | .windowed_window_size = { \ 34 | .width = 0, \ 35 | .height = 0, \ 36 | }, \ 37 | .has_frame = false, \ 38 | .fullscreen = false, \ 39 | .no_window = false, \ 40 | .frame_changed = false, \ 41 | } 42 | 43 | // initialize default values 44 | void 45 | screen_init(struct screen *screen); 46 | 47 | // initialize screen, create window, renderer and texture (window is hidden) 48 | bool 49 | screen_init_rendering(struct screen *screen, const char *window_title, 50 | struct size frame_size, bool always_on_top); 51 | 52 | // show the window 53 | void 54 | screen_show_window(struct screen *screen); 55 | 56 | // destroy window, renderer and texture (if any) 57 | void 58 | screen_destroy(struct screen *screen); 59 | 60 | // resize if necessary and write the rendered frame into the texture 61 | bool 62 | screen_update_frame(struct screen *screen, struct video_buffer *vb); 63 | 64 | // render the texture to the renderer 65 | void 66 | screen_render(struct screen *screen); 67 | 68 | // switch the fullscreen mode 69 | void 70 | screen_switch_fullscreen(struct screen *screen); 71 | 72 | // resize window to optimal size (remove black borders) 73 | void 74 | screen_resize_to_fit(struct screen *screen); 75 | 76 | // resize window to 1:1 (pixel-perfect) 77 | void 78 | screen_resize_to_pixel_perfect(struct screen *screen); 79 | 80 | #endif 81 | -------------------------------------------------------------------------------- /app/src/server.h: -------------------------------------------------------------------------------- 1 | #ifndef SERVER_H 2 | #define SERVER_H 3 | 4 | #include 5 | #include 6 | 7 | #include "command.h" 8 | #include "net.h" 9 | 10 | #define IPV4_LOCALHOST 0x7F000001 11 | 12 | struct server { 13 | char *serial; 14 | process_t process; 15 | socket_t server_socket; // only used if !tunnel_forward 16 | socket_t video_socket; 17 | socket_t control_socket; 18 | uint32_t addr; 19 | uint16_t local_port; 20 | bool tunnel_enabled; 21 | bool tunnel_forward; // use "adb forward" instead of "adb reverse" 22 | }; 23 | 24 | #define SERVER_INITIALIZER { \ 25 | .serial = NULL, \ 26 | .process = PROCESS_NONE, \ 27 | .server_socket = INVALID_SOCKET, \ 28 | .video_socket = INVALID_SOCKET, \ 29 | .control_socket = INVALID_SOCKET, \ 30 | .addr = IPV4_LOCALHOST, \ 31 | .local_port = 0, \ 32 | .tunnel_enabled = false, \ 33 | .tunnel_forward = false, \ 34 | } 35 | 36 | struct server_params { 37 | const char *crop; 38 | uint16_t local_port; 39 | uint16_t max_size; 40 | uint32_t bit_rate; 41 | bool control; 42 | 43 | uint16_t density; 44 | const char* size; 45 | bool tablet; 46 | bool useIME; 47 | bool disableHWOverlays; 48 | }; 49 | 50 | // init default values 51 | void 52 | server_init(struct server *server); 53 | 54 | // push, enable tunnel et start the server 55 | bool 56 | server_start(struct server *server, const char *serial, 57 | const struct server_params *params); 58 | 59 | // block until the communication with the server is established 60 | bool 61 | server_connect_to(struct server *server); 62 | 63 | // disconnect and kill the server process 64 | void 65 | server_stop(struct server *server); 66 | 67 | // close and release sockets 68 | void 69 | server_destroy(struct server *server); 70 | 71 | #endif 72 | -------------------------------------------------------------------------------- /app/src/str_util.c: -------------------------------------------------------------------------------- 1 | #include "str_util.h" 2 | 3 | #include 4 | #include 5 | 6 | #ifdef _WIN32 7 | # include 8 | # include 9 | #endif 10 | 11 | #include 12 | 13 | size_t 14 | xstrncpy(char *dest, const char *src, size_t n) { 15 | size_t i; 16 | for (i = 0; i < n - 1 && src[i] != '\0'; ++i) 17 | dest[i] = src[i]; 18 | if (n) 19 | dest[i] = '\0'; 20 | return src[i] == '\0' ? i : n; 21 | } 22 | 23 | size_t 24 | xstrjoin(char *dst, const char *const tokens[], char sep, size_t n) { 25 | const char *const *remaining = tokens; 26 | const char *token = *remaining++; 27 | size_t i = 0; 28 | while (token) { 29 | if (i) { 30 | dst[i++] = sep; 31 | if (i == n) 32 | goto truncated; 33 | } 34 | size_t w = xstrncpy(dst + i, token, n - i); 35 | if (w >= n - i) 36 | goto truncated; 37 | i += w; 38 | token = *remaining++; 39 | } 40 | return i; 41 | 42 | truncated: 43 | dst[n - 1] = '\0'; 44 | return n; 45 | } 46 | 47 | char * 48 | strquote(const char *src) { 49 | size_t len = strlen(src); 50 | char *quoted = SDL_malloc(len + 3); 51 | if (!quoted) { 52 | return NULL; 53 | } 54 | memcpy("ed[1], src, len); 55 | quoted[0] = '"'; 56 | quoted[len + 1] = '"'; 57 | quoted[len + 2] = '\0'; 58 | return quoted; 59 | } 60 | 61 | size_t 62 | utf8_truncation_index(const char *utf8, size_t max_len) { 63 | size_t len = strlen(utf8); 64 | if (len <= max_len) { 65 | return len; 66 | } 67 | len = max_len; 68 | // see UTF-8 encoding 69 | while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) { 70 | // the next byte is not the start of a new UTF-8 codepoint 71 | // so if we would cut there, the character would be truncated 72 | len--; 73 | } 74 | return len; 75 | } 76 | 77 | #ifdef _WIN32 78 | 79 | wchar_t * 80 | utf8_to_wide_char(const char *utf8) { 81 | int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); 82 | if (!len) { 83 | return NULL; 84 | } 85 | 86 | wchar_t *wide = SDL_malloc(len * sizeof(wchar_t)); 87 | if (!wide) { 88 | return NULL; 89 | } 90 | 91 | MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wide, len); 92 | return wide; 93 | } 94 | 95 | char * 96 | utf8_from_wide_char(const wchar_t *ws) { 97 | int len = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL); 98 | if (!len) { 99 | return NULL; 100 | } 101 | 102 | char *utf8 = SDL_malloc(len); 103 | if (!utf8) { 104 | return NULL; 105 | } 106 | 107 | WideCharToMultiByte(CP_UTF8, 0, ws, -1, utf8, len, NULL, NULL); 108 | return utf8; 109 | } 110 | 111 | #endif 112 | -------------------------------------------------------------------------------- /app/src/str_util.h: -------------------------------------------------------------------------------- 1 | #ifndef STRUTIL_H 2 | #define STRUTIL_H 3 | 4 | #include 5 | 6 | // like strncpy, except: 7 | // - it copies at most n-1 chars 8 | // - the dest string is nul-terminated 9 | // - it does not write useless bytes if strlen(src) < n 10 | // - it returns the number of chars actually written (max n-1) if src has 11 | // been copied completely, or n if src has been truncated 12 | size_t 13 | xstrncpy(char *dest, const char *src, size_t n); 14 | 15 | // join tokens by sep into dst 16 | // returns the number of chars actually written (max n-1) if no trucation 17 | // occurred, or n if truncated 18 | size_t 19 | xstrjoin(char *dst, const char *const tokens[], char sep, size_t n); 20 | 21 | // quote a string 22 | // returns the new allocated string, to be freed by the caller 23 | char * 24 | strquote(const char *src); 25 | 26 | // return the index to truncate a UTF-8 string at a valid position 27 | size_t 28 | utf8_truncation_index(const char *utf8, size_t max_len); 29 | 30 | #ifdef _WIN32 31 | // convert a UTF-8 string to a wchar_t string 32 | // returns the new allocated string, to be freed by the caller 33 | wchar_t * 34 | utf8_to_wide_char(const char *utf8); 35 | 36 | char * 37 | utf8_from_wide_char(const wchar_t *s); 38 | #endif 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /app/src/stream.h: -------------------------------------------------------------------------------- 1 | #ifndef STREAM_H 2 | #define STREAM_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "net.h" 11 | 12 | struct video_buffer; 13 | 14 | struct stream { 15 | socket_t socket; 16 | struct video_buffer *video_buffer; 17 | SDL_Thread *thread; 18 | struct decoder *decoder; 19 | struct recorder *recorder; 20 | AVCodecContext *codec_ctx; 21 | AVCodecParserContext *parser; 22 | // successive packets may need to be concatenated, until a non-config 23 | // packet is available 24 | bool has_pending; 25 | AVPacket pending; 26 | }; 27 | 28 | void 29 | stream_init(struct stream *stream, socket_t socket, 30 | struct decoder *decoder, struct recorder *recorder); 31 | 32 | bool 33 | stream_start(struct stream *stream); 34 | 35 | void 36 | stream_stop(struct stream *stream); 37 | 38 | void 39 | stream_join(struct stream *stream); 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /app/src/sys/unix/command.c: -------------------------------------------------------------------------------- 1 | // for portability 2 | #define _POSIX_SOURCE // for kill() 3 | #define _BSD_SOURCE // for readlink() 4 | 5 | // modern glibc will complain without this 6 | #define _DEFAULT_SOURCE 7 | 8 | #include "command.h" 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include "log.h" 19 | 20 | enum process_result 21 | cmd_execute(const char *path, const char *const argv[], pid_t *pid) { 22 | int fd[2]; 23 | 24 | if (pipe(fd) == -1) { 25 | perror("pipe"); 26 | return PROCESS_ERROR_GENERIC; 27 | } 28 | 29 | enum process_result ret = PROCESS_SUCCESS; 30 | 31 | *pid = fork(); 32 | if (*pid == -1) { 33 | perror("fork"); 34 | ret = PROCESS_ERROR_GENERIC; 35 | goto end; 36 | } 37 | 38 | if (*pid > 0) { 39 | // parent close write side 40 | close(fd[1]); 41 | fd[1] = -1; 42 | // wait for EOF or receive errno from child 43 | if (read(fd[0], &ret, sizeof(ret)) == -1) { 44 | perror("read"); 45 | ret = PROCESS_ERROR_GENERIC; 46 | goto end; 47 | } 48 | } else if (*pid == 0) { 49 | // child close read side 50 | close(fd[0]); 51 | if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) { 52 | execvp(path, (char *const *)argv); 53 | if (errno == ENOENT) { 54 | ret = PROCESS_ERROR_MISSING_BINARY; 55 | } else { 56 | ret = PROCESS_ERROR_GENERIC; 57 | } 58 | perror("exec"); 59 | } else { 60 | perror("fcntl"); 61 | ret = PROCESS_ERROR_GENERIC; 62 | } 63 | // send ret to the parent 64 | if (write(fd[1], &ret, sizeof(ret)) == -1) { 65 | perror("write"); 66 | } 67 | // close write side before exiting 68 | close(fd[1]); 69 | _exit(1); 70 | } 71 | 72 | end: 73 | if (fd[0] != -1) { 74 | close(fd[0]); 75 | } 76 | if (fd[1] != -1) { 77 | close(fd[1]); 78 | } 79 | return ret; 80 | } 81 | 82 | bool 83 | cmd_terminate(pid_t pid) { 84 | if (pid <= 0) { 85 | LOGC("Requested to kill %d, this is an error. Please report the bug.\n", 86 | (int) pid); 87 | abort(); 88 | } 89 | return kill(pid, SIGTERM) != -1; 90 | } 91 | 92 | bool 93 | cmd_simple_wait(pid_t pid, int *exit_code) { 94 | int status; 95 | int code; 96 | if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) { 97 | // could not wait, or exited unexpectedly, probably by a signal 98 | code = -1; 99 | } else { 100 | code = WEXITSTATUS(status); 101 | } 102 | if (exit_code) { 103 | *exit_code = code; 104 | } 105 | return !code; 106 | } 107 | 108 | char * 109 | get_executable_path(void) { 110 | // 111 | #ifdef __linux__ 112 | char buf[PATH_MAX + 1]; // +1 for the null byte 113 | ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX); 114 | if (len == -1) { 115 | perror("readlink"); 116 | return NULL; 117 | } 118 | buf[len] = '\0'; 119 | return SDL_strdup(buf); 120 | #else 121 | // in practice, we only need this feature for portable builds, only used on 122 | // Windows, so we don't care implementing it for every platform 123 | // (it's useful to have a working version on Linux for debugging though) 124 | return NULL; 125 | #endif 126 | } 127 | -------------------------------------------------------------------------------- /app/src/sys/unix/net.c: -------------------------------------------------------------------------------- 1 | #include "net.h" 2 | 3 | #include 4 | 5 | bool 6 | net_init(void) { 7 | // do nothing 8 | return true; 9 | } 10 | 11 | void 12 | net_cleanup(void) { 13 | // do nothing 14 | } 15 | 16 | bool 17 | net_close(socket_t socket) { 18 | return !close(socket); 19 | } 20 | -------------------------------------------------------------------------------- /app/src/sys/win/command.c: -------------------------------------------------------------------------------- 1 | #include "command.h" 2 | 3 | #include "config.h" 4 | #include "log.h" 5 | #include "str_util.h" 6 | 7 | static int 8 | build_cmd(char *cmd, size_t len, const char *const argv[]) { 9 | // Windows command-line parsing is WTF: 10 | // 11 | // only make it work for this very specific program 12 | // (don't handle escaping nor quotes) 13 | size_t ret = xstrjoin(cmd, argv, ' ', len); 14 | if (ret >= len) { 15 | LOGE("Command too long (%" PRIsizet " chars)", len - 1); 16 | return -1; 17 | } 18 | return 0; 19 | } 20 | 21 | enum process_result 22 | cmd_execute(const char *path, const char *const argv[], HANDLE *handle) { 23 | STARTUPINFOW si; 24 | PROCESS_INFORMATION pi; 25 | memset(&si, 0, sizeof(si)); 26 | si.cb = sizeof(si); 27 | 28 | char cmd[256]; 29 | if (build_cmd(cmd, sizeof(cmd), argv)) { 30 | *handle = NULL; 31 | return PROCESS_ERROR_GENERIC; 32 | } 33 | 34 | wchar_t *wide = utf8_to_wide_char(cmd); 35 | if (!wide) { 36 | LOGC("Could not allocate wide char string"); 37 | return PROCESS_ERROR_GENERIC; 38 | } 39 | 40 | #ifdef WINDOWS_NOCONSOLE 41 | int flags = CREATE_NO_WINDOW; 42 | #else 43 | int flags = 0; 44 | #endif 45 | if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si, 46 | &pi)) { 47 | SDL_free(wide); 48 | *handle = NULL; 49 | if (GetLastError() == ERROR_FILE_NOT_FOUND) { 50 | return PROCESS_ERROR_MISSING_BINARY; 51 | } 52 | return PROCESS_ERROR_GENERIC; 53 | } 54 | 55 | SDL_free(wide); 56 | *handle = pi.hProcess; 57 | return PROCESS_SUCCESS; 58 | } 59 | 60 | bool 61 | cmd_terminate(HANDLE handle) { 62 | return TerminateProcess(handle, 1) && CloseHandle(handle); 63 | } 64 | 65 | bool 66 | cmd_simple_wait(HANDLE handle, DWORD *exit_code) { 67 | DWORD code; 68 | if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 69 | || !GetExitCodeProcess(handle, &code)) { 70 | // could not wait or retrieve the exit code 71 | code = -1; // max value, it's unsigned 72 | } 73 | if (exit_code) { 74 | *exit_code = code; 75 | } 76 | return !code; 77 | } 78 | 79 | char * 80 | get_executable_path(void) { 81 | HMODULE hModule = GetModuleHandleW(NULL); 82 | if (!hModule) { 83 | return NULL; 84 | } 85 | WCHAR buf[MAX_PATH + 1]; // +1 for the null byte 86 | int len = GetModuleFileNameW(hModule, buf, MAX_PATH); 87 | if (!len) { 88 | return NULL; 89 | } 90 | buf[len] = '\0'; 91 | return utf8_from_wide_char(buf); 92 | } 93 | -------------------------------------------------------------------------------- /app/src/sys/win/net.c: -------------------------------------------------------------------------------- 1 | #include "net.h" 2 | 3 | #include "log.h" 4 | 5 | bool 6 | net_init(void) { 7 | WSADATA wsa; 8 | int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; 9 | if (res < 0) { 10 | LOGC("WSAStartup failed with error %d", res); 11 | return false; 12 | } 13 | return true; 14 | } 15 | 16 | void 17 | net_cleanup(void) { 18 | WSACleanup(); 19 | } 20 | 21 | bool 22 | net_close(socket_t socket) { 23 | return !closesocket(socket); 24 | } 25 | -------------------------------------------------------------------------------- /app/src/tiny_xpm.c: -------------------------------------------------------------------------------- 1 | #include "tiny_xpm.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "log.h" 9 | 10 | struct index { 11 | char c; 12 | uint32_t color; 13 | }; 14 | 15 | static bool 16 | find_color(struct index *index, int len, char c, uint32_t *color) { 17 | // there are typically very few color, so it's ok to iterate over the array 18 | for (int i = 0; i < len; ++i) { 19 | if (index[i].c == c) { 20 | *color = index[i].color; 21 | return true; 22 | } 23 | } 24 | *color = 0; 25 | return false; 26 | } 27 | 28 | // We encounter some problems with SDL2_image on MSYS2 (Windows), 29 | // so here is our own XPM parsing not to depend on SDL_image. 30 | // 31 | // We do not hardcode the binary image to keep some flexibility to replace the 32 | // icon easily (just by replacing icon.xpm). 33 | // 34 | // Parameter is not "const char *" because XPM formats are generally stored in a 35 | // (non-const) "char *" 36 | SDL_Surface * 37 | read_xpm(char *xpm[]) { 38 | #if SDL_ASSERT_LEVEL >= 2 39 | // patch the XPM to change the icon color in debug mode 40 | xpm[2] = ". c #CC00CC"; 41 | #endif 42 | 43 | char *endptr; 44 | // *** No error handling, assume the XPM source is valid *** 45 | // (it's in our source repo) 46 | // Assertions are only checked in debug 47 | int width = strtol(xpm[0], &endptr, 10); 48 | int height = strtol(endptr + 1, &endptr, 10); 49 | int colors = strtol(endptr + 1, &endptr, 10); 50 | int chars = strtol(endptr + 1, &endptr, 10); 51 | 52 | // sanity checks 53 | SDL_assert(0 <= width && width < 256); 54 | SDL_assert(0 <= height && height < 256); 55 | SDL_assert(0 <= colors && colors < 256); 56 | SDL_assert(chars == 1); // this implementation does not support more 57 | 58 | // init index 59 | struct index index[colors]; 60 | for (int i = 0; i < colors; ++i) { 61 | const char *line = xpm[1+i]; 62 | index[i].c = line[0]; 63 | SDL_assert(line[1] == '\t'); 64 | SDL_assert(line[2] == 'c'); 65 | SDL_assert(line[3] == ' '); 66 | if (line[4] == '#') { 67 | index[i].color = 0xff000000 | strtol(&line[5], &endptr, 0x10); 68 | SDL_assert(*endptr == '\0'); 69 | } else { 70 | SDL_assert(!strcmp("None", &line[4])); 71 | index[i].color = 0; 72 | } 73 | } 74 | 75 | // parse image 76 | uint32_t *pixels = SDL_malloc(4 * width * height); 77 | if (!pixels) { 78 | LOGE("Could not allocate icon memory"); 79 | return NULL; 80 | } 81 | for (int y = 0; y < height; ++y) { 82 | const char *line = xpm[1 + colors + y]; 83 | for (int x = 0; x < width; ++x) { 84 | char c = line[x]; 85 | uint32_t color; 86 | bool color_found = find_color(index, colors, c, &color); 87 | SDL_assert(color_found); 88 | pixels[y * width + x] = color; 89 | } 90 | } 91 | 92 | #if SDL_BYTEORDER == SDL_BIG_ENDIAN 93 | uint32_t amask = 0x000000ff; 94 | uint32_t rmask = 0x0000ff00; 95 | uint32_t gmask = 0x00ff0000; 96 | uint32_t bmask = 0xff000000; 97 | #else // little endian, like x86 98 | uint32_t amask = 0xff000000; 99 | uint32_t rmask = 0x00ff0000; 100 | uint32_t gmask = 0x0000ff00; 101 | uint32_t bmask = 0x000000ff; 102 | #endif 103 | 104 | SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(pixels, 105 | width, height, 106 | 32, 4 * width, 107 | rmask, gmask, bmask, amask); 108 | if (!surface) { 109 | LOGE("Could not create icon surface"); 110 | return NULL; 111 | } 112 | // make the surface own the raw pixels 113 | surface->flags &= ~SDL_PREALLOC; 114 | return surface; 115 | } 116 | -------------------------------------------------------------------------------- /app/src/tiny_xpm.h: -------------------------------------------------------------------------------- 1 | #ifndef TINYXPM_H 2 | #define TINYXPM_H 3 | 4 | #include 5 | 6 | SDL_Surface * 7 | read_xpm(char *xpm[]); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /app/src/video_buffer.c: -------------------------------------------------------------------------------- 1 | #include "video_buffer.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "config.h" 9 | #include "lock_util.h" 10 | #include "log.h" 11 | 12 | bool 13 | video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, 14 | bool render_expired_frames) { 15 | vb->fps_counter = fps_counter; 16 | 17 | if (!(vb->decoding_frame = av_frame_alloc())) { 18 | goto error_0; 19 | } 20 | 21 | if (!(vb->rendering_frame = av_frame_alloc())) { 22 | goto error_1; 23 | } 24 | 25 | if (!(vb->mutex = SDL_CreateMutex())) { 26 | goto error_2; 27 | } 28 | 29 | vb->render_expired_frames = render_expired_frames; 30 | if (render_expired_frames) { 31 | if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) { 32 | SDL_DestroyMutex(vb->mutex); 33 | goto error_2; 34 | } 35 | // interrupted is not used if expired frames are not rendered 36 | // since offering a frame will never block 37 | vb->interrupted = false; 38 | } 39 | 40 | // there is initially no rendering frame, so consider it has already been 41 | // consumed 42 | vb->rendering_frame_consumed = true; 43 | 44 | return true; 45 | 46 | error_2: 47 | av_frame_free(&vb->rendering_frame); 48 | error_1: 49 | av_frame_free(&vb->decoding_frame); 50 | error_0: 51 | return false; 52 | } 53 | 54 | void 55 | video_buffer_destroy(struct video_buffer *vb) { 56 | if (vb->render_expired_frames) { 57 | SDL_DestroyCond(vb->rendering_frame_consumed_cond); 58 | } 59 | SDL_DestroyMutex(vb->mutex); 60 | av_frame_free(&vb->rendering_frame); 61 | av_frame_free(&vb->decoding_frame); 62 | } 63 | 64 | static void 65 | video_buffer_swap_frames(struct video_buffer *vb) { 66 | AVFrame *tmp = vb->decoding_frame; 67 | vb->decoding_frame = vb->rendering_frame; 68 | vb->rendering_frame = tmp; 69 | } 70 | 71 | void 72 | video_buffer_offer_decoded_frame(struct video_buffer *vb, 73 | bool *previous_frame_skipped) { 74 | mutex_lock(vb->mutex); 75 | if (vb->render_expired_frames) { 76 | // wait for the current (expired) frame to be consumed 77 | while (!vb->rendering_frame_consumed && !vb->interrupted) { 78 | cond_wait(vb->rendering_frame_consumed_cond, vb->mutex); 79 | } 80 | } else if (!vb->rendering_frame_consumed) { 81 | fps_counter_add_skipped_frame(vb->fps_counter); 82 | } 83 | 84 | video_buffer_swap_frames(vb); 85 | 86 | *previous_frame_skipped = !vb->rendering_frame_consumed; 87 | vb->rendering_frame_consumed = false; 88 | 89 | mutex_unlock(vb->mutex); 90 | } 91 | 92 | const AVFrame * 93 | video_buffer_consume_rendered_frame(struct video_buffer *vb) { 94 | SDL_assert(!vb->rendering_frame_consumed); 95 | vb->rendering_frame_consumed = true; 96 | fps_counter_add_rendered_frame(vb->fps_counter); 97 | if (vb->render_expired_frames) { 98 | // unblock video_buffer_offer_decoded_frame() 99 | cond_signal(vb->rendering_frame_consumed_cond); 100 | } 101 | return vb->rendering_frame; 102 | } 103 | 104 | void 105 | video_buffer_interrupt(struct video_buffer *vb) { 106 | if (vb->render_expired_frames) { 107 | mutex_lock(vb->mutex); 108 | vb->interrupted = true; 109 | mutex_unlock(vb->mutex); 110 | // wake up blocking wait 111 | cond_signal(vb->rendering_frame_consumed_cond); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/src/video_buffer.h: -------------------------------------------------------------------------------- 1 | #ifndef VIDEO_BUFFER_H 2 | #define VIDEO_BUFFER_H 3 | 4 | #include 5 | #include 6 | 7 | #include "fps_counter.h" 8 | 9 | // forward declarations 10 | typedef struct AVFrame AVFrame; 11 | 12 | struct video_buffer { 13 | AVFrame *decoding_frame; 14 | AVFrame *rendering_frame; 15 | SDL_mutex *mutex; 16 | bool render_expired_frames; 17 | bool interrupted; 18 | SDL_cond *rendering_frame_consumed_cond; 19 | bool rendering_frame_consumed; 20 | struct fps_counter *fps_counter; 21 | }; 22 | 23 | bool 24 | video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, 25 | bool render_expired_frames); 26 | 27 | void 28 | video_buffer_destroy(struct video_buffer *vb); 29 | 30 | // set the decoded frame as ready for rendering 31 | // this function locks frames->mutex during its execution 32 | // the output flag is set to report whether the previous frame has been skipped 33 | void 34 | video_buffer_offer_decoded_frame(struct video_buffer *vb, 35 | bool *previous_frame_skipped); 36 | 37 | // mark the rendering frame as consumed and return it 38 | // MUST be called with frames->mutex locked!!! 39 | // the caller is expected to render the returned frame to some texture before 40 | // unlocking frames->mutex 41 | const AVFrame * 42 | video_buffer_consume_rendered_frame(struct video_buffer *vb); 43 | 44 | // wake up and avoid any blocking call 45 | void 46 | video_buffer_interrupt(struct video_buffer *vb); 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /app/tests/test_cbuf.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "cbuf.h" 5 | 6 | struct int_queue CBUF(int, 32); 7 | 8 | static void test_cbuf_empty(void) { 9 | struct int_queue queue; 10 | cbuf_init(&queue); 11 | 12 | assert(cbuf_is_empty(&queue)); 13 | 14 | bool push_ok = cbuf_push(&queue, 42); 15 | assert(push_ok); 16 | assert(!cbuf_is_empty(&queue)); 17 | 18 | int item; 19 | bool take_ok = cbuf_take(&queue, &item); 20 | assert(take_ok); 21 | assert(cbuf_is_empty(&queue)); 22 | 23 | bool take_empty_ok = cbuf_take(&queue, &item); 24 | assert(!take_empty_ok); // the queue is empty 25 | } 26 | 27 | static void test_cbuf_full(void) { 28 | struct int_queue queue; 29 | cbuf_init(&queue); 30 | 31 | assert(!cbuf_is_full(&queue)); 32 | 33 | // fill the queue 34 | for (int i = 0; i < 32; ++i) { 35 | bool ok = cbuf_push(&queue, i); 36 | assert(ok); 37 | } 38 | bool ok = cbuf_push(&queue, 42); 39 | assert(!ok); // the queue if full 40 | 41 | int item; 42 | bool take_ok = cbuf_take(&queue, &item); 43 | assert(take_ok); 44 | assert(!cbuf_is_full(&queue)); 45 | } 46 | 47 | static void test_cbuf_push_take(void) { 48 | struct int_queue queue; 49 | cbuf_init(&queue); 50 | 51 | bool push1_ok = cbuf_push(&queue, 42); 52 | assert(push1_ok); 53 | 54 | bool push2_ok = cbuf_push(&queue, 35); 55 | assert(push2_ok); 56 | 57 | int item; 58 | 59 | bool take1_ok = cbuf_take(&queue, &item); 60 | assert(take1_ok); 61 | assert(item == 42); 62 | 63 | bool take2_ok = cbuf_take(&queue, &item); 64 | assert(take2_ok); 65 | assert(item == 35); 66 | } 67 | 68 | int main(void) { 69 | test_cbuf_empty(); 70 | test_cbuf_full(); 71 | test_cbuf_push_take(); 72 | return 0; 73 | } 74 | -------------------------------------------------------------------------------- /app/tests/test_control_msg_serialize.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "control_msg.h" 5 | 6 | static void test_serialize_inject_keycode(void) { 7 | struct control_msg msg = { 8 | .type = CONTROL_MSG_TYPE_INJECT_KEYCODE, 9 | .inject_keycode = { 10 | .action = AKEY_EVENT_ACTION_UP, 11 | .keycode = AKEYCODE_ENTER, 12 | .metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON, 13 | }, 14 | }; 15 | 16 | unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; 17 | int size = control_msg_serialize(&msg, buf); 18 | assert(size == 10); 19 | 20 | const unsigned char expected[] = { 21 | CONTROL_MSG_TYPE_INJECT_KEYCODE, 22 | 0x01, // AKEY_EVENT_ACTION_UP 23 | 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER 24 | 0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON 25 | }; 26 | assert(!memcmp(buf, expected, sizeof(expected))); 27 | } 28 | 29 | static void test_serialize_inject_text(void) { 30 | struct control_msg msg = { 31 | .type = CONTROL_MSG_TYPE_INJECT_TEXT, 32 | .inject_text = { 33 | .text = "hello, world!", 34 | }, 35 | }; 36 | 37 | unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; 38 | int size = control_msg_serialize(&msg, buf); 39 | assert(size == 16); 40 | 41 | const unsigned char expected[] = { 42 | CONTROL_MSG_TYPE_INJECT_TEXT, 43 | 0x00, 0x0d, // text length 44 | 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text 45 | }; 46 | assert(!memcmp(buf, expected, sizeof(expected))); 47 | } 48 | 49 | static void test_serialize_inject_text_long(void) { 50 | struct control_msg msg; 51 | msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; 52 | char text[CONTROL_MSG_TEXT_MAX_LENGTH + 1]; 53 | memset(text, 'a', sizeof(text)); 54 | text[CONTROL_MSG_TEXT_MAX_LENGTH] = '\0'; 55 | msg.inject_text.text = text; 56 | 57 | unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; 58 | int size = control_msg_serialize(&msg, buf); 59 | assert(size == 3 + CONTROL_MSG_TEXT_MAX_LENGTH); 60 | 61 | unsigned char expected[3 + CONTROL_MSG_TEXT_MAX_LENGTH]; 62 | expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT; 63 | expected[1] = 0x01; 64 | expected[2] = 0x2c; // text length (16 bits) 65 | memset(&expected[3], 'a', CONTROL_MSG_TEXT_MAX_LENGTH); 66 | 67 | assert(!memcmp(buf, expected, sizeof(expected))); 68 | } 69 | 70 | static void test_serialize_inject_mouse_event(void) { 71 | struct control_msg msg = { 72 | .type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, 73 | .inject_mouse_event = { 74 | .action = AMOTION_EVENT_ACTION_DOWN, 75 | .buttons = AMOTION_EVENT_BUTTON_PRIMARY, 76 | .position = { 77 | .point = { 78 | .x = 260, 79 | .y = 1026, 80 | }, 81 | .screen_size = { 82 | .width = 1080, 83 | .height = 1920, 84 | }, 85 | }, 86 | }, 87 | }; 88 | 89 | unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; 90 | int size = control_msg_serialize(&msg, buf); 91 | assert(size == 18); 92 | 93 | const unsigned char expected[] = { 94 | CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, 95 | 0x00, // AKEY_EVENT_ACTION_DOWN 96 | 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY 97 | 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 98 | 0x04, 0x38, 0x07, 0x80, // 1080 1920 99 | }; 100 | assert(!memcmp(buf, expected, sizeof(expected))); 101 | } 102 | 103 | static void test_serialize_inject_scroll_event(void) { 104 | struct control_msg msg = { 105 | .type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, 106 | .inject_scroll_event = { 107 | .position = { 108 | .point = { 109 | .x = 260, 110 | .y = 1026, 111 | }, 112 | .screen_size = { 113 | .width = 1080, 114 | .height = 1920, 115 | }, 116 | }, 117 | .hscroll = 1, 118 | .vscroll = -1, 119 | }, 120 | }; 121 | 122 | unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; 123 | int size = control_msg_serialize(&msg, buf); 124 | assert(size == 21); 125 | 126 | const unsigned char expected[] = { 127 | CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, 128 | 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 129 | 0x04, 0x38, 0x07, 0x80, // 1080 1920 130 | 0x00, 0x00, 0x00, 0x01, // 1 131 | 0xFF, 0xFF, 0xFF, 0xFF, // -1 132 | }; 133 | assert(!memcmp(buf, expected, sizeof(expected))); 134 | } 135 | 136 | static void test_serialize_get_clipboard(void) { 137 | struct control_msg msg = { 138 | .type = CONTROL_MSG_TYPE_COMMAND, 139 | .command_event.action = CONTROL_COMMAND_GET_CLIPBOARD, 140 | }; 141 | 142 | unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; 143 | int size = control_msg_serialize(&msg, buf); 144 | assert(size == 2); 145 | 146 | const unsigned char expected[] = { 147 | CONTROL_MSG_TYPE_COMMAND, CONTROL_COMMAND_GET_CLIPBOARD 148 | }; 149 | assert(!memcmp(buf, expected, sizeof(expected))); 150 | } 151 | 152 | static void test_serialize_set_clipboard(void) { 153 | struct control_msg msg = { 154 | .type = CONTROL_MSG_TYPE_SET_CLIPBOARD, 155 | .inject_text = { 156 | .text = "hello, world!", 157 | }, 158 | }; 159 | 160 | unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; 161 | int size = control_msg_serialize(&msg, buf); 162 | assert(size == 16); 163 | 164 | const unsigned char expected[] = { 165 | CONTROL_MSG_TYPE_SET_CLIPBOARD, 166 | 0x00, 0x0d, // text length 167 | 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text 168 | }; 169 | assert(!memcmp(buf, expected, sizeof(expected))); 170 | } 171 | 172 | static void test_serialize_set_screen_power_mode(void) { 173 | struct control_msg msg = { 174 | .type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, 175 | .set_screen_power_mode = { 176 | .mode = SCREEN_POWER_MODE_NORMAL, 177 | }, 178 | }; 179 | 180 | unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; 181 | int size = control_msg_serialize(&msg, buf); 182 | assert(size == 2); 183 | 184 | const unsigned char expected[] = { 185 | CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, 186 | 0x02, // SCREEN_POWER_MODE_NORMAL 187 | }; 188 | assert(!memcmp(buf, expected, sizeof(expected))); 189 | } 190 | 191 | int main(void) { 192 | test_serialize_inject_keycode(); 193 | test_serialize_inject_text(); 194 | test_serialize_inject_text_long(); 195 | test_serialize_inject_mouse_event(); 196 | test_serialize_inject_scroll_event(); 197 | test_serialize_get_clipboard(); 198 | test_serialize_set_clipboard(); 199 | test_serialize_set_screen_power_mode(); 200 | return 0; 201 | } 202 | -------------------------------------------------------------------------------- /app/tests/test_device_msg_deserialize.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "device_msg.h" 5 | 6 | #include 7 | static void test_deserialize_clipboard(void) { 8 | const unsigned char input[] = { 9 | DEVICE_MSG_TYPE_CLIPBOARD, 10 | 0x00, 0x03, // text length 11 | 0x41, 0x42, 0x43, // "ABC" 12 | }; 13 | 14 | struct device_msg msg; 15 | ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); 16 | assert(r == 6); 17 | 18 | assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); 19 | assert(msg.clipboard.text); 20 | assert(!strcmp("ABC", msg.clipboard.text)); 21 | 22 | device_msg_destroy(&msg); 23 | } 24 | 25 | int main(void) { 26 | test_deserialize_clipboard(); 27 | return 0; 28 | } 29 | -------------------------------------------------------------------------------- /app/tests/test_queue.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | struct foo { 6 | int value; 7 | struct foo *next; 8 | }; 9 | 10 | static void test_queue(void) { 11 | struct my_queue QUEUE(struct foo) queue; 12 | queue_init(&queue); 13 | 14 | assert(queue_is_empty(&queue)); 15 | 16 | struct foo v1 = { .value = 42 }; 17 | struct foo v2 = { .value = 27 }; 18 | 19 | queue_push(&queue, next, &v1); 20 | queue_push(&queue, next, &v2); 21 | 22 | struct foo *foo; 23 | 24 | assert(!queue_is_empty(&queue)); 25 | queue_take(&queue, next, &foo); 26 | assert(foo->value == 42); 27 | 28 | assert(!queue_is_empty(&queue)); 29 | queue_take(&queue, next, &foo); 30 | assert(foo->value == 27); 31 | 32 | assert(queue_is_empty(&queue)); 33 | } 34 | 35 | int main(void) { 36 | test_queue(); 37 | return 0; 38 | } 39 | -------------------------------------------------------------------------------- /app/tests/test_strutil.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "str_util.h" 5 | 6 | static void test_xstrncpy_simple(void) { 7 | char s[] = "xxxxxxxxxx"; 8 | size_t w = xstrncpy(s, "abcdef", sizeof(s)); 9 | 10 | // returns strlen of copied string 11 | assert(w == 6); 12 | 13 | // is nul-terminated 14 | assert(s[6] == '\0'); 15 | 16 | // does not write useless bytes 17 | assert(s[7] == 'x'); 18 | 19 | // copies the content as expected 20 | assert(!strcmp("abcdef", s)); 21 | } 22 | 23 | static void test_xstrncpy_just_fit(void) { 24 | char s[] = "xxxxxx"; 25 | size_t w = xstrncpy(s, "abcdef", sizeof(s)); 26 | 27 | // returns strlen of copied string 28 | assert(w == 6); 29 | 30 | // is nul-terminated 31 | assert(s[6] == '\0'); 32 | 33 | // copies the content as expected 34 | assert(!strcmp("abcdef", s)); 35 | } 36 | 37 | static void test_xstrncpy_truncated(void) { 38 | char s[] = "xxx"; 39 | size_t w = xstrncpy(s, "abcdef", sizeof(s)); 40 | 41 | // returns 'n' (sizeof(s)) 42 | assert(w == 4); 43 | 44 | // is nul-terminated 45 | assert(s[3] == '\0'); 46 | 47 | // copies the content as expected 48 | assert(!strncmp("abcdef", s, 3)); 49 | } 50 | 51 | static void test_xstrjoin_simple(void) { 52 | const char *const tokens[] = { "abc", "de", "fghi", NULL }; 53 | char s[] = "xxxxxxxxxxxxxx"; 54 | size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); 55 | 56 | // returns strlen of concatenation 57 | assert(w == 11); 58 | 59 | // is nul-terminated 60 | assert(s[11] == '\0'); 61 | 62 | // does not write useless bytes 63 | assert(s[12] == 'x'); 64 | 65 | // copies the content as expected 66 | assert(!strcmp("abc de fghi", s)); 67 | } 68 | 69 | static void test_xstrjoin_just_fit(void) { 70 | const char *const tokens[] = { "abc", "de", "fghi", NULL }; 71 | char s[] = "xxxxxxxxxxx"; 72 | size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); 73 | 74 | // returns strlen of concatenation 75 | assert(w == 11); 76 | 77 | // is nul-terminated 78 | assert(s[11] == '\0'); 79 | 80 | // copies the content as expected 81 | assert(!strcmp("abc de fghi", s)); 82 | } 83 | 84 | static void test_xstrjoin_truncated_in_token(void) { 85 | const char *const tokens[] = { "abc", "de", "fghi", NULL }; 86 | char s[] = "xxxxx"; 87 | size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); 88 | 89 | // returns 'n' (sizeof(s)) 90 | assert(w == 6); 91 | 92 | // is nul-terminated 93 | assert(s[5] == '\0'); 94 | 95 | // copies the content as expected 96 | assert(!strcmp("abc d", s)); 97 | } 98 | 99 | static void test_xstrjoin_truncated_before_sep(void) { 100 | const char *const tokens[] = { "abc", "de", "fghi", NULL }; 101 | char s[] = "xxxxxx"; 102 | size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); 103 | 104 | // returns 'n' (sizeof(s)) 105 | assert(w == 7); 106 | 107 | // is nul-terminated 108 | assert(s[6] == '\0'); 109 | 110 | // copies the content as expected 111 | assert(!strcmp("abc de", s)); 112 | } 113 | 114 | static void test_xstrjoin_truncated_after_sep(void) { 115 | const char *const tokens[] = { "abc", "de", "fghi", NULL }; 116 | char s[] = "xxxxxxx"; 117 | size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); 118 | 119 | // returns 'n' (sizeof(s)) 120 | assert(w == 8); 121 | 122 | // is nul-terminated 123 | assert(s[7] == '\0'); 124 | 125 | // copies the content as expected 126 | assert(!strcmp("abc de ", s)); 127 | } 128 | 129 | static void test_utf8_truncate(void) { 130 | const char *s = "aÉbÔc"; 131 | assert(strlen(s) == 7); // É and Ô are 2 bytes-wide 132 | 133 | size_t count; 134 | 135 | count = utf8_truncation_index(s, 1); 136 | assert(count == 1); 137 | 138 | count = utf8_truncation_index(s, 2); 139 | assert(count == 1); // É is 2 bytes-wide 140 | 141 | count = utf8_truncation_index(s, 3); 142 | assert(count == 3); 143 | 144 | count = utf8_truncation_index(s, 4); 145 | assert(count == 4); 146 | 147 | count = utf8_truncation_index(s, 5); 148 | assert(count == 4); // Ô is 2 bytes-wide 149 | 150 | count = utf8_truncation_index(s, 6); 151 | assert(count == 6); 152 | 153 | count = utf8_truncation_index(s, 7); 154 | assert(count == 7); 155 | 156 | count = utf8_truncation_index(s, 8); 157 | assert(count == 7); // no more chars 158 | } 159 | 160 | int main(void) { 161 | test_xstrncpy_simple(); 162 | test_xstrncpy_just_fit(); 163 | test_xstrncpy_truncated(); 164 | test_xstrjoin_simple(); 165 | test_xstrjoin_just_fit(); 166 | test_xstrjoin_truncated_in_token(); 167 | test_xstrjoin_truncated_before_sep(); 168 | test_xstrjoin_truncated_after_sep(); 169 | test_utf8_truncate(); 170 | return 0; 171 | } 172 | -------------------------------------------------------------------------------- /assets/screenshot-debian-600.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lurker00/scrcpy/60e5d327193506ba3992ae1ade712e41afc65964/assets/screenshot-debian-600.jpg -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.4.2' 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | } 22 | } 23 | 24 | task clean(type: Delete) { 25 | delete rootProject.buildDir 26 | } 27 | -------------------------------------------------------------------------------- /config/android-checkstyle.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'checkstyle' 2 | check.dependsOn 'checkstyle' 3 | 4 | checkstyle { 5 | toolVersion = '6.19' 6 | } 7 | 8 | task checkstyle(type: Checkstyle) { 9 | description = "Check Java style with Checkstyle" 10 | configFile = rootProject.file("config/checkstyle/checkstyle.xml") 11 | source = javaSources() 12 | classpath = files() 13 | ignoreFailures = true 14 | } 15 | 16 | def javaSources() { 17 | def files = [] 18 | android.sourceSets.each { sourceSet -> 19 | sourceSet.java.each { javaSource -> 20 | javaSource.getSrcDirs().each { 21 | if (it.exists()) { 22 | files.add(it) 23 | } 24 | } 25 | } 26 | } 27 | return files 28 | } 29 | -------------------------------------------------------------------------------- /cross_win32.txt: -------------------------------------------------------------------------------- 1 | # apt install mingw-w64 mingw-w64-tools 2 | 3 | [binaries] 4 | name = 'mingw' 5 | c = '/usr/bin/i686-w64-mingw32-gcc' 6 | cpp = '/usr/bin/i686-w64-mingw32-g++' 7 | ar = '/usr/bin/i686-w64-mingw32-ar' 8 | strip = '/usr/bin/i686-w64-mingw32-strip' 9 | pkgconfig = '/usr/bin/i686-w64-mingw32-pkg-config' 10 | 11 | [host_machine] 12 | system = 'windows' 13 | cpu_family = 'x86' 14 | cpu = 'i686' 15 | endian = 'little' 16 | 17 | [properties] 18 | prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win32-shared' 19 | prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win32-dev' 20 | prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32' 21 | -------------------------------------------------------------------------------- /cross_win64.txt: -------------------------------------------------------------------------------- 1 | # apt install mingw-w64 mingw-w64-tools 2 | 3 | [binaries] 4 | name = 'mingw' 5 | c = '/usr/bin/x86_64-w64-mingw32-gcc' 6 | cpp = '/usr/bin/x86_64-w64-mingw32-g++' 7 | ar = '/usr/bin/x86_64-w64-mingw32-ar' 8 | strip = '/usr/bin/x86_64-w64-mingw32-strip' 9 | pkgconfig = '/usr/bin/x86_64-w64-mingw32-pkg-config' 10 | 11 | [host_machine] 12 | system = 'windows' 13 | cpu_family = 'x86' 14 | cpu = 'x86_64' 15 | endian = 'little' 16 | 17 | [properties] 18 | prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win64-shared' 19 | prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win64-dev' 20 | prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32' 21 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lurker00/scrcpy/60e5d327193506ba3992ae1ade712e41afc65964/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('scrcpy', 'c', 2 | version: '1.10', 3 | meson_version: '>= 0.37', 4 | default_options: 'c_std=c11') 5 | 6 | if get_option('compile_app') 7 | subdir('app') 8 | endif 9 | 10 | if get_option('compile_server') 11 | subdir('server') 12 | endif 13 | 14 | run_target('run', command: ['scripts/run-scrcpy.sh']) 15 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('compile_app', type: 'boolean', value: true, description: 'Build the client') 2 | option('compile_server', type: 'boolean', value: true, description: 'Build the server') 3 | option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') 4 | option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') 5 | option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') 6 | option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable') 7 | option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') 8 | -------------------------------------------------------------------------------- /prebuilt-deps/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !/.gitignore 3 | !/Makefile 4 | !/prepare-dep 5 | -------------------------------------------------------------------------------- /prebuilt-deps/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: prepare-win32 prepare-win64 \ 2 | prepare-ffmpeg-shared-win32 \ 3 | prepare-ffmpeg-dev-win32 \ 4 | prepare-ffmpeg-shared-win64 \ 5 | prepare-ffmpeg-dev-win64 \ 6 | prepare-sdl2 \ 7 | prepare-adb 8 | 9 | prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-adb 10 | prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb 11 | 12 | prepare-ffmpeg-shared-win32: 13 | @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1.4-win32-shared.zip \ 14 | 596608277f6b937c3dea7c46e854665d75b3de56790bae07f655ca331440f003 \ 15 | ffmpeg-4.1.4-win32-shared 16 | 17 | prepare-ffmpeg-dev-win32: 18 | @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.4-win32-dev.zip \ 19 | a80c86e263cfad26e202edfa5e6e939a2c88843ae26f031d3e0d981a39fd03fb \ 20 | ffmpeg-4.1.4-win32-dev 21 | 22 | prepare-ffmpeg-shared-win64: 23 | @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.4-win64-shared.zip \ 24 | a90889871de2cab8a79b392591313a188189a353f69dde1db98aebe20b280989 \ 25 | ffmpeg-4.1.4-win64-shared 26 | 27 | prepare-ffmpeg-dev-win64: 28 | @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.4-win64-dev.zip \ 29 | 6c9d53f9e94ce1821e975ec668e5b9d6e9deb4a45d0d7e30264685d3dfbbb068 \ 30 | ffmpeg-4.1.4-win64-dev 31 | 32 | prepare-sdl2: 33 | @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \ 34 | a90a7cddaec4996f4d7be6d80c57ec69b062e132bffc513965f99217f603274a \ 35 | SDL2-2.0.10 36 | 37 | prepare-adb: 38 | @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.2-windows.zip \ 39 | d78f02e5e2c9c4c1d046dcd4e6fbdf586e5f57ef66eb0da5c2b49d745d85d5ee \ 40 | platform-tools 41 | -------------------------------------------------------------------------------- /prebuilt-deps/prepare-dep: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | url="$1" 4 | sum="$2" 5 | dir="$3" 6 | 7 | checksum() { 8 | local file="$1" 9 | local sum="$2" 10 | echo "$file: verifying checksum..." 11 | echo "$sum $file" | sha256sum -c 12 | } 13 | 14 | get_file() { 15 | local url="$1" 16 | local file="$2" 17 | local sum="$3" 18 | if [[ -f "$file" ]] 19 | then 20 | echo "$file: found" 21 | else 22 | echo "$file: not found, downloading..." 23 | wget "$url" -O "$file" 24 | fi 25 | checksum "$file" "$sum" 26 | } 27 | 28 | extract() { 29 | local file="$1" 30 | echo "Extracting $file..." 31 | if [[ "$file" == *.zip ]] 32 | then 33 | unzip -q "$file" 34 | elif [[ "$file" == *.tar.gz ]] 35 | then 36 | tar xf "$file" 37 | else 38 | echo "Unsupported file: $file" 39 | return 1 40 | fi 41 | } 42 | 43 | get_dep() { 44 | local url="$1" 45 | local sum="$2" 46 | local dir="$3" 47 | local file="${url##*/}" 48 | if [[ -d "$dir" ]] 49 | then 50 | echo "$dir: found" 51 | else 52 | echo "$dir: not found" 53 | get_file "$url" "$file" "$sum" 54 | extract "$file" 55 | fi 56 | } 57 | 58 | get_dep "$url" "$sum" "$dir" 59 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # test locally 5 | TESTDIR=build_test 6 | rm -rf "$TESTDIR" 7 | # run client tests with ASAN enabled 8 | meson "$TESTDIR" -Db_sanitize=address 9 | ninja -C"$TESTDIR" test 10 | 11 | # test server 12 | GRADLE=${GRADLE:-./gradlew} 13 | $GRADLE -p server check 14 | 15 | BUILDDIR=build_release 16 | rm -rf "$BUILDDIR" 17 | meson "$BUILDDIR" --buildtype release --strip -Db_lto=true 18 | cd "$BUILDDIR" 19 | ninja 20 | cd - 21 | 22 | # build Windows releases 23 | make -f Makefile.CrossWindows 24 | 25 | # the generated server must be the same everywhere 26 | cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win32/scrcpy-server.jar 27 | cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win64/scrcpy-server.jar 28 | 29 | # get version name 30 | TAG=$(git describe --tags --always) 31 | 32 | # create release directory 33 | mkdir -p "release-$TAG" 34 | cp "$BUILDDIR/server/scrcpy-server.jar" "release-$TAG/scrcpy-server-$TAG.jar" 35 | cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/" 36 | cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/" 37 | 38 | # generate checksums 39 | cd "release-$TAG" 40 | sha256sum "scrcpy-server-$TAG.jar" \ 41 | "scrcpy-win32-$TAG.zip" \ 42 | "scrcpy-win64-$TAG.zip" > SHA256SUMS.txt 43 | 44 | echo "Release generated in release-$TAG/" 45 | -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Run scrcpy generated in the specified BUILDDIR. 3 | # 4 | # This provides the same feature as "ninja run", except that it is possible to 5 | # pass arguments to scrcpy. 6 | # 7 | # Syntax: ./run BUILDDIR 8 | if [[ $# = 0 ]] 9 | then 10 | echo "Syntax: $0 BUILDDIR " >&2 11 | exit 1 12 | fi 13 | 14 | BUILDDIR="$1" 15 | shift 16 | 17 | if [[ ! -d "$BUILDDIR" ]] 18 | then 19 | echo "The build dir \"$BUILDDIR\" does not exist." >&2 20 | exit 1 21 | fi 22 | 23 | SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server.jar" "$BUILDDIR/app/scrcpy" "$@" 24 | -------------------------------------------------------------------------------- /scripts/run-scrcpy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server.jar" "$MESON_BUILD_ROOT/app/scrcpy" 3 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /server/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 29 5 | defaultConfig { 6 | applicationId "com.genymobile.scrcpy" 7 | minSdkVersion 21 8 | targetSdkVersion 29 9 | versionCode 11 10 | versionName "1.10" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | testImplementation 'junit:junit:4.12' 24 | } 25 | 26 | apply from: "$project.rootDir/config/android-checkstyle.gradle" 27 | -------------------------------------------------------------------------------- /server/meson.build: -------------------------------------------------------------------------------- 1 | # It may be useful to use a prebuilt server, so that no Android SDK is required 2 | # to build. If the 'prebuilt_server' option is set, just copy the file as is. 3 | prebuilt_server = get_option('prebuilt_server') 4 | if prebuilt_server == '' 5 | custom_target('scrcpy-server', 6 | build_always: true, # gradle is responsible for tracking source changes 7 | output: 'scrcpy-server.jar', 8 | command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], 9 | console: true, 10 | install: true, 11 | install_dir: 'share/scrcpy') 12 | else 13 | if not prebuilt_server.startswith('/') 14 | # relative path needs some trick 15 | prebuilt_server = meson.source_root() + '/' + prebuilt_server 16 | endif 17 | custom_target('scrcpy-server-prebuilt', 18 | input: prebuilt_server, 19 | output: 'scrcpy-server.jar', 20 | command: ['cp', '@INPUT@', '@OUTPUT@'], 21 | install: true, 22 | install_dir: 'share/scrcpy') 23 | endif 24 | -------------------------------------------------------------------------------- /server/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /server/scripts/build-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Wrapper script to invoke gradle from meson 3 | set -e 4 | 5 | # Do not execute gradle when ninja is called as root (it would download the 6 | # whole gradle world in /root/.gradle). 7 | # This is typically useful for calling "sudo ninja install" after a "ninja 8 | # install" 9 | if [[ "$EUID" == 0 ]] 10 | then 11 | echo "(not invoking gradle, since we are root)" >&2 12 | exit 0 13 | fi 14 | 15 | PROJECT_ROOT="$1" 16 | OUTPUT="$2" 17 | BUILDTYPE="$3" 18 | 19 | # gradlew is in the parent of the server directory 20 | GRADLE=${GRADLE:-$PROJECT_ROOT/../gradlew} 21 | 22 | if [[ "$BUILDTYPE" == debug ]] 23 | then 24 | "$GRADLE" -p "$PROJECT_ROOT" assembleDebug 25 | cp "$PROJECT_ROOT/build/outputs/apk/debug/server-debug.apk" "$OUTPUT" 26 | else 27 | "$GRADLE" -p "$PROJECT_ROOT" assembleRelease 28 | cp "$PROJECT_ROOT/build/outputs/apk/release/server-release-unsigned.apk" "$OUTPUT" 29 | fi 30 | -------------------------------------------------------------------------------- /server/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /server/src/main/aidl/android/view/IRotationWatcher.aidl: -------------------------------------------------------------------------------- 1 | /* //device/java/android/android/hardware/ISensorListener.aidl 2 | ** 3 | ** Copyright 2008, The Android Open Source Project 4 | ** 5 | ** Licensed under the Apache License, Version 2.0 (the "License"); 6 | ** you may not use this file except in compliance with the License. 7 | ** You may obtain a copy of the License at 8 | ** 9 | ** http://www.apache.org/licenses/LICENSE-2.0 10 | ** 11 | ** Unless required by applicable law or agreed to in writing, software 12 | ** distributed under the License is distributed on an "AS IS" BASIS, 13 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ** See the License for the specific language governing permissions and 15 | ** limitations under the License. 16 | */ 17 | 18 | package android.view; 19 | 20 | /** 21 | * {@hide} 22 | */ 23 | interface IRotationWatcher { 24 | oneway void onRotationChanged(int rotation); 25 | } 26 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/ControlMessage.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import android.os.SystemClock; 4 | import android.view.MotionEvent; 5 | 6 | /** 7 | * Union of all supported event types, identified by their {@code type}. 8 | */ 9 | public final class ControlMessage { 10 | 11 | public static final int TYPE_INJECT_KEYCODE = 0; 12 | public static final int TYPE_INJECT_TEXT = 1; 13 | public static final int TYPE_INJECT_MOUSE_EVENT = 2; 14 | public static final int TYPE_INJECT_TOUCH_EVENT = 3; 15 | public static final int TYPE_INJECT_SCROLL_EVENT = 4; 16 | public static final int TYPE_COMMAND = 5; 17 | public static final int TYPE_SET_CLIPBOARD = 6; 18 | public static final int TYPE_SET_SCREEN_POWER_MODE = 7; 19 | 20 | public static final int COMMAND_BACK_OR_SCREEN_ON = 0; 21 | public static final int COMMAND_EXPAND_NOTIFICATION_PANEL = 1; 22 | public static final int COMMAND_COLLAPSE_NOTIFICATION_PANEL = 2; 23 | public static final int COMMAND_QUIT = 3; 24 | public static final int COMMAND_TO_PORTRAIT = 4; 25 | public static final int COMMAND_TO_LANDSCAPE = 5; 26 | public static final int COMMAND_PING = 6; 27 | public static final int COMMAND_GET_CLIPBOARD = 7; 28 | 29 | public static final int MAX_FINGERS = 10; 30 | 31 | private int type; 32 | private String text; 33 | private int metaState; // KeyEvent.META_* 34 | private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or COMMAND_* or POWER_MODE_* 35 | private int keycode; // KeyEvent.KEYCODE_* 36 | private int buttons; // MotionEvent.BUTTON_* 37 | private Position position; 38 | private int hScroll; 39 | private int vScroll; 40 | private int fingerId; 41 | private final long timestamp; 42 | 43 | private static long referenceTime = 0; 44 | private static long lastEvent = 0; 45 | 46 | private ControlMessage() { 47 | this.timestamp = SystemClock.uptimeMillis(); 48 | } 49 | 50 | private ControlMessage(long t) { 51 | /* 52 | t is actually 32-bit unsigned value, and it wraps in 49.7 days. 53 | Here it is presumed that the server will never run continuously 54 | for so long time. 55 | */ 56 | long nowLocal = SystemClock.uptimeMillis(); 57 | long now = referenceTime+t; 58 | 59 | /* 60 | It is important to reproduce real delays between events, but 61 | we can't predict network delays. 62 | */ 63 | if (now > nowLocal) { 64 | final long delay = now - nowLocal; 65 | SystemClock.sleep(delay > 50 ? 50 : delay); 66 | now = SystemClock.uptimeMillis(); 67 | if (delay > 50) 68 | referenceTime -= 50; 69 | } else { 70 | final long delay = nowLocal - now; 71 | if (delay > 50) // Too far in the past? Better to be in the future a little bit! 72 | referenceTime += 50; 73 | } 74 | 75 | if (now < lastEvent) // Make all events subsequent! 76 | now = lastEvent; 77 | 78 | this.timestamp = now; 79 | lastEvent = now; 80 | } 81 | 82 | public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) { 83 | ControlMessage event = new ControlMessage(); 84 | event.type = TYPE_INJECT_KEYCODE; 85 | event.action = action; 86 | event.keycode = keycode; 87 | event.metaState = metaState; 88 | return event; 89 | } 90 | 91 | public static ControlMessage createInjectText(String text) { 92 | ControlMessage event = new ControlMessage(); 93 | event.type = TYPE_INJECT_TEXT; 94 | event.text = text; 95 | return event; 96 | } 97 | 98 | public static ControlMessage createInjectMouseEvent(int action, int buttons, Position position, long timestamp) { 99 | ControlMessage event = new ControlMessage(timestamp); 100 | event.type = TYPE_INJECT_MOUSE_EVENT; 101 | event.action = action; 102 | event.buttons = buttons; 103 | event.position = position; 104 | return event; 105 | } 106 | 107 | public static ControlMessage createInjectTouchEvent(int action, int fingerId, Position position, long timestamp) { 108 | if (fingerId < 0 || fingerId >= MAX_FINGERS) 109 | fingerId = 0; 110 | ControlMessage event = new ControlMessage(timestamp); 111 | event.type = TYPE_INJECT_TOUCH_EVENT; 112 | event.action = action; 113 | event.position = position; 114 | event.fingerId = fingerId; 115 | return event; 116 | } 117 | 118 | public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll, long timestamp) { 119 | ControlMessage event = new ControlMessage(timestamp); 120 | event.type = TYPE_INJECT_SCROLL_EVENT; 121 | event.position = position; 122 | event.hScroll = hScroll; 123 | event.vScroll = vScroll; 124 | return event; 125 | } 126 | 127 | public static ControlMessage createSetClipboard(String text) { 128 | ControlMessage event = new ControlMessage(); 129 | event.type = TYPE_SET_CLIPBOARD; 130 | event.text = text; 131 | return event; 132 | } 133 | 134 | /** 135 | * @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants 136 | */ 137 | public static ControlMessage createSetScreenPowerMode(int mode) { 138 | ControlMessage event = new ControlMessage(); 139 | event.type = TYPE_SET_SCREEN_POWER_MODE; 140 | event.action = mode; 141 | return event; 142 | } 143 | 144 | public static ControlMessage createEmpty(int type) { 145 | ControlMessage event = new ControlMessage(); 146 | event.type = type; 147 | return event; 148 | } 149 | 150 | public static ControlMessage createCommandEvent(int action, long timestamp) { 151 | if (action == COMMAND_PING) { 152 | if (referenceTime == 0) 153 | referenceTime = SystemClock.uptimeMillis() - timestamp; 154 | } 155 | 156 | ControlMessage event = new ControlMessage(); 157 | event.type = TYPE_COMMAND; 158 | event.action = action; 159 | return event; 160 | } 161 | 162 | public String getText() { return text; } 163 | public Position getPosition() { return position; } 164 | public int getType() { return type; } 165 | public int getMetaState() { return metaState; } 166 | public int getAction() { return action; } 167 | public int getKeycode() { return keycode; } 168 | public int getButtons() { return buttons; } 169 | public int getHScroll() { return hScroll; } 170 | public int getVScroll() { return vScroll; } 171 | public int getFingerId() { return fingerId; } 172 | public long getTime() { return timestamp; } 173 | } 174 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/Device.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import com.genymobile.scrcpy.wrappers.ServiceManager; 4 | import com.genymobile.scrcpy.wrappers.SurfaceControl; 5 | 6 | import android.graphics.Rect; 7 | import android.os.Build; 8 | import android.os.IBinder; 9 | import android.os.RemoteException; 10 | import android.view.IRotationWatcher; 11 | import android.view.InputEvent; 12 | 13 | public final class Device { 14 | 15 | public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; 16 | public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; 17 | 18 | public interface RotationListener { 19 | void onRotationChanged(int rotation); 20 | } 21 | 22 | private final ServiceManager serviceManager = new ServiceManager(); 23 | 24 | private ScreenInfo screenInfo; 25 | private RotationListener rotationListener; 26 | 27 | public Device(Options options) { 28 | screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize()); 29 | registerRotationWatcher(new IRotationWatcher.Stub() { 30 | @Override 31 | public void onRotationChanged(int rotation) throws RemoteException { 32 | synchronized (Device.this) { 33 | screenInfo = screenInfo.withRotation(rotation); 34 | 35 | // notify 36 | if (rotationListener != null) { 37 | rotationListener.onRotationChanged(rotation); 38 | } 39 | } 40 | } 41 | }); 42 | } 43 | 44 | public synchronized ScreenInfo getScreenInfo() { 45 | return screenInfo; 46 | } 47 | 48 | private ScreenInfo computeScreenInfo(Rect crop, int maxSize) { 49 | DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); 50 | boolean rotated = (displayInfo.getRotation() & 1) != 0; 51 | Size deviceSize = displayInfo.getSize(); 52 | Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); 53 | if (crop != null) { 54 | if (rotated) { 55 | // the crop (provided by the user) is expressed in the natural orientation 56 | crop = flipRect(crop); 57 | } 58 | if (!contentRect.intersect(crop)) { 59 | // intersect() changes contentRect so that it is intersected with crop 60 | Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); 61 | contentRect = new Rect(); // empty 62 | } 63 | } 64 | 65 | Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); 66 | return new ScreenInfo(contentRect, videoSize, rotated); 67 | } 68 | 69 | private static String formatCrop(Rect rect) { 70 | return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; 71 | } 72 | 73 | @SuppressWarnings("checkstyle:MagicNumber") 74 | private static Size computeVideoSize(int w, int h, int maxSize) { 75 | // Compute the video size and the padding of the content inside this video. 76 | // Principle: 77 | // - scale down the great side of the screen to maxSize (if necessary); 78 | // - scale down the other side so that the aspect ratio is preserved; 79 | // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) 80 | w &= ~7; // in case it's not a multiple of 8 81 | h &= ~7; 82 | if (maxSize > 0) { 83 | if (BuildConfig.DEBUG && maxSize % 8 != 0) { 84 | throw new AssertionError("Max size must be a multiple of 8"); 85 | } 86 | boolean portrait = h > w; 87 | int major = portrait ? h : w; 88 | int minor = portrait ? w : h; 89 | if (major > maxSize) { 90 | int minorExact = minor * maxSize / major; 91 | // +4 to round the value to the nearest multiple of 8 92 | minor = (minorExact + 4) & ~7; 93 | major = maxSize; 94 | } 95 | w = portrait ? minor : major; 96 | h = portrait ? major : minor; 97 | } 98 | return new Size(w, h); 99 | } 100 | 101 | public Point getPhysicalPoint(Position position) { 102 | // it hides the field on purpose, to read it with a lock 103 | @SuppressWarnings("checkstyle:HiddenField") 104 | ScreenInfo screenInfo = getScreenInfo(); // read with synchronization 105 | Size videoSize = screenInfo.getVideoSize(); 106 | Size clientVideoSize = position.getScreenSize(); 107 | if (!videoSize.equals(clientVideoSize)) { 108 | // The client sends a click relative to a video with wrong dimensions, 109 | // the device may have been rotated since the event was generated, so ignore the event 110 | return null; 111 | } 112 | Rect contentRect = screenInfo.getContentRect(); 113 | Point point = position.getPoint(); 114 | int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); 115 | int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); 116 | return new Point(scaledX, scaledY); 117 | } 118 | 119 | public static String getDeviceName() { 120 | return Build.MODEL; 121 | } 122 | 123 | public boolean injectInputEvent(InputEvent inputEvent, int mode) { 124 | return serviceManager.getInputManager().injectInputEvent(inputEvent, mode); 125 | } 126 | 127 | public boolean isScreenOn() { 128 | return serviceManager.getPowerManager().isScreenOn(); 129 | } 130 | 131 | public void registerRotationWatcher(IRotationWatcher rotationWatcher) { 132 | serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher); 133 | } 134 | 135 | public synchronized void setRotationListener(RotationListener rotationListener) { 136 | this.rotationListener = rotationListener; 137 | } 138 | 139 | public void expandNotificationPanel() { 140 | serviceManager.getStatusBarManager().expandNotificationsPanel(); 141 | } 142 | 143 | public void collapsePanels() { 144 | serviceManager.getStatusBarManager().collapsePanels(); 145 | } 146 | 147 | public String getClipboardText() { 148 | CharSequence s = serviceManager.getClipboardManager().getText(); 149 | if (s == null) { 150 | return null; 151 | } 152 | return s.toString(); 153 | } 154 | 155 | public void setClipboardText(String text) { 156 | serviceManager.getClipboardManager().setText(text); 157 | Ln.i("Device clipboard set"); 158 | } 159 | 160 | /** 161 | * @param mode one of the {@code SCREEN_POWER_MODE_*} constants 162 | */ 163 | public void setScreenPowerMode(int mode) { 164 | IBinder d = SurfaceControl.getBuiltInDisplay(0); 165 | SurfaceControl.setDisplayPowerMode(d, mode); 166 | Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); 167 | } 168 | 169 | static Rect flipRect(Rect crop) { 170 | return new Rect(crop.top, crop.left, crop.bottom, crop.right); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | public final class DeviceMessage { 4 | 5 | public static final int TYPE_CLIPBOARD = 0; 6 | 7 | private int type; 8 | private String text; 9 | 10 | private DeviceMessage() { 11 | } 12 | 13 | public static DeviceMessage createClipboard(String text) { 14 | DeviceMessage event = new DeviceMessage(); 15 | event.type = TYPE_CLIPBOARD; 16 | event.text = text; 17 | return event; 18 | } 19 | 20 | public int getType() { 21 | return type; 22 | } 23 | 24 | public String getText() { 25 | return text; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import java.io.IOException; 4 | 5 | public final class DeviceMessageSender { 6 | 7 | private final DesktopConnection connection; 8 | 9 | private String clipboardText; 10 | private boolean running = true; 11 | 12 | public DeviceMessageSender(DesktopConnection connection) { 13 | this.connection = connection; 14 | } 15 | 16 | public synchronized void pushClipboardText(String text) { 17 | clipboardText = text; 18 | notify(); 19 | } 20 | 21 | public void loop() throws IOException, InterruptedException { 22 | while (running) { 23 | String text; 24 | synchronized (this) { 25 | while (running && clipboardText == null) { 26 | wait(); 27 | } 28 | text = clipboardText; 29 | clipboardText = null; 30 | } 31 | if (text != null && !text.isEmpty()) { 32 | DeviceMessage event = DeviceMessage.createClipboard(text); 33 | connection.sendDeviceMessage(event); 34 | } 35 | } 36 | } 37 | 38 | public synchronized void stop() { 39 | running = false; 40 | notify(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | import java.nio.ByteBuffer; 6 | import java.nio.charset.StandardCharsets; 7 | 8 | public class DeviceMessageWriter { 9 | 10 | public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; 11 | private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3; 12 | 13 | private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE]; 14 | private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); 15 | 16 | @SuppressWarnings("checkstyle:MagicNumber") 17 | public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { 18 | buffer.clear(); 19 | buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD); 20 | switch (msg.getType()) { 21 | case DeviceMessage.TYPE_CLIPBOARD: 22 | String text = msg.getText(); 23 | byte[] raw = text.getBytes(StandardCharsets.UTF_8); 24 | int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); 25 | buffer.putShort((short) len); 26 | buffer.put(raw, 0, len); 27 | output.write(rawBuffer, 0, buffer.position()); 28 | break; 29 | default: 30 | Ln.w("Unknown device message: " + msg.getType()); 31 | break; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | public final class DisplayInfo { 4 | private final Size size; 5 | private final int rotation; 6 | 7 | public DisplayInfo(Size size, int rotation) { 8 | this.size = size; 9 | this.rotation = rotation; 10 | } 11 | 12 | public Size getSize() { 13 | return size; 14 | } 15 | 16 | public int getRotation() { 17 | return rotation; 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/IME.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import android.content.Intent; 4 | 5 | import com.genymobile.scrcpy.wrappers.ActivityManager; 6 | import com.genymobile.scrcpy.wrappers.ServiceManager; 7 | 8 | public final class IME { 9 | private final ActivityManager activityManager = (new ServiceManager()).getActivityManager(); 10 | 11 | private boolean enabled = DeviceControl.isAdbIMEEnabled(); 12 | 13 | IME() { 14 | } 15 | 16 | public boolean send(final String text) { 17 | if (!enabled) return false; 18 | Intent intent = new Intent(); 19 | intent.setAction("ADB_INPUT_TEXT"); 20 | intent.putExtra("msg", text); 21 | return activityManager.broadcastIntent(intent); 22 | } 23 | 24 | public void Finish() { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/KeyComposition.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * Decompose accented characters. 8 | *

9 | * For example, {@link #decompose(char) decompose('é')} returns {@code "\u0301e"}. 10 | *

11 | * This is useful for injecting key events to generate the expected character ({@link android.view.KeyCharacterMap#getEvents(char[])} 12 | * KeyCharacterMap.getEvents()} returns {@code null} with input {@code "é"} but works with input {@code "\u0301e"}). 13 | *

14 | * See diacritical dead key characters. 15 | */ 16 | public final class KeyComposition { 17 | 18 | private static final String KEY_DEAD_GRAVE = "\u0300"; 19 | private static final String KEY_DEAD_ACUTE = "\u0301"; 20 | private static final String KEY_DEAD_CIRCUMFLEX = "\u0302"; 21 | private static final String KEY_DEAD_TILDE = "\u0303"; 22 | private static final String KEY_DEAD_UMLAUT = "\u0308"; 23 | 24 | private static final Map COMPOSITION_MAP = createDecompositionMap(); 25 | 26 | private KeyComposition() { 27 | // not instantiable 28 | } 29 | 30 | public static String decompose(char c) { 31 | return COMPOSITION_MAP.get(c); 32 | } 33 | 34 | private static String grave(char c) { 35 | return KEY_DEAD_GRAVE + c; 36 | } 37 | 38 | private static String acute(char c) { 39 | return KEY_DEAD_ACUTE + c; 40 | } 41 | 42 | private static String circumflex(char c) { 43 | return KEY_DEAD_CIRCUMFLEX + c; 44 | } 45 | 46 | private static String tilde(char c) { 47 | return KEY_DEAD_TILDE + c; 48 | } 49 | 50 | private static String umlaut(char c) { 51 | return KEY_DEAD_UMLAUT + c; 52 | } 53 | 54 | private static Map createDecompositionMap() { 55 | Map map = new HashMap<>(); 56 | map.put('À', grave('A')); 57 | map.put('È', grave('E')); 58 | map.put('Ì', grave('I')); 59 | map.put('Ò', grave('O')); 60 | map.put('Ù', grave('U')); 61 | map.put('à', grave('a')); 62 | map.put('è', grave('e')); 63 | map.put('ì', grave('i')); 64 | map.put('ò', grave('o')); 65 | map.put('ù', grave('u')); 66 | map.put('Ǹ', grave('N')); 67 | map.put('ǹ', grave('n')); 68 | map.put('Ẁ', grave('W')); 69 | map.put('ẁ', grave('w')); 70 | map.put('Ỳ', grave('Y')); 71 | map.put('ỳ', grave('y')); 72 | 73 | map.put('Á', acute('A')); 74 | map.put('É', acute('E')); 75 | map.put('Í', acute('I')); 76 | map.put('Ó', acute('O')); 77 | map.put('Ú', acute('U')); 78 | map.put('Ý', acute('Y')); 79 | map.put('á', acute('a')); 80 | map.put('é', acute('e')); 81 | map.put('í', acute('i')); 82 | map.put('ó', acute('o')); 83 | map.put('ú', acute('u')); 84 | map.put('ý', acute('y')); 85 | map.put('Ć', acute('C')); 86 | map.put('ć', acute('c')); 87 | map.put('Ĺ', acute('L')); 88 | map.put('ĺ', acute('l')); 89 | map.put('Ń', acute('N')); 90 | map.put('ń', acute('n')); 91 | map.put('Ŕ', acute('R')); 92 | map.put('ŕ', acute('r')); 93 | map.put('Ś', acute('S')); 94 | map.put('ś', acute('s')); 95 | map.put('Ź', acute('Z')); 96 | map.put('ź', acute('z')); 97 | map.put('Ǵ', acute('G')); 98 | map.put('ǵ', acute('g')); 99 | map.put('Ḉ', acute('Ç')); 100 | map.put('ḉ', acute('ç')); 101 | map.put('Ḱ', acute('K')); 102 | map.put('ḱ', acute('k')); 103 | map.put('Ḿ', acute('M')); 104 | map.put('ḿ', acute('m')); 105 | map.put('Ṕ', acute('P')); 106 | map.put('ṕ', acute('p')); 107 | map.put('Ẃ', acute('W')); 108 | map.put('ẃ', acute('w')); 109 | 110 | map.put('Â', circumflex('A')); 111 | map.put('Ê', circumflex('E')); 112 | map.put('Î', circumflex('I')); 113 | map.put('Ô', circumflex('O')); 114 | map.put('Û', circumflex('U')); 115 | map.put('â', circumflex('a')); 116 | map.put('ê', circumflex('e')); 117 | map.put('î', circumflex('i')); 118 | map.put('ô', circumflex('o')); 119 | map.put('û', circumflex('u')); 120 | map.put('Ĉ', circumflex('C')); 121 | map.put('ĉ', circumflex('c')); 122 | map.put('Ĝ', circumflex('G')); 123 | map.put('ĝ', circumflex('g')); 124 | map.put('Ĥ', circumflex('H')); 125 | map.put('ĥ', circumflex('h')); 126 | map.put('Ĵ', circumflex('J')); 127 | map.put('ĵ', circumflex('j')); 128 | map.put('Ŝ', circumflex('S')); 129 | map.put('ŝ', circumflex('s')); 130 | map.put('Ŵ', circumflex('W')); 131 | map.put('ŵ', circumflex('w')); 132 | map.put('Ŷ', circumflex('Y')); 133 | map.put('ŷ', circumflex('y')); 134 | map.put('Ẑ', circumflex('Z')); 135 | map.put('ẑ', circumflex('z')); 136 | 137 | map.put('Ã', tilde('A')); 138 | map.put('Ñ', tilde('N')); 139 | map.put('Õ', tilde('O')); 140 | map.put('ã', tilde('a')); 141 | map.put('ñ', tilde('n')); 142 | map.put('õ', tilde('o')); 143 | map.put('Ĩ', tilde('I')); 144 | map.put('ĩ', tilde('i')); 145 | map.put('Ũ', tilde('U')); 146 | map.put('ũ', tilde('u')); 147 | map.put('Ẽ', tilde('E')); 148 | map.put('ẽ', tilde('e')); 149 | map.put('Ỹ', tilde('Y')); 150 | map.put('ỹ', tilde('y')); 151 | 152 | map.put('Ä', umlaut('A')); 153 | map.put('Ë', umlaut('E')); 154 | map.put('Ï', umlaut('I')); 155 | map.put('Ö', umlaut('O')); 156 | map.put('Ü', umlaut('U')); 157 | map.put('ä', umlaut('a')); 158 | map.put('ë', umlaut('e')); 159 | map.put('ï', umlaut('i')); 160 | map.put('ö', umlaut('o')); 161 | map.put('ü', umlaut('u')); 162 | map.put('ÿ', umlaut('y')); 163 | map.put('Ÿ', umlaut('Y')); 164 | map.put('Ḧ', umlaut('H')); 165 | map.put('ḧ', umlaut('h')); 166 | map.put('Ẅ', umlaut('W')); 167 | map.put('ẅ', umlaut('w')); 168 | map.put('Ẍ', umlaut('X')); 169 | map.put('ẍ', umlaut('x')); 170 | map.put('ẗ', umlaut('t')); 171 | 172 | return map; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/Ln.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | * Log both to Android logger (so that logs are visible in "adb logcat") and standard output/error (so that they are visible in the terminal 7 | * directly). 8 | */ 9 | public final class Ln { 10 | 11 | private static final String TAG = "scrcpy"; 12 | private static final String PREFIX = "[server] "; 13 | 14 | enum Level { 15 | DEBUG, 16 | INFO, 17 | WARN, 18 | ERROR; 19 | } 20 | 21 | private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO; 22 | 23 | private Ln() { 24 | // not instantiable 25 | } 26 | 27 | public static boolean isEnabled(Level level) { 28 | return level.ordinal() >= THRESHOLD.ordinal(); 29 | } 30 | 31 | public static void d(String message) { 32 | if (isEnabled(Level.DEBUG)) { 33 | Log.d(TAG, message); 34 | System.out.println(PREFIX + "DEBUG: " + message); 35 | } 36 | } 37 | 38 | public static void i(String message) { 39 | if (isEnabled(Level.INFO)) { 40 | Log.i(TAG, message); 41 | System.out.println(PREFIX + "INFO: " + message); 42 | } 43 | } 44 | 45 | public static void w(String message) { 46 | if (isEnabled(Level.WARN)) { 47 | Log.w(TAG, message); 48 | System.out.println(PREFIX + "WARN: " + message); 49 | } 50 | } 51 | 52 | public static void e(String message, Throwable throwable) { 53 | if (isEnabled(Level.ERROR)) { 54 | Log.e(TAG, message, throwable); 55 | System.out.println(PREFIX + "ERROR: " + message); 56 | if (throwable != null) { 57 | throwable.printStackTrace(); 58 | throwable.printStackTrace(System.out); 59 | } 60 | } 61 | } 62 | 63 | public static void e(String message) { 64 | e(message, null); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/Options.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import android.graphics.Point; 4 | import android.graphics.Rect; 5 | 6 | public class Options { 7 | private int maxSize; 8 | private int bitRate; 9 | private boolean tunnelForward; 10 | private Rect crop; 11 | private boolean sendFrameMeta; // send PTS so that the client may record properly 12 | private boolean control; 13 | private int density = 0; 14 | private final Point size = new Point(0,0); 15 | private boolean tabletMode = false; 16 | private int local_port = 0; 17 | private boolean useIME = false; 18 | private boolean disableHWOverlays = false; 19 | 20 | public int getMaxSize() { 21 | return maxSize; 22 | } 23 | 24 | public void setMaxSize(int maxSize) { 25 | this.maxSize = maxSize; 26 | } 27 | 28 | public int getBitRate() { 29 | return bitRate; 30 | } 31 | 32 | public void setBitRate(int bitRate) { 33 | this.bitRate = bitRate; 34 | } 35 | 36 | public boolean isTunnelForward() { 37 | return tunnelForward; 38 | } 39 | 40 | public void setTunnelForward(boolean tunnelForward) { 41 | this.tunnelForward = tunnelForward; 42 | } 43 | 44 | public Rect getCrop() { 45 | return crop; 46 | } 47 | 48 | public void setCrop(Rect crop) { 49 | this.crop = crop; 50 | } 51 | 52 | public boolean getSendFrameMeta() { 53 | return sendFrameMeta; 54 | } 55 | 56 | public void setSendFrameMeta(boolean sendFrameMeta) { 57 | this.sendFrameMeta = sendFrameMeta; 58 | } 59 | 60 | public boolean getControl() { 61 | return control; 62 | } 63 | 64 | public void setControl(boolean control) { 65 | this.control = control; 66 | } 67 | 68 | public int getDensity() { return density; } 69 | 70 | public Point getSize() { return size; } 71 | 72 | public boolean getTabletMode() { return tabletMode; } 73 | 74 | public int getPort() { return local_port; } 75 | 76 | public boolean getUseIME() { return useIME; } 77 | 78 | public boolean getDisableHWOverlays() { return disableHWOverlays; } 79 | 80 | public void setOption(final String option) { 81 | String[] pair = option.split("="); 82 | if (pair.length != 2) { 83 | Ln.w("Expected key=value pair ("+option+")"); 84 | return; 85 | } 86 | if ("density".equals(pair[0])) { 87 | density = Integer.parseInt(pair[1]); 88 | } else if("size".equals(pair[0])) { 89 | String[] value=pair[1].split(":"); 90 | if (value.length != 2) value=pair[1].split("x"); 91 | if (value.length != 2) { 92 | Ln.w("Expected size=width:height ("+option+")"); 93 | return; 94 | } 95 | size.x = Integer.parseInt(value[0]); 96 | size.y = Integer.parseInt(value[1]); 97 | } else if("tablet".equals(pair[0])) { 98 | tabletMode = Boolean.parseBoolean(pair[1]); 99 | } else if("port".equals(pair[0])) { 100 | local_port = Integer.parseInt(pair[1]); 101 | } else if("useIME".equals(pair[0])) { 102 | useIME = Boolean.parseBoolean(pair[1]); 103 | } else if("disableHWOverlays".equals(pair[0])) { 104 | disableHWOverlays = Boolean.parseBoolean(pair[1]); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/Point.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import java.util.Objects; 4 | 5 | public class Point { 6 | private final int x; 7 | private final int y; 8 | 9 | public Point(int x, int y) { 10 | this.x = x; 11 | this.y = y; 12 | } 13 | 14 | public int getX() { 15 | return x; 16 | } 17 | 18 | public int getY() { 19 | return y; 20 | } 21 | 22 | @Override 23 | public boolean equals(Object o) { 24 | if (this == o) { 25 | return true; 26 | } 27 | if (o == null || getClass() != o.getClass()) { 28 | return false; 29 | } 30 | Point point = (Point) o; 31 | return x == point.x 32 | && y == point.y; 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return Objects.hash(x, y); 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return "Point{" 43 | + "x=" + x 44 | + ", y=" + y 45 | + '}'; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/Position.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import java.util.Objects; 4 | 5 | public class Position { 6 | private Point point; 7 | private Size screenSize; 8 | 9 | public Position(Point point, Size screenSize) { 10 | this.point = point; 11 | this.screenSize = screenSize; 12 | } 13 | 14 | public Position(int x, int y, int screenWidth, int screenHeight) { 15 | this(new Point(x, y), new Size(screenWidth, screenHeight)); 16 | } 17 | 18 | public Point getPoint() { 19 | return point; 20 | } 21 | 22 | public Size getScreenSize() { 23 | return screenSize; 24 | } 25 | 26 | @Override 27 | public boolean equals(Object o) { 28 | if (this == o) { 29 | return true; 30 | } 31 | if (o == null || getClass() != o.getClass()) { 32 | return false; 33 | } 34 | Position position = (Position) o; 35 | return Objects.equals(point, position.point) 36 | && Objects.equals(screenSize, position.screenSize); 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | return Objects.hash(point, screenSize); 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return "Position{" 47 | + "point=" + point 48 | + ", screenSize=" + screenSize 49 | + '}'; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import android.graphics.Rect; 4 | 5 | public final class ScreenInfo { 6 | private final Rect contentRect; // device size, possibly cropped 7 | private final Size videoSize; 8 | private final boolean rotated; 9 | 10 | public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) { 11 | this.contentRect = contentRect; 12 | this.videoSize = videoSize; 13 | this.rotated = rotated; 14 | } 15 | 16 | public Rect getContentRect() { 17 | return contentRect; 18 | } 19 | 20 | public Size getVideoSize() { 21 | return videoSize; 22 | } 23 | 24 | public ScreenInfo withRotation(int rotation) { 25 | boolean newRotated = (rotation & 1) != 0; 26 | if (rotated == newRotated) { 27 | return this; 28 | } 29 | return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/Size.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import android.graphics.Rect; 4 | 5 | import java.util.Objects; 6 | 7 | public final class Size { 8 | private final int width; 9 | private final int height; 10 | 11 | public Size(int width, int height) { 12 | this.width = width; 13 | this.height = height; 14 | } 15 | 16 | public int getWidth() { 17 | return width; 18 | } 19 | 20 | public int getHeight() { 21 | return height; 22 | } 23 | 24 | public Size rotate() { 25 | return new Size(height, width); 26 | } 27 | 28 | public Rect toRect() { 29 | return new Rect(0, 0, width, height); 30 | } 31 | 32 | @Override 33 | public boolean equals(Object o) { 34 | if (this == o) { 35 | return true; 36 | } 37 | if (o == null || getClass() != o.getClass()) { 38 | return false; 39 | } 40 | Size size = (Size) o; 41 | return width == size.width 42 | && height == size.height; 43 | } 44 | 45 | @Override 46 | public int hashCode() { 47 | return Objects.hash(width, height); 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return "Size{" 53 | + "width=" + width 54 | + ", height=" + height 55 | + '}'; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/StringUtils.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | public final class StringUtils { 4 | private StringUtils() { 5 | // not instantiable 6 | } 7 | 8 | @SuppressWarnings("checkstyle:MagicNumber") 9 | public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) { 10 | int len = utf8.length; 11 | if (len <= maxLength) { 12 | return len; 13 | } 14 | len = maxLength; 15 | // see UTF-8 encoding 16 | while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) { 17 | // the next byte is not the start of a new UTF-8 codepoint 18 | // so if we would cut there, the character would be truncated 19 | len--; 20 | } 21 | return len; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import android.content.ComponentName; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.os.IInterface; 7 | 8 | import com.genymobile.scrcpy.Ln; 9 | 10 | import java.lang.reflect.Method; 11 | 12 | public final class ActivityManager { 13 | private final IInterface manager; 14 | private Method broadcastIntentMethod; 15 | 16 | public static final int OP_NONE = -1; 17 | private static final String SHELL_PACKAGE_NAME = "com.android.shell"; 18 | 19 | public ActivityManager(IInterface manager) { 20 | this.manager = manager; 21 | } 22 | /* 23 | int broadcastIntent(IApplicationThread caller, Intent intent, 24 | String resolvedType, IIntentReceiver resultTo, int resultCode, 25 | String resultData, Bundle map, String[] requiredPermissions, 26 | int appOp, Bundle options, boolean serialized, boolean sticky, int userId); 27 | */ 28 | public boolean broadcastIntent(Intent intent) { 29 | Class cls = manager.getClass(); 30 | if (broadcastIntentMethod == null) { 31 | try { 32 | Class IApplicationThread = Class.forName("android.app.IApplicationThread"); 33 | Class IIntentReceiver = Class.forName("android.content.IIntentReceiver"); 34 | broadcastIntentMethod = cls.getMethod("broadcastIntent" 35 | , IApplicationThread, Intent.class 36 | , String.class, IIntentReceiver, int.class 37 | , String.class, Bundle.class, String[].class 38 | , int.class, Bundle.class, boolean.class, boolean.class, int.class); 39 | broadcastIntentMethod.setAccessible(true); 40 | } catch (Exception e) { 41 | Ln.e("broadcastIntent", e); 42 | } 43 | } 44 | if (broadcastIntentMethod == null) return false; 45 | try { 46 | broadcastIntentMethod.invoke(manager, 47 | null, intent, 48 | null, null/* receiver*/, 0, 49 | null, null, null, 50 | OP_NONE, null, true, false, ServiceManager.USER_CURRENT); 51 | return true; 52 | } catch (Exception e) { 53 | Ln.e("broadcastIntent.invoke", e); 54 | } 55 | return false; 56 | } 57 | /* 58 | ComponentName startService(in IApplicationThread caller, in Intent service, 59 | in String resolvedType, boolean requireForeground, 60 | in String callingPackage, int userId); 61 | */ 62 | public boolean startService(final String service) { 63 | try { 64 | Class cls = manager.getClass(); 65 | Intent intent = new Intent(); 66 | ComponentName cn = ComponentName.unflattenFromString(service); 67 | if (cn == null) { 68 | Ln.w("Bad component name:"+service); 69 | return false; 70 | } 71 | intent.setComponent(cn); 72 | Class IApplicationThread = Class.forName("android.app.IApplicationThread"); 73 | try { 74 | // Android 8+ 75 | // startService(IApplicationThread caller, Intent service, String resolvedType, boolean requireForeground, String callingPackage, int userId) 76 | Object ret = cls.getMethod("startService", IApplicationThread, Intent.class, String.class, boolean.class, String.class, int.class).invoke(manager 77 | , null, intent 78 | , intent.getType(), false 79 | , SHELL_PACKAGE_NAME, ServiceManager.USER_CURRENT); 80 | cn = (ComponentName)ret; 81 | } catch (NoSuchMethodException e) { 82 | try { 83 | // Android 6, 7 84 | // startService(IApplicationThread caller, Intent service, String resolvedType, String callingPackage, int userId) 85 | Object ret = cls.getMethod("startService", IApplicationThread, Intent.class, String.class, String.class, int.class).invoke(manager 86 | , null, intent 87 | , intent.getType() 88 | , SHELL_PACKAGE_NAME, ServiceManager.USER_CURRENT); 89 | cn = (ComponentName)ret; 90 | } catch (NoSuchMethodException ee) { 91 | // Android 5 92 | // startService(IApplicationThread caller, Intent service, String resolvedType, int userId) 93 | Object ret = cls.getMethod("startService", IApplicationThread, Intent.class, String.class, int.class).invoke(manager 94 | , null, intent 95 | , intent.getType() 96 | , ServiceManager.USER_CURRENT); 97 | cn = (ComponentName)ret; 98 | } 99 | } 100 | if (cn == null) { 101 | Ln.w("Error: Not found; no service started."); 102 | return false; 103 | } else if (cn.getPackageName().equals("!")) { 104 | Ln.w("Error: Requires permission " + cn.getClassName()); 105 | return false; 106 | } else if (cn.getPackageName().equals("!!")) { 107 | Ln.w("Error: " + cn.getClassName()); 108 | return false; 109 | } 110 | return true; 111 | } catch (Exception e) { 112 | Ln.e("startService", e); 113 | } 114 | return false; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import android.content.ClipData; 4 | import android.os.IInterface; 5 | 6 | import java.lang.reflect.InvocationTargetException; 7 | import java.lang.reflect.Method; 8 | 9 | public class ClipboardManager { 10 | private final IInterface manager; 11 | private final Method getPrimaryClipMethod; 12 | private final Method setPrimaryClipMethod; 13 | 14 | public ClipboardManager(IInterface manager) { 15 | this.manager = manager; 16 | try { 17 | getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); 18 | setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); 19 | } catch (NoSuchMethodException e) { 20 | throw new AssertionError(e); 21 | } 22 | } 23 | 24 | public CharSequence getText() { 25 | try { 26 | ClipData clipData = (ClipData) getPrimaryClipMethod.invoke(manager, "com.android.shell"); 27 | if (clipData == null || clipData.getItemCount() == 0) { 28 | return null; 29 | } 30 | return clipData.getItemAt(0).getText(); 31 | } catch (InvocationTargetException | IllegalAccessException e) { 32 | throw new AssertionError(e); 33 | } 34 | } 35 | 36 | public void setText(CharSequence text) { 37 | ClipData clipData = ClipData.newPlainText(null, text); 38 | try { 39 | setPrimaryClipMethod.invoke(manager, clipData, "com.android.shell"); 40 | } catch (InvocationTargetException | IllegalAccessException e) { 41 | throw new AssertionError(e); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import com.genymobile.scrcpy.DisplayInfo; 4 | import com.genymobile.scrcpy.Size; 5 | 6 | import android.os.IInterface; 7 | 8 | public final class DisplayManager { 9 | private final IInterface manager; 10 | 11 | public DisplayManager(IInterface manager) { 12 | this.manager = manager; 13 | } 14 | 15 | public DisplayInfo getDisplayInfo() { 16 | try { 17 | Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0); 18 | Class cls = displayInfo.getClass(); 19 | // width and height already take the rotation into account 20 | int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo); 21 | int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo); 22 | int rotation = cls.getDeclaredField("rotation").getInt(displayInfo); 23 | return new DisplayInfo(new Size(width, height), rotation); 24 | } catch (Exception e) { 25 | throw new AssertionError(e); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import android.os.IInterface; 4 | import android.view.InputEvent; 5 | 6 | import java.lang.reflect.InvocationTargetException; 7 | import java.lang.reflect.Method; 8 | 9 | public final class InputManager { 10 | 11 | public static final int INJECT_INPUT_EVENT_MODE_ASYNC = 0; 12 | public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1; 13 | public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; 14 | 15 | private final IInterface manager; 16 | private final Method injectInputEventMethod; 17 | 18 | public InputManager(IInterface manager) { 19 | this.manager = manager; 20 | try { 21 | injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); 22 | } catch (NoSuchMethodException e) { 23 | throw new AssertionError(e); 24 | } 25 | } 26 | 27 | public boolean injectInputEvent(InputEvent inputEvent, int mode) { 28 | try { 29 | return (Boolean) injectInputEventMethod.invoke(manager, inputEvent, mode); 30 | } catch (InvocationTargetException | IllegalAccessException e) { 31 | throw new AssertionError(e); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/InputMethodManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import android.os.Build; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | 7 | import com.genymobile.scrcpy.Ln; 8 | 9 | public final class InputMethodManager { 10 | private final IInterface manager; 11 | 12 | public InputMethodManager(IInterface manager) { this.manager = manager; } 13 | 14 | public boolean setInputMethodEnabled(String id, boolean enabled) { 15 | // The method was removed in Pie (API 28) 16 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) 17 | return true; 18 | try { 19 | Class cls = manager.getClass(); 20 | try { 21 | return (Boolean) cls.getMethod("setInputMethodEnabled", String.class, boolean.class).invoke(manager, id, enabled); 22 | } catch (NoSuchMethodException e) { 23 | Ln.e("setInputMethodEnabled", e); 24 | } 25 | } catch (Exception e) { 26 | Ln.e("setInputMethodEnabled", e); 27 | } 28 | return true; // Don't try to disable 29 | } 30 | 31 | public boolean setInputMethod(String id) { 32 | try { 33 | Class cls = manager.getClass(); 34 | try { 35 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) 36 | // In Android 10, setInputMethod became deprecated in InputMethodManager, and was removed 37 | // from IInputMethosManager. The deprecated method works essentially this way: 38 | Settings.put(Settings.SECURE, Settings.DEFAULT_INPUT_METHOD, id); 39 | else 40 | cls.getMethod("setInputMethod", IBinder.class, String.class).invoke(manager, null, id); 41 | return true; 42 | } catch (NoSuchMethodException e) { 43 | Ln.e("setInputMethod", e); 44 | } 45 | } catch (Exception e) { 46 | Ln.e("setInputMethod", e); 47 | } 48 | return false; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/PackageManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import android.os.IInterface; 4 | 5 | import com.genymobile.scrcpy.Ln; 6 | 7 | public final class PackageManager { 8 | private final IInterface manager; 9 | 10 | public PackageManager(IInterface manager) { 11 | this.manager = manager; 12 | } 13 | 14 | public boolean isPackageAvailable(String packageName) { 15 | try { 16 | Class cls = manager.getClass(); 17 | try { 18 | return (Boolean) cls.getMethod("isPackageAvailable", String.class, int.class).invoke(manager, packageName, 0); 19 | } catch (NoSuchMethodException e) { 20 | Ln.e("isPackageAvailable", e); 21 | } 22 | } catch (Exception e) { 23 | Ln.e("isPackageAvailable", e); 24 | } 25 | return false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.Build; 5 | import android.os.IInterface; 6 | 7 | import java.lang.reflect.InvocationTargetException; 8 | import java.lang.reflect.Method; 9 | 10 | public final class PowerManager { 11 | private final IInterface manager; 12 | private final Method isScreenOnMethod; 13 | 14 | public PowerManager(IInterface manager) { 15 | this.manager = manager; 16 | try { 17 | @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future 18 | String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; 19 | isScreenOnMethod = manager.getClass().getMethod(methodName); 20 | } catch (NoSuchMethodException e) { 21 | throw new AssertionError(e); 22 | } 23 | } 24 | 25 | public boolean isScreenOn() { 26 | try { 27 | return (Boolean) isScreenOnMethod.invoke(manager); 28 | } catch (InvocationTargetException | IllegalAccessException e) { 29 | throw new AssertionError(e); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | 7 | import java.lang.reflect.Method; 8 | 9 | import com.genymobile.scrcpy.Ln; 10 | 11 | @SuppressLint("PrivateApi") 12 | public final class ServiceManager { 13 | private final Method getServiceMethod; 14 | private final Method checkServiceMethod; 15 | 16 | private WindowManager windowManager; 17 | private DisplayManager displayManager; 18 | private InputManager inputManager; 19 | private PowerManager powerManager; 20 | private StatusBarManager statusBarManager; 21 | private ClipboardManager clipboardManager; 22 | private ActivityManager activityManager; 23 | private InputMethodManager inputMethodManager; 24 | private PackageManager packageManager; 25 | 26 | public static final int USER_CURRENT = -2; 27 | 28 | public ServiceManager() { 29 | try { 30 | getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); 31 | checkServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("checkService", String.class); 32 | } catch (Exception e) { 33 | Ln.e("ServiceManager", e); 34 | throw new AssertionError(e); 35 | } 36 | } 37 | 38 | private IInterface getService(String service, String type) { 39 | try { 40 | IBinder binder = (IBinder) getServiceMethod.invoke(null, service); 41 | Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class); 42 | return (IInterface) asInterfaceMethod.invoke(null, binder); 43 | } catch (Exception e) { 44 | Ln.e("getService("+service+")", e); 45 | throw new AssertionError(e); 46 | } 47 | } 48 | 49 | private IInterface checkService(String service, String type) { 50 | try { 51 | IBinder binder = (IBinder) checkServiceMethod.invoke(null, service); 52 | Method asInterfaceMethod = Class.forName(type).getMethod("asInterface", IBinder.class); 53 | return (IInterface) asInterfaceMethod.invoke(null, binder); 54 | } catch (Exception e) { 55 | Ln.e("checkService("+service+")", e); 56 | throw new AssertionError(e); 57 | } 58 | } 59 | 60 | public WindowManager getWindowManager() { 61 | if (windowManager == null) { 62 | windowManager = new WindowManager(getService("window", "android.view.IWindowManager")); 63 | } 64 | return windowManager; 65 | } 66 | 67 | public DisplayManager getDisplayManager() { 68 | if (displayManager == null) { 69 | displayManager = new DisplayManager(getService("display", "android.hardware.display.IDisplayManager")); 70 | } 71 | return displayManager; 72 | } 73 | 74 | public InputManager getInputManager() { 75 | if (inputManager == null) { 76 | inputManager = new InputManager(getService("input", "android.hardware.input.IInputManager")); 77 | } 78 | return inputManager; 79 | } 80 | 81 | public PowerManager getPowerManager() { 82 | if (powerManager == null) { 83 | powerManager = new PowerManager(getService("power", "android.os.IPowerManager")); 84 | } 85 | return powerManager; 86 | } 87 | 88 | public StatusBarManager getStatusBarManager() { 89 | if (statusBarManager == null) { 90 | statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService")); 91 | } 92 | return statusBarManager; 93 | } 94 | 95 | public ClipboardManager getClipboardManager() { 96 | if (clipboardManager == null) { 97 | clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard")); 98 | } 99 | return clipboardManager; 100 | } 101 | 102 | public ActivityManager getActivityManager() { 103 | if (activityManager == null) { 104 | // This does not work for Android 6 105 | // activityManager = new ActivityManager(getService("activity", "android.app.IActivityManager")); 106 | // This works for both Android 6 and 8 107 | activityManager = new ActivityManager(checkService("activity", "android.app.ActivityManagerNative")); 108 | } 109 | return activityManager; 110 | } 111 | 112 | public InputMethodManager getInputMethodManager() { 113 | if (inputMethodManager == null) { 114 | inputMethodManager = new InputMethodManager(getService("input_method", "com.android.internal.view.IInputMethodManager")); 115 | } 116 | return inputMethodManager; 117 | } 118 | 119 | public PackageManager getPackageManager() { 120 | if (packageManager == null) { 121 | packageManager = new PackageManager(getService("package", "android.content.pm.IPackageManager")); 122 | } 123 | return packageManager; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/Settings.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import com.genymobile.scrcpy.Ln; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.InputStreamReader; 7 | 8 | public final class Settings { 9 | public static final String SYSTEM = "system"; 10 | public static final String SECURE = "secure"; 11 | public static final String SCREEN_BRIGHTNESS = android.provider.Settings.System.SCREEN_BRIGHTNESS; 12 | public static final String HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY = "hide_rotation_lock_toggle_for_accessibility"; //android.provider.Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY; 13 | public static final String ACCELEROMETER_ROTATION = android.provider.Settings.System.ACCELEROMETER_ROTATION; 14 | public static final String USER_ROTATION = android.provider.Settings.System.USER_ROTATION; 15 | public static final String DEFAULT_INPUT_METHOD = "default_input_method"; 16 | 17 | public static String get(final String namespace, final String key) { 18 | String[] cmd = new String[]{"settings", "get", namespace, key}; 19 | try { 20 | final Process p = Runtime.getRuntime().exec(cmd); 21 | BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream(),"UTF-8")); 22 | String line = reader.readLine(); 23 | reader.close(); 24 | return line; 25 | } 26 | catch (Exception e) { 27 | Ln.e("Settings.get", e); 28 | } 29 | return ""; 30 | } 31 | 32 | public static void put(final String namespace, final String key, final String value) { 33 | String[] cmd = new String[]{"settings","put", namespace, key, value}; 34 | try { 35 | final Process p = Runtime.getRuntime().exec(cmd); 36 | p.waitFor(); 37 | } 38 | catch (Exception e) {} 39 | } 40 | 41 | public static void setDisableHWOverlays(boolean value) { 42 | String[] cmd = new String[]{"su","-c", "service", "call", "SurfaceFlinger", "1008", "i32", value ? "1" : "0"}; 43 | try { 44 | final Process p = Runtime.getRuntime().exec(cmd); 45 | p.waitFor(); 46 | } 47 | catch (Exception e) {} 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import com.genymobile.scrcpy.Ln; 4 | 5 | import android.os.IInterface; 6 | 7 | import java.lang.reflect.InvocationTargetException; 8 | import java.lang.reflect.Method; 9 | 10 | public class StatusBarManager { 11 | 12 | private final IInterface manager; 13 | private Method expandNotificationsPanelMethod; 14 | private Method collapsePanelsMethod; 15 | 16 | public StatusBarManager(IInterface manager) { 17 | this.manager = manager; 18 | } 19 | 20 | public void expandNotificationsPanel() { 21 | if (expandNotificationsPanelMethod == null) { 22 | try { 23 | expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); 24 | } catch (NoSuchMethodException e) { 25 | Ln.e("ServiceBarManager.expandNotificationsPanel() is not available on this device"); 26 | return; 27 | } 28 | } 29 | try { 30 | expandNotificationsPanelMethod.invoke(manager); 31 | } catch (InvocationTargetException | IllegalAccessException e) { 32 | Ln.e("Could not invoke ServiceBarManager.expandNotificationsPanel()", e); 33 | } 34 | } 35 | 36 | public void collapsePanels() { 37 | if (collapsePanelsMethod == null) { 38 | try { 39 | collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); 40 | } catch (NoSuchMethodException e) { 41 | Ln.e("ServiceBarManager.collapsePanels() is not available on this device"); 42 | return; 43 | } 44 | } 45 | try { 46 | collapsePanelsMethod.invoke(manager); 47 | } catch (InvocationTargetException | IllegalAccessException e) { 48 | Ln.e("Could not invoke ServiceBarManager.collapsePanels()", e); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy.wrappers; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.graphics.Rect; 5 | import android.os.Build; 6 | import android.os.IBinder; 7 | import android.view.Surface; 8 | 9 | @SuppressLint("PrivateApi") 10 | public final class SurfaceControl { 11 | 12 | private static final Class CLASS; 13 | 14 | // see 15 | public static final int POWER_MODE_OFF = 0; 16 | public static final int POWER_MODE_NORMAL = 2; 17 | 18 | static { 19 | try { 20 | CLASS = Class.forName("android.view.SurfaceControl"); 21 | } catch (ClassNotFoundException e) { 22 | throw new AssertionError(e); 23 | } 24 | } 25 | 26 | private SurfaceControl() { 27 | // only static methods 28 | } 29 | 30 | public static void openTransaction() { 31 | try { 32 | CLASS.getMethod("openTransaction").invoke(null); 33 | } catch (Exception e) { 34 | throw new AssertionError(e); 35 | } 36 | } 37 | 38 | public static void closeTransaction() { 39 | try { 40 | CLASS.getMethod("closeTransaction").invoke(null); 41 | } catch (Exception e) { 42 | throw new AssertionError(e); 43 | } 44 | } 45 | 46 | public static void setDisplayProjection(IBinder displayToken, int orientation, Rect layerStackRect, Rect displayRect) { 47 | try { 48 | CLASS.getMethod("setDisplayProjection", IBinder.class, int.class, Rect.class, Rect.class) 49 | .invoke(null, displayToken, orientation, layerStackRect, displayRect); 50 | } catch (Exception e) { 51 | throw new AssertionError(e); 52 | } 53 | } 54 | 55 | public static void setDisplayLayerStack(IBinder displayToken, int layerStack) { 56 | try { 57 | CLASS.getMethod("setDisplayLayerStack", IBinder.class, int.class).invoke(null, displayToken, layerStack); 58 | } catch (Exception e) { 59 | throw new AssertionError(e); 60 | } 61 | } 62 | 63 | public static void setDisplaySurface(IBinder displayToken, Surface surface) { 64 | try { 65 | CLASS.getMethod("setDisplaySurface", IBinder.class, Surface.class).invoke(null, displayToken, surface); 66 | } catch (Exception e) { 67 | throw new AssertionError(e); 68 | } 69 | } 70 | 71 | public static IBinder createDisplay(String name, boolean secure) { 72 | try { 73 | return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure); 74 | } catch (Exception e) { 75 | throw new AssertionError(e); 76 | } 77 | } 78 | 79 | public static IBinder getBuiltInDisplay(int builtInDisplayId) { 80 | try { 81 | // the method signature has changed in Android Q 82 | // 83 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { 84 | return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId); 85 | } 86 | return (IBinder) CLASS.getMethod("getPhysicalDisplayToken", long.class).invoke(null, builtInDisplayId); 87 | } catch (Exception e) { 88 | throw new AssertionError(e); 89 | } 90 | } 91 | 92 | public static void setDisplayPowerMode(IBinder displayToken, int mode) { 93 | try { 94 | CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class).invoke(null, displayToken, mode); 95 | } catch (Exception e) { 96 | throw new AssertionError(e); 97 | } 98 | } 99 | 100 | public static void destroyDisplay(IBinder displayToken) { 101 | try { 102 | CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken); 103 | } catch (Exception e) { 104 | throw new AssertionError(e); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.DataOutputStream; 8 | import java.io.IOException; 9 | import java.nio.charset.StandardCharsets; 10 | 11 | public class DeviceMessageWriterTest { 12 | 13 | @Test 14 | public void testSerializeClipboard() throws IOException { 15 | DeviceMessageWriter writer = new DeviceMessageWriter(); 16 | 17 | String text = "aéûoç"; 18 | byte[] data = text.getBytes(StandardCharsets.UTF_8); 19 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 20 | DataOutputStream dos = new DataOutputStream(bos); 21 | dos.writeByte(DeviceMessage.TYPE_CLIPBOARD); 22 | dos.writeShort(data.length); 23 | dos.write(data); 24 | 25 | byte[] expected = bos.toByteArray(); 26 | 27 | DeviceMessage msg = DeviceMessage.createClipboard(text); 28 | bos = new ByteArrayOutputStream(); 29 | writer.writeTo(msg, bos); 30 | 31 | byte[] actual = bos.toByteArray(); 32 | 33 | Assert.assertArrayEquals(expected, actual); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.genymobile.scrcpy; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | 8 | public class StringUtilsTest { 9 | 10 | @Test 11 | @SuppressWarnings("checkstyle:MagicNumber") 12 | public void testUtf8Truncate() { 13 | String s = "aÉbÔc"; 14 | byte[] utf8 = s.getBytes(StandardCharsets.UTF_8); 15 | Assert.assertEquals(7, utf8.length); 16 | 17 | int count; 18 | 19 | count = StringUtils.getUtf8TruncationIndex(utf8, 1); 20 | Assert.assertEquals(1, count); 21 | 22 | count = StringUtils.getUtf8TruncationIndex(utf8, 2); 23 | Assert.assertEquals(1, count); // É is 2 bytes-wide 24 | 25 | count = StringUtils.getUtf8TruncationIndex(utf8, 3); 26 | Assert.assertEquals(3, count); 27 | 28 | count = StringUtils.getUtf8TruncationIndex(utf8, 4); 29 | Assert.assertEquals(4, count); 30 | 31 | count = StringUtils.getUtf8TruncationIndex(utf8, 5); 32 | Assert.assertEquals(4, count); // Ô is 2 bytes-wide 33 | 34 | count = StringUtils.getUtf8TruncationIndex(utf8, 6); 35 | Assert.assertEquals(6, count); 36 | 37 | count = StringUtils.getUtf8TruncationIndex(utf8, 7); 38 | Assert.assertEquals(7, count); 39 | 40 | count = StringUtils.getUtf8TruncationIndex(utf8, 8); 41 | Assert.assertEquals(7, count); // no more chars 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':server' 2 | --------------------------------------------------------------------------------