diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..1c04da7f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + + - [ ] I have read the [FAQ](https://github.com/Genymobile/scrcpy/blob/master/FAQ.md). + - [ ] I have searched in existing [issues](https://github.com/Genymobile/scrcpy/issues). + +**Environment** + - OS: [e.g. Debian, Windows, macOS...] + - scrcpy version: [e.g. 1.12.1] + - installation method: [e.g. manual build, apt, snap, brew, Windows release...] + - device model: + - Android version: [e.g. 10] + +**Describe the bug** +A clear and concise description of what the bug is. + +On errors, please provide the output of the console (and `adb logcat` if relevant). + +``` +Please paste terminal output in a code block. +``` + +Please do not post screenshots of your terminal, just post the content as text instead. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..524c370f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + + - [ ] I have checked that a similar [feature request](https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3A%22feature+request%22) does not already exist. + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore index 59bc840d..f152b4b8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ build/ /dist/ .idea/ .gradle/ +.vs/ +/x/ diff --git a/BUILD.md b/BUILD.md index e35d07d0..e4f175ef 100644 --- a/BUILD.md +++ b/BUILD.md @@ -8,6 +8,22 @@ case, use the [prebuilt server] (so you will not need Java or the Android SDK). [prebuilt server]: #prebuilt-server +## Branches + +### `master` + +The `master` branch concerns the latest release, and is the home page of the +project on Github. + + +### `dev` + +`dev` is the current development branch. Every commit present in `dev` will be +in the next release. + +If you want to contribute code, please base your commits on the latest `dev` +branch. + ## Requirements @@ -233,10 +249,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.12.1`][direct-scrcpy-server] - _(SHA-256: 63e569c8a1d0c1df31d48c4214871c479a601782945fed50c1e61167d78266ea)_ + - [`scrcpy-server-v1.13`][direct-scrcpy-server] + _(SHA-256: 5fee64ca1ccdc2f38550f31f5353c66de3de30c2e929a964e30fa2d005d5f885)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-server-v1.12.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-server-v1.13 Download the prebuilt server somewhere, and specify its path during the Meson configuration: @@ -247,3 +263,6 @@ meson x --buildtype release --strip -Db_lto=true \ ninja -Cx sudo ninja -Cx install ``` + +The server only works with a matching client version (this server works with the +`master` branch). diff --git a/DEVELOP.md b/DEVELOP.md index 0258782f..4d8acc59 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -282,6 +282,15 @@ meson x -Dserver_debugger=true meson configure x -Dserver_debugger=true ``` +If your device runs Android 8 or below, set the `server_debugger_method` to +`old` in addition: + +```bash +meson x -Dserver_debugger=true -Dserver_debugger_method=old +# or, if x is already configured +meson configure x -Dserver_debugger=true -Dserver_debugger_method=old +``` + Then recompile. When you start scrcpy, it will start a debugger on port 5005 on the device. diff --git a/FAQ.md b/FAQ.md index 49382471..871ae8f0 100644 --- a/FAQ.md +++ b/FAQ.md @@ -3,19 +3,102 @@ Here are the common reported problems and their status. -### On Windows, my device is not detected +## `adb` issues -The most common is your device not being detected by `adb`, or is unauthorized. -Check everything is ok by calling: +`scrcpy` execute `adb` commands to initialize the connection with the device. If +`adb` fails, then scrcpy will not work. - adb devices +In that case, it will print this error: -Windows may need some [drivers] to detect your device. +> ERROR: "adb push" returned with value 1 + +This is typically not a bug in _scrcpy_, but a problem in your environment. + +To find out the cause, execute: + +```bash +adb devices +``` + +### `adb` not found + +You need `adb` accessible from your `PATH`. + +On Windows, the current directory is in your `PATH`, and `adb.exe` is included +in the release, so it should work out-of-the-box. + + +### Device unauthorized + +Check [stackoverflow][device-unauthorized]. + +[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized + + +### Device not detected + +If your device is not detected, you may need some [drivers] (on Windows). [drivers]: https://developer.android.com/studio/run/oem-usb.html -### I can only mirror, I cannot interact with the device +### Several devices connected + +If several devices are connected, you will encounter this error: + +> adb: error: failed to get feature set: more than one device/emulator + +the identifier of the device you want to mirror must be provided: + +```bash +scrcpy -s 01234567890abcdef +``` + +Note that if your device is connected over TCP/IP, you'll get this message: + +> adb: error: more than one device/emulator +> ERROR: "adb reverse" returned with value 1 +> WARN: 'adb reverse' failed, fallback to 'adb forward' + +This is expected (due to a bug on old Android versions, see [#5]), but in that +case, scrcpy fallbacks to a different method, which should work. + +[#5]: https://github.com/Genymobile/scrcpy/issues/5 + + +### Conflicts between adb versions + +> adb server version (41) doesn't match this client (39); killing... + +This error occurs when you use several `adb` versions simultaneously. You must +find the program using a different `adb` version, and use the same `adb` version +everywhere. + +You could overwrite the `adb` binary in the other program, or ask _scrcpy_ to +use a specific `adb` binary, by setting the `ADB` environment variable: + +```bash +set ADB=/path/to/your/adb +scrcpy +``` + + +### Device disconnected + +If _scrcpy_ stops itself with the warning "Device disconnected", then the +`adb` connection has been closed. + +Try with another USB cable or plug it into another USB port. See [#281] and +[#283]. + +[#281]: https://github.com/Genymobile/scrcpy/issues/281 +[#283]: https://github.com/Genymobile/scrcpy/issues/283 + + + +## Control issues + +### Mouse and keyboard do not work On some devices, you may need to enable an option to allow [simulating input]. In developer options, enable: @@ -29,22 +112,43 @@ In developer options, enable: ### Mouse clicks at wrong location On MacOS, with HiDPI support and multiple screens, input location are wrongly -scaled. See [issue 15]. +scaled. See [#15]. -[issue 15]: https://github.com/Genymobile/scrcpy/issues/15 +[#15]: https://github.com/Genymobile/scrcpy/issues/15 -A workaround is to build with HiDPI support disabled: +Open _scrcpy_ directly on the monitor you use it. -```bash -meson x --buildtype release -Dhidpi_support=false + +### Special characters do not work + +Injecting text input is [limited to ASCII characters][text-input]. A trick +allows to also inject some [accented characters][accented-characters], but +that's all. See [#37]. + +[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode +[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters +[#37]: https://github.com/Genymobile/scrcpy/issues/37 + + +## Client issues + +### The quality is low + +If the definition of your client window is smaller than that of your device +screen, then you might get poor quality, especially visible on text (see [#40]). + +[#40]: https://github.com/Genymobile/scrcpy/issues/40 + +To improve downscaling quality, trilinear filtering is enabled automatically +if the renderer is OpenGL and if it supports mipmapping. + +On Windows, you might want to force OpenGL: + +``` +scrcpy --render-driver=opengl ``` -However, the video will be displayed at lower resolution. - - -### The quality is low on HiDPI display - -On Windows, you may need to configure the [scaling behavior]. +You may also need to configure the [scaling behavior]: > `scrcpy.exe` > Properties > Compatibility > Change high DPI settings > > Override high DPI scaling behavior > Scaling performed by: _Application_. @@ -52,6 +156,7 @@ On Windows, you may need to configure the [scaling behavior]. [scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 + ### KWin compositor crashes On Plasma Desktop, compositor is disabled while _scrcpy_ is running. @@ -61,19 +166,29 @@ As a workaround, [disable "Block compositing"][kwin]. [kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 -### I get an error "Could not open video stream" +## Crashes + +### Exception There may be many reasons. One common cause is that the hardware encoder of your device is not able to encode at the given definition: -``` -ERROR: Exception on thread Thread[main,5,main] -android.media.MediaCodec$CodecException: Error 0xfffffc0e -... -Exit due to uncaughtException in main thread: -ERROR: Could not open video stream -INFO: Initial texture: 1080x2336 -``` +> ``` +> ERROR: Exception on thread Thread[main,5,main] +> android.media.MediaCodec$CodecException: Error 0xfffffc0e +> ... +> Exit due to uncaughtException in main thread: +> ERROR: Could not open video stream +> INFO: Initial texture: 1080x2336 +> ``` + +or + +> ``` +> ERROR: Exception on thread Thread[main,5,main] +> java.lang.IllegalStateException +> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) +> ``` Just try with a lower definition: diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 2b30dcb5..3aed2019 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -100,30 +100,30 @@ dist-win32: build-server build-win32 build-win32-noconsole cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/SDL2-2.0.10/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.12/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 build-win64-noconsole mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/SDL2-2.0.10/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.12/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ diff --git a/README.ko.md b/README.ko.md index b232accd..4e6d8fc5 100644 --- a/README.ko.md +++ b/README.ko.md @@ -1,3 +1,5 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + # scrcpy (v1.11) This document will be updated frequently along with the original Readme file diff --git a/README.md b/README.md index 6684d8ca..04b644a4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.12.1) +# scrcpy (v1.13) This application provides display and control of Android devices connected on USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. @@ -37,7 +37,7 @@ control it using keyboard and mouse. ### Linux -In Debian (_testing_ and _sid_ for now): +On Debian (_testing_ and _sid_ for now) and Ubuntu (20.04): ``` apt install scrcpy @@ -66,16 +66,13 @@ hard). ### Windows -For Windows, for simplicity, prebuilt archives with all the dependencies -(including `adb`) are available: +For Windows, for simplicity, a prebuilt archive with all the dependencies +(including `adb`) is available: - - [`scrcpy-win32-v1.12.1.zip`][direct-win32] - _(SHA-256: 0f4b3b063536b50a2df05dc42c760f9cc0093a9a26dbdf02d8232c74dab43480)_ - - [`scrcpy-win64-v1.12.1.zip`][direct-win64] - _(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_ + - [`scrcpy-win64-v1.13.zip`][direct-win64] + _(SHA-256: 806aafc00d4db01513193addaa24f47858893ba5efe75770bfef6ae1ea987d27)_ -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win32-v1.12.1.zip -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-win64-v1.13.zip It is also available in [Chocolatey]: @@ -83,14 +80,18 @@ It is also available in [Chocolatey]: ```bash choco install scrcpy +choco install adb # if you don't have it yet ``` -You need `adb`, accessible from your `PATH`. If you don't have it yet: +And in [Scoop]: ```bash -choco install adb +scoop install scrcpy +scoop install adb # if you don't have it yet ``` +[Scoop]: https://scoop.sh + You can also [build the app manually][BUILD]. @@ -158,12 +159,14 @@ scrcpy -b 2M # short version #### Limit frame rate -On devices with Android >= 10, the capture frame rate can be limited: +The capture frame rate can be limited: ```bash scrcpy --max-fps 15 ``` +This is officially supported since Android 10, but may work on earlier versions. + #### Crop The device screen may be cropped to mirror only part of the screen. @@ -177,6 +180,21 @@ scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) If `--max-size` is also specified, resizing is applied after cropping. +#### Lock video orientation + + +To lock the orientation of the mirroring: + +```bash +scrcpy --lock-video-orientation 0 # natural orientation +scrcpy --lock-video-orientation 1 # 90° counterclockwise +scrcpy --lock-video-orientation 2 # 180° +scrcpy --lock-video-orientation 3 # 90° clockwise +``` + +This affects recording orientation. + + ### Recording It is possible to record the screen while mirroring: @@ -201,22 +219,6 @@ variation] does not impact the recorded file. [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation -### Serve - -It is possible to forward the video stream: - -```bash -scrcpy --serve tcp:localhost:1234 -``` - -To disable mirroring while forwarding the stream: - -```bash -scrcpy --no-display --serve tcp:localhost:1234 -scrcpy -N --serve tcp:localhost:1234 -# interrupt serve with Ctrl+C -# Ctrl+C does not terminate properly on Windows, so disconnect the device -``` ### Connection @@ -260,6 +262,16 @@ scrcpy -s 192.168.0.1:5555 # short version You can start several instances of _scrcpy_ for several devices. +#### Autostart on device connection + +You could use [AutoAdb]: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + #### SSH tunnel To connect to a remote device, it is possible to connect a local `adb` client to @@ -329,6 +341,33 @@ scrcpy -f # short version Fullscreen can then be toggled dynamically with `Ctrl`+`f`. +#### Rotation + +The window may be rotated: + +```bash +scrcpy --rotation 1 +``` + +Possibles values are: + - `0`: no rotation + - `1`: 90 degrees counterclockwise + - `2`: 180 degrees + - `3`: 90 degrees clockwise + +The rotation can also be changed dynamically with `Ctrl`+`←` _(left)_ and +`Ctrl`+`→` _(right)_. + +Note that _scrcpy_ manages 3 different rotations: + - `Ctrl`+`r` requests the device to switch between portrait and landscape (the + current running app may refuse, if it does support the requested + orientation). + - `--lock-video-orientation` changes the mirroring orientation (the orientation + of the video sent from the device to the computer). This affects the + recording. + - `--rotation` (or `Ctrl`+`←`/`Ctrl`+`→`) rotates only the window content. This + affects only the display, not the recording. + ### Other mirroring options @@ -342,6 +381,25 @@ scrcpy --no-control scrcpy -n ``` +#### Display + +If several displays are available, it is possible to select the display to +mirror: + +```bash +scrcpy --display 1 +``` + +The list of display ids can be retrieved by: + +``` +adb shell dumpsys display # search "mDisplayId=" in the output +``` + +The secondary display may only be controlled if the device runs at least Android +10 (otherwise it is mirrored in read-only). + + #### Turn screen off It is possible to turn the device screen off while mirroring on start with a @@ -465,6 +523,8 @@ Also see [issue #14]. | Action | Shortcut | Shortcut (macOS) | -------------------------------------- |:----------------------------- |:----------------------------- | Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f` + | Rotate display left | `Ctrl`+`←` _(left)_ | `Cmd`+`←` _(left)_ + | Rotate display right | `Ctrl`+`→` _(right)_ | `Cmd`+`→` _(right)_ | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g` | Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ | Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ diff --git a/README.pt-br.md b/README.pt-br.md new file mode 100644 index 00000000..654f62cb --- /dev/null +++ b/README.pt-br.md @@ -0,0 +1,531 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + +# scrcpy (v1.12.1) + +Esta aplicação fornece visualização e controle de dispositivos Android conectados via +USB (ou [via TCP/IP][article-tcpip]). Não requer nenhum acesso root. +Funciona em _GNU/Linux_, _Windows_ e _macOS_. + +![screenshot](assets/screenshot-debian-600.jpg) + +Foco em: + + - **leveza** (Nativo, mostra apenas a tela do dispositivo) + - **performance** (30~60fps) + - **qualidade** (1920×1080 ou acima) + - **baixa latência** ([35~70ms][lowlatency]) + - **baixo tempo de inicialização** (~1 segundo para mostrar a primeira imagem) + - **não intrusivo** (nada é deixado instalado no dispositivo) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## Requisitos + +O Dispositivo Android requer pelo menos a API 21 (Android 5.0). + + +Tenha certeza de ter [ativado a depuração USB][enable-adb] no(s) seu(s) dispositivo(s). + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + + +Em alguns dispositivos, você também precisará ativar [uma opção adicional][control] para controlá-lo usando o teclado e mouse. + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## Obtendo o app + + +### Linux + +No Debian (_em testes_ e _sid_ por enquanto): + +``` +apt install scrcpy +``` + +O pacote [Snap] está disponível: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +Para Arch Linux, um pacote [AUR] está disponível: [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +Para Gentoo, uma [Ebuild] está disponível: [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + + +Você também pode [compilar a aplicação manualmente][BUILD] (não se preocupe, não é tão difícil). + + +### Windows + +Para Windows, para simplicidade, um arquivo pré-compilado com todas as dependências +(incluindo `adb`) está disponível: + + - [`scrcpy-win64-v1.12.1.zip`][direct-win64] + _(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_ + +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip + +Também disponível em [Chocolatey]: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # se você ainda não o tem +``` + +E no [Scoop]: + +```bash +scoop install scrcpy +scoop install adb # se você ainda não o tem +``` + +[Scoop]: https://scoop.sh + +Você também pode [compilar a aplicação manualmente][BUILD]. + + +### macOS + +A aplicação está disponível em [Homebrew]. Apenas a instale: + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +Você precisa do `adb`, acessível através do seu `PATH`. Se você ainda não o tem: + +```bash +brew cask install android-platform-tools +``` + +Você também pode [compilar a aplicação manualmente][BUILD]. + + +## Executar + +Plugue um dispositivo Android e execute: + +```bash +scrcpy +``` + +Também aceita argumentos de linha de comando, listados por: + +```bash +scrcpy --help +``` + +## Funcionalidades + +### Configuração de captura + +#### Redução de tamanho + +Algumas vezes, é útil espelhar um dispositivo Android em uma resolução menor para +aumentar performance. + +Para limitar ambos(largura e altura) para algum valor (ex: 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # versão reduzida +``` + +A outra dimensão é calculada para que a proporção do dispositivo seja preservada. +Dessa forma, um dispositivo em 1920x1080 será espelhado em 1024x576. + + +#### Mudanças no bit-rate + +O Padrão de bit-rate é 8 mbps. Para mudar o bitrate do vídeo (ex: para 2 Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # versão reduzida +``` + +#### Limitar frame rates + +Em dispositivos com Android >= 10, a captura de frame rate pode ser limitada: + +```bash +scrcpy --max-fps 15 +``` + +#### Cortar + +A tela do dispositivo pode ser cortada para espelhar apenas uma parte da tela. + +Isso é útil por exemplo, ao espelhar apenas um olho do Oculus Go: + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 no deslocamento (0,0) +``` + +Se `--max-size` também for especificado, redimensionar é aplicado após os cortes. + + +### Gravando + +É possível gravar a tela enquanto ocorre o espelhamento: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +Para desativar o espelhamento durante a gravação: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# interrompe a gravação com Ctrl+C +# Ctrl+C não encerrar propriamente no Windows, então desconecte o dispositivo +``` + +"Frames pulados" são gravados, mesmo que não sejam mostrado em tempo real (por motivos de performance). +Frames tem seu _horário_ _carimbado_ no dispositivo, então [Variação de atraso nos pacotes] não impacta na gravação do arquivo. + +[Variação de atraso de pacote]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +### Conexão + +#### Wireless/Sem fio + +_Scrcpy_ usa `adb` para se comunicar com o dispositivo, e `adb` pode [conectar-se] à um dispositivo via TCP/IP: + +1. Conecte o dispositivo a mesma rede Wi-Fi do seu computador. +2. Pegue o endereço de IP do seu dispositivo (Em Configurações → Sobre o Telefone → Status). +3. Ative o adb via TCP/IP no seu dispositivo: `adb tcpip 5555`. +4. Desplugue seu dispositivo. +5. Conecte-se ao seu dispositivo: `adb connect DEVICE_IP:5555` _(substitua o `DEVICE_IP`)_. +6. Execute `scrcpy` como de costume. + +Pode ser útil diminuir o bit-rate e a resolução: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # versão reduzida +``` + +[conectar-se]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### N-dispositivos + +Se alguns dispositivos estão listados em `adb devices`, você precisa especificar o _serial_: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # versão reduzida +``` + +Se o dispositivo está conectado via TCP/IP: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # versão reduzida +``` + +Você pode iniciar algumas instâncias do _scrcpy_ para alguns dispositivos. + +#### Conexão via SSH + +Para conectar-se à um dispositivo remoto, é possível se conectar um cliente local `adb` à um servidor `adb` remoto (contanto que eles usem a mesma versão do protocolo _adb_): + +```bash +adb kill-server # encerra o servidor local na 5037 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# mantém isso aberto +``` + +De outro terminal: + +```bash +scrcpy +``` + +Igual para conexões sem fio, pode ser útil reduzir a qualidade: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### Configurações de Janela + +#### Título + +Por padrão, o título da janela é o modelo do dispositivo. Isto pode ser mudado: + +```bash +scrcpy --window-title 'Meu dispositivo' +``` + +#### Posição e tamanho + +A posição e tamanho iniciais da janela podem ser especificados: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### Sem bordas + +Para desativar decorações da janela: + +```bash +scrcpy --window-borderless +``` + +#### Sempre visível + +Para manter a janela do scrcpy sempre visível: + +```bash +scrcpy --always-on-top +``` + +#### Tela cheia + +A aplicação pode ser iniciada diretamente em tela cheia: + +```bash +scrcpy --fullscreen +scrcpy -f # versão reduzida +``` + +Tela cheia pode ser alternada dinamicamente com `Ctrl`+`f`. + + +### Outras opções de espelhamento + +#### Apenas leitura + +Para desativar controles (tudo que possa interagir com o dispositivo: teclas de entrada, eventos de mouse, arrastar e soltar arquivos): + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### Desligar a tela + +É possível desligar a tela do dispositivo durante o início do espelhamento com uma opção de linha de comando: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +Ou apertando `Ctrl`+`o` durante qualquer momento. + +Para ligar novamente, pressione `POWER` (ou `Ctrl`+`p`). + +#### Frames expirados de renderização + +Por padrão, para minimizar a latência, _scrcpy_ sempre renderiza o último frame decodificado disponível e descarta o anterior. + +Para forçar a renderização de todos os frames ( com o custo de aumento de latência), use: + +```bash +scrcpy --render-expired-frames +``` + +#### Mostrar toques + +Para apresentações, pode ser útil mostrar toques físicos(dispositivo físico). + +Android fornece esta funcionalidade nas _Opções do Desenvolvedor_. + +_Scrcpy_ fornece esta opção de ativar esta funcionalidade no início e desativar no encerramento: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +Note que isto mostra apenas toques _físicos_ (com o dedo no dispositivo). + + +### Controle de entrada + +#### Rotacionar a tela do dispositivo + +Pressione `Ctrl`+`r` para mudar entre os modos Retrato e Paisagem. + +Note que só será rotacionado se a aplicação em primeiro plano tiver suporte para o modo requisitado. + +#### Copiar-Colar + +É possível sincronizar áreas de transferência entre computador e o dispositivo, +para ambas direções: + + - `Ctrl`+`c` copia a área de transferência do dispositivo para a área de trasferência do computador; + - `Ctrl`+`Shift`+`v` copia a área de transferência do computador para a área de transferência do dispositivo; + - `Ctrl`+`v` _cola_ a área de transferência do computador como uma sequência de eventos de texto (mas + quebra caracteres não-ASCII). + +#### Preferências de injeção de texto + +Existe dois tipos de [eventos][textevents] gerados ao digitar um texto: + - _eventos de teclas_, sinalizando que a tecla foi pressionada ou solta; + - _eventos de texto_, sinalizando que o texto foi inserido. + +Por padrão, letras são injetadas usando eventos de teclas, assim teclados comportam-se +como esperado em jogos (normalmente para tecladas WASD) + +Mas isto pode [causar problemas][prefertext]. Se você encontrar tal problema, +pode evitá-lo usando: + +```bash +scrcpy --prefer-text +``` + +(mas isto vai quebrar o comportamento do teclado em jogos) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +### Transferência de arquivo + +#### Instalar APK + +Para instalar um APK, arraste e solte o arquivo APK(com extensão `.apk`) na janela _scrcpy_. + +Não existe feedback visual, um log é imprimido no console. + + +#### Enviar arquivo para o dispositivo + +Para enviar um arquivo para o diretório `/sdcard/` no dispositivo, arraste e solte um arquivo não APK para a janela do +_scrcpy_. + +Não existe feedback visual, um log é imprimido no console. + +O diretório alvo pode ser mudado ao iniciar: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` + + +### Encaminhamento de áudio + +Áudio não é encaminhando pelo _scrcpy_. Use [USBaudio] (Apenas linux). + +Também veja [issue #14]. + +[USBaudio]: https://github.com/rom1v/usbaudio +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## Atalhos + + | Ação | Atalho | Atalho (macOS) + | ------------------------------------------------------------- |:------------------------------- |:----------------------------- + | Alternar para modo de tela cheia | `Ctrl`+`f` | `Cmd`+`f` + | Redimensionar janela para pixel-perfect(Escala 1:1) | `Ctrl`+`g` | `Cmd`+`g` + | Redimensionar janela para tirar as bordas pretas | `Ctrl`+`x` \| _Clique-duplo¹_ | `Cmd`+`x` \| _Clique-duplo¹_ + | Clicar em `HOME` | `Ctrl`+`h` \| _Clique-central_ | `Ctrl`+`h` \| _Clique-central_ + | Clicar em `BACK` | `Ctrl`+`b` \| _Clique-direito²_ | `Cmd`+`b` \| _Clique-direito²_ + | Clicar em `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s` + | Clicar em `MENU` | `Ctrl`+`m` | `Ctrl`+`m` + | Clicar em `VOLUME_UP` | `Ctrl`+`↑` _(cima)_ | `Cmd`+`↑` _(cima)_ + | Clicar em `VOLUME_DOWN` | `Ctrl`+`↓` _(baixo)_ | `Cmd`+`↓` _(baixo)_ + | Clicar em `POWER` | `Ctrl`+`p` | `Cmd`+`p` + | Ligar | _Clique-direito²_ | _Clique-direito²_ + | Desligar a tela do dispositivo | `Ctrl`+`o` | `Cmd`+`o` + | Rotacionar tela do dispositivo | `Ctrl`+`r` | `Cmd`+`r` + | Expandir painel de notificação | `Ctrl`+`n` | `Cmd`+`n` + | Esconder painel de notificação | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` + | Copiar área de transferência do dispositivo para o computador | `Ctrl`+`c` | `Cmd`+`c` + | Colar área de transferência do computador para o dispositivo | `Ctrl`+`v` | `Cmd`+`v` + | Copiar área de transferência do computador para dispositivo | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` + | Ativar/desativar contador de FPS(Frames por segundo) | `Ctrl`+`i` | `Cmd`+`i` + +_¹Clique-duplo em bordas pretas para removê-las._ +_²Botão direito liga a tela se ela estiver desligada, clique BACK para o contrário._ + + +## Caminhos personalizados + +Para usar um binário específico _adb_, configure seu caminho na variável de ambiente `ADB`: + + ADB=/caminho/para/adb scrcpy + +Para sobrepor o caminho do arquivo `scrcpy-server`, configure seu caminho em +`SCRCPY_SERVER_PATH`. + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + + +## Por quê _scrcpy_? + +Um colega me desafiou a encontrar um nome impronunciável como [gnirehtet]. + +[`strcpy`] copia uma **str**ing; `scrcpy` copia uma **scr**een. + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + +## Como compilar? + +Veja [BUILD]. + +[BUILD]: BUILD.md + + +## Problemas comuns + +Veja [FAQ](FAQ.md). + + +## Desenvolvedores + +Leia a [developers page]. + +[developers page]: DEVELOP.md + + +## Licença + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-2020 Romain Vimont + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +## Artigos + +- [Introducing scrcpy][article-intro] +- [Scrcpy now works wirelessly][article-tcpip] + +[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ +[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ diff --git a/app/meson.build b/app/meson.build index fbda1340..5d2b4caa 100644 --- a/app/meson.build +++ b/app/meson.build @@ -11,11 +11,11 @@ src = [ 'src/file_handler.c', 'src/fps_counter.c', 'src/input_manager.c', + 'src/opengl.c', 'src/receiver.c', 'src/recorder.c', 'src/scrcpy.c', 'src/screen.c', - 'src/serve.c', 'src/server.c', 'src/stream.c', 'src/tiny_xpm.c', @@ -77,11 +77,9 @@ cc = meson.get_compiler('c') if host_machine.system() == 'windows' src += [ 'src/sys/win/command.c' ] - src += [ 'src/sys/win/net.c' ] dependencies += cc.find_library('ws2_32') else src += [ 'src/sys/unix/command.c' ] - src += [ 'src/sys/unix/net.c' ] endif conf = configuration_data() @@ -99,14 +97,21 @@ conf.set_quoted('PREFIX', get_option('prefix')) # directory as the executable) conf.set('PORTABLE', get_option('portable')) -# the default client TCP port for the "adb reverse" tunnel +# the default client TCP port range for the "adb reverse" tunnel # overridden by option --port -conf.set('DEFAULT_LOCAL_PORT', '27183') +conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183') +conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199') # the default max video size for both dimensions, in pixels # overridden by option --max-size conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited +# the default video orientation +# natural device orientation is 0 and each increment adds 90 degrees +# counterclockwise +# overridden by option --lock-video-orientation +conf.set('DEFAULT_LOCK_VIDEO_ORIENTATION', '-1') # -1: unlocked + # the default video bitrate, in bits/second # overridden by option --bit-rate conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps @@ -120,6 +125,9 @@ conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole')) # run a server debugger and wait for a client to be attached conf.set('SERVER_DEBUGGER', get_option('server_debugger')) +# select the debugger method ('old' for Android < 9, 'new' for Android >= 9) +conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new') + configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 9560df1c..673cd11d 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -33,6 +33,15 @@ The values are expressed in the device natural orientation (typically, portrait .B \-\-max\-size value is computed on the cropped size. +.TP +.BI "\-\-display " id +Specify the display id to mirror. + +The list of possible display ids can be listed by "adb shell dumpsys display" +(search "mDisplayId=" in the output). + +Default is 0. + .TP .B \-f, \-\-fullscreen Start in fullscreen. @@ -41,9 +50,15 @@ Start in fullscreen. .B \-h, \-\-help Print this help. +.TP +.BI "\-\-lock\-video\-orientation " value +Lock video orientation to \fIvalue\fR. Possible values are -1 (unlocked), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees otation counterclockwise. + +Default is -1 (unlocked). + .TP .BI "\-\-max\-fps " value -Limit the framerate of screen capture (only supported on devices with Android >= 10). +Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions). .TP .BI "\-m, \-\-max\-size " value @@ -60,10 +75,14 @@ Disable device control (mirror the device in read\-only). Do not display device (only when screen recording is enabled). .TP -.BI "\-p, \-\-port " port -Set the TCP port the client listens on. +.B \-\-no\-mipmaps +If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps. -Default is 27183. +.TP +.BI "\-p, \-\-port " port[:port] +Set the TCP port (range) used by the client to listen. + +Default is 27183:27199. .TP .B \-\-prefer\-text @@ -91,10 +110,22 @@ option if set, or by the file extension (.mp4 or .mkv). .BI "\-\-record\-format " format Force recording format (either mp4 or mkv). +.TP +.BI "\-\-render\-driver " name +Request SDL to use the given render driver (this is just a hint). + +Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software". +.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER +.UE + .TP .B \-\-render\-expired\-frames By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency. +.TP +.BI "\-\-rotation " value +Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise. + .TP .BI "\-s, \-\-serial " number The device serial number. Mandatory only if several devices are connected to adb. @@ -125,13 +156,13 @@ Set a custom window title. .BI "\-\-window\-x " value Set the initial window horizontal position. -Default is -1 (automatic).\n +Default is "auto".\n .TP .BI "\-\-window\-y " value Set the initial window vertical position. -Default is -1 (automatic).\n +Default is "auto".\n .TP .BI "\-\-window\-width " value @@ -149,15 +180,23 @@ Default is 0 (automatic).\n .TP .B Ctrl+f -switch fullscreen mode +Switch fullscreen mode + +.TP +.B Ctrl+Left +Rotate display left + +.TP +.B Ctrl+Right +Rotate display right .TP .B Ctrl+g -resize window to 1:1 (pixel\-perfect) +Resize window to 1:1 (pixel\-perfect) .TP .B Ctrl+x, Double\-click on black borders -resize window to remove black borders +Resize window to remove black borders .TP .B Ctrl+h, Home, Middle\-click @@ -189,43 +228,43 @@ Click on POWER (turn screen on/off) .TP .B Right\-click (when screen is off) -turn screen on +Turn screen on .TP .B Ctrl+o -turn device screen off (keep mirroring) +Turn device screen off (keep mirroring) .TP .B Ctrl+r -rotate device screen +Rotate device screen .TP .B Ctrl+n -expand notification panel +Expand notification panel .TP .B Ctrl+Shift+n -collapse notification panel +Collapse notification panel .TP .B Ctrl+c -copy device clipboard to computer +Copy device clipboard to computer .TP .B Ctrl+v -paste computer clipboard to device +Paste computer clipboard to device .TP .B Ctrl+Shift+v -copy computer clipboard to device +Copy computer clipboard to device .TP .B Ctrl+i -enable/disable FPS counter (print frames/second in logs) +Enable/disable FPS counter (print frames/second in logs) .TP .B Drag & drop APK file -install APK from computer +Install APK from computer .SH Environment variables diff --git a/app/src/cli.c b/app/src/cli.c index a449f912..69b89899 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1,5 +1,6 @@ #include "cli.h" +#include #include #include #include @@ -10,8 +11,6 @@ #include "util/log.h" #include "util/str_util.h" -#define IPV4_LOCALHOST 0x7F000001 - void scrcpy_print_usage(const char *arg0) { #ifdef __APPLE__ @@ -38,15 +37,31 @@ scrcpy_print_usage(const char *arg0) { " (typically, portrait for a phone, landscape for a tablet).\n" " Any --max-size value is computed on the cropped size.\n" "\n" + " --display id\n" + " Specify the display id to mirror.\n" + "\n" + " The list of possible display ids can be listed by:\n" + " adb shell dumpsys display\n" + " (search \"mDisplayId=\" in the output)\n" + "\n" + " Default is 0.\n" + "\n" " -f, --fullscreen\n" " Start in fullscreen.\n" "\n" " -h, --help\n" " Print this help.\n" "\n" + " --lock-video-orientation value\n" + " Lock video orientation to value.\n" + " Possible values are -1 (unlocked), 0, 1, 2 and 3.\n" + " Natural device orientation is 0, and each increment adds a\n" + " 90 degrees rotation counterclockwise.\n" + " Default is %d%s.\n" + "\n" " --max-fps value\n" - " Limit the frame rate of screen capture (only supported on\n" - " devices with Android >= 10).\n" + " Limit the frame rate of screen capture (officially supported\n" + " since Android 10, but may work on earlier versions).\n" "\n" " -m, --max-size value\n" " Limit both the width and height of the video to value. The\n" @@ -61,9 +76,14 @@ scrcpy_print_usage(const char *arg0) { " Do not display device (only when screen recording is\n" " enabled).\n" "\n" - " -p, --port port\n" - " Set the TCP port the client listens on.\n" - " Default is %d.\n" + " --no-mipmaps\n" + " If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then\n" + " mipmaps are automatically generated to improve downscaling\n" + " quality. This option disables the generation of mipmaps.\n" + "\n" + " -p, --port port[:port]\n" + " Set the TCP port (range) used by the client to listen.\n" + " Default is %d:%d.\n" "\n" " --prefer-text\n" " Inject alpha characters and space as text events instead of\n" @@ -82,20 +102,32 @@ scrcpy_print_usage(const char *arg0) { " The format is determined by the --record-format option if\n" " set, or by the file extension (.mp4 or .mkv).\n" "\n" - " --serve tcp:localhost:1234\n" - " Open a socket to redirect video stream.\n" - " It will Wait for a client to connect before starting the mirroring,\n" - " then it would forward the video stream.\n" - "\n" " --record-format format\n" " Force recording format (either mp4 or mkv).\n" "\n" + " --serve tcp:localhost:1234\n" + " Open a socket to redirect video stream.\n" + " It will wait for a client to connect before starting the mirroring,\n" + " then it would forward the video stream.\n" + "\n" + " --render-driver name\n" + " Request SDL to use the given render driver (this is just a\n" + " hint).\n" + " Supported names are currently \"direct3d\", \"opengl\",\n" + " \"opengles2\", \"opengles\", \"metal\" and \"software\".\n" + " \n" + "\n" " --render-expired-frames\n" " By default, to minimize latency, scrcpy always renders the\n" " last available decoded frame, and drops any previous ones.\n" " This flag forces to render all frames, at a cost of a\n" " possible increased latency.\n" "\n" + " --rotation value\n" + " Set the initial display rotation.\n" + " Possibles values are 0, 1, 2 and 3. Each increment adds a 90\n" + " degrees rotation counterclockwise.\n" + "\n" " -s, --serial serial\n" " The device serial number. Mandatory only if several devices\n" " are connected to adb.\n" @@ -118,11 +150,11 @@ scrcpy_print_usage(const char *arg0) { "\n" " --window-x value\n" " Set the initial window horizontal position.\n" - " Default is -1 (automatic).\n" + " Default is \"auto\".\n" "\n" " --window-y value\n" " Set the initial window vertical position.\n" - " Default is -1 (automatic).\n" + " Default is \"auto\".\n" "\n" " --window-width value\n" " Set the initial window width.\n" @@ -135,73 +167,80 @@ scrcpy_print_usage(const char *arg0) { "Shortcuts:\n" "\n" " " CTRL_OR_CMD "+f\n" - " switch fullscreen mode\n" + " Switch fullscreen mode\n" + "\n" + " " CTRL_OR_CMD "+Left\n" + " Rotate display left\n" + "\n" + " " CTRL_OR_CMD "+Right\n" + " Rotate display right\n" "\n" " " CTRL_OR_CMD "+g\n" - " resize window to 1:1 (pixel-perfect)\n" + " Resize window to 1:1 (pixel-perfect)\n" "\n" " " CTRL_OR_CMD "+x\n" " Double-click on black borders\n" - " resize window to remove black borders\n" + " Resize window to remove black borders\n" "\n" " Ctrl+h\n" " Middle-click\n" - " click on HOME\n" + " Click on HOME\n" "\n" " " CTRL_OR_CMD "+b\n" " " CTRL_OR_CMD "+Backspace\n" " Right-click (when screen is on)\n" - " click on BACK\n" + " Click on BACK\n" "\n" " " CTRL_OR_CMD "+s\n" - " click on APP_SWITCH\n" + " Click on APP_SWITCH\n" "\n" " Ctrl+m\n" - " click on MENU\n" + " Click on MENU\n" "\n" " " CTRL_OR_CMD "+Up\n" - " click on VOLUME_UP\n" + " Click on VOLUME_UP\n" "\n" " " CTRL_OR_CMD "+Down\n" - " click on VOLUME_DOWN\n" + " Click on VOLUME_DOWN\n" "\n" " " CTRL_OR_CMD "+p\n" - " click on POWER (turn screen on/off)\n" + " Click on POWER (turn screen on/off)\n" "\n" " Right-click (when screen is off)\n" - " power on\n" + " Power on\n" "\n" " " CTRL_OR_CMD "+o\n" - " turn device screen off (keep mirroring)\n" + " Turn device screen off (keep mirroring)\n" "\n" " " CTRL_OR_CMD "+r\n" - " rotate device screen\n" + " Rotate device screen\n" "\n" " " CTRL_OR_CMD "+n\n" - " expand notification panel\n" + " Expand notification panel\n" "\n" " " CTRL_OR_CMD "+Shift+n\n" - " collapse notification panel\n" + " Collapse notification panel\n" "\n" " " CTRL_OR_CMD "+c\n" - " copy device clipboard to computer\n" + " Copy device clipboard to computer\n" "\n" " " CTRL_OR_CMD "+v\n" - " paste computer clipboard to device\n" + " Paste computer clipboard to device\n" "\n" " " CTRL_OR_CMD "+Shift+v\n" - " copy computer clipboard to device\n" + " Copy computer clipboard to device\n" "\n" " " CTRL_OR_CMD "+i\n" - " enable/disable FPS counter (print frames/second in logs)\n" + " Enable/disable FPS counter (print frames/second in logs)\n" "\n" " Drag & drop APK file\n" - " install APK from computer\n" + " Install APK from computer\n" "\n", arg0, DEFAULT_BIT_RATE, + DEFAULT_LOCK_VIDEO_ORIENTATION, DEFAULT_LOCK_VIDEO_ORIENTATION >= 0 ? "" : " (unlocked)", DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)", - DEFAULT_LOCAL_PORT); + DEFAULT_LOCAL_PORT_RANGE_FIRST, DEFAULT_LOCAL_PORT_RANGE_LAST); } static bool @@ -229,6 +268,27 @@ parse_integer_arg(const char *s, long *out, bool accept_suffix, long min, return true; } +static size_t +parse_integers_arg(const char *s, size_t max_items, long *out, long min, + long max, const char *name) { + size_t count = parse_integers(s, ':', max_items, out); + if (!count) { + LOGE("Could not parse %s: %s", name, s); + return 0; + } + + for (size_t i = 0; i < count; ++i) { + long value = out[i]; + if (value < min || value > max) { + LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)", + name, value, min, max); + return 0; + } + } + + return count; +} + static bool parse_bit_rate(const char *s, uint32_t *bit_rate) { long value; @@ -268,9 +328,42 @@ parse_max_fps(const char *s, uint16_t *max_fps) { } static bool -parse_window_position(const char *s, int16_t *position) { +parse_lock_video_orientation(const char *s, int8_t *lock_video_orientation) { long value; - bool ok = parse_integer_arg(s, &value, false, -1, 0x7FFF, + bool ok = parse_integer_arg(s, &value, false, -1, 3, + "lock video orientation"); + if (!ok) { + return false; + } + + *lock_video_orientation = (int8_t) value; + return true; +} + +static bool +parse_rotation(const char *s, uint8_t *rotation) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 3, "rotation"); + if (!ok) { + return false; + } + + *rotation = (uint8_t) value; + return true; +} + +static bool +parse_window_position(const char *s, int16_t *position) { + // special value for "auto" + static_assert(WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value"); + + if (!strcmp(s, "auto")) { + *position = WINDOW_POSITION_UNDEFINED; + return true; + } + + long value; + bool ok = parse_integer_arg(s, &value, false, -0x7FFF, 0x7FFF, "window position"); if (!ok) { return false; @@ -294,18 +387,45 @@ parse_window_dimension(const char *s, uint16_t *dimension) { } static bool -parse_port(const char *s, uint16_t *port) { +parse_port_range(const char *s, struct port_range *port_range) { + long values[2]; + size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port"); + if (!count) { + return false; + } + + uint16_t v0 = (uint16_t) values[0]; + if (count == 1) { + port_range->first = v0; + port_range->last = v0; + return true; + } + + assert(count == 2); + uint16_t v1 = (uint16_t) values[1]; + if (v0 < v1) { + port_range->first = v0; + port_range->last = v1; + } else { + port_range->first = v1; + port_range->last = v0; + } + + return true; +} + +static bool +parse_display_id(const char *s, uint16_t *display_id) { long value; - bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "port"); + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "display id"); if (!ok) { return false; } - *port = (uint16_t) value; + *display_id = (uint16_t) value; return true; } - static bool parse_record_format(const char *optarg, enum recorder_format *format) { if (!strcmp(optarg, "mp4")) { @@ -320,154 +440,6 @@ parse_record_format(const char *optarg, enum recorder_format *format) { return false; } -char** -str_split(const char* a_str, const char a_delim) -{ - char** result = 0; - size_t count = 0; - char* tmp = (char*)a_str; - char str[100]; - strncpy(str, a_str, sizeof(str)); - char* last_comma = 0; - char delim[2]; - delim[0] = a_delim; - delim[1] = 0; - - /* Count how many elements will be extracted. */ - while (*tmp) - { - if (a_delim == *tmp) - { - count++; - last_comma = tmp; - } - tmp++; - } - - /* Add space for trailing token. */ - count += last_comma < (str + strlen(str) - 1); - - /* Add space for terminating null string so caller - knows where the list of returned strings ends. */ - count++; - - result = malloc(sizeof(char*) * count); - - if (result) - { - size_t idx = 0; - char* token = strtok(str, delim); - - while (token) - { - assert(idx < count); - *(result + idx++) = strdup(token); - token = strtok(0, delim); - } - assert(idx == count - 1); - *(result + idx) = 0; - } - - return result; -} - -int -validate_ip(char* ip) { //check whether the IP is valid or not - int num, dots = 0; - char* ptr; - if (ip == NULL) - return 0; - ptr = strtok(ip, "."); //cut the string using dor delimiter - if (ptr == NULL) - return 0; - while (ptr) { - long value; - if (!parse_integer(ptr, &value)) //check whether the sub string is holding only number or not - return 0; - num = atoi(ptr); //convert substring to number - if (num >= 0 && num <= 255) { - ptr = strtok(NULL, "."); //cut the next part of the string - if (ptr != NULL) - dots++; //increase the dot count - } - else - return 0; - } - if (dots != 3) //if the number of dots are not 3, return false - return 0; - return 1; -} - -static bool -parse_serve_args(const char *optarg, char **s_protocol, uint32_t *s_ip, uint16_t *s_port) { - bool protocol_valid = false; - bool ip_valid = false; - bool port_valid = false; - - char* protocol = NULL; - char* ip = NULL; - uint32_t ip_value; - char* port = NULL; - char** values; - - values = str_split(optarg, ':'); - - if (values) - { - protocol = *values; - ip = *(values + 1); - port = *(values + 2); - } - - free(values); - - if (!strcmp(protocol, "tcp")) - { - //protocol = "tcp"; - protocol_valid = true; - } else if (!strcmp(protocol, "udp")) - { - //protocol = "udp"; - protocol_valid = true; - } - else { - LOGE("Unexpected protocol: %s (expected tcp or udp)", protocol); - return false; - } - - //Allowing to write localhost or the IP address - if (!strcmp(ip, "localhost")) - { - ip_value = IPV4_LOCALHOST; - ip_valid = true; - } else if (validate_ip(ip)) { - ip_valid = true; - } - else { - LOGE("Unexpected ip address (expected \"localhost\" or 255.255.255.255)"); - return false; - } - - long port_value = 0; - port_valid = parse_integer_arg(port, &port_value, false, 0, 0xFFFF, "port"); - - //Check if everything is valid - if (!protocol_valid || !ip_valid || !port_valid) { - LOGE("Unexpected argument format: %s (expected [tcp/udp]:[ip or \"localhost\"]:[port])", optarg); - return false; - } - - /*LOGI("%s", protocol); - LOGI("%d", ip_value); - LOGI("%ld", port_value);*/ - - *s_protocol = protocol; - *s_ip = (uint32_t)ip_value; - *s_port = (uint16_t)port_value; - - return true; -} - static enum recorder_format guess_record_format(const char *filename) { size_t len = strlen(filename); @@ -484,53 +456,202 @@ guess_record_format(const char *filename) { return 0; } -#define OPT_RENDER_EXPIRED_FRAMES 1000 -#define OPT_WINDOW_TITLE 1001 -#define OPT_PUSH_TARGET 1002 -#define OPT_ALWAYS_ON_TOP 1003 -#define OPT_CROP 1004 -#define OPT_RECORD_FORMAT 1005 -#define OPT_PREFER_TEXT 1006 -#define OPT_WINDOW_X 1007 -#define OPT_WINDOW_Y 1008 -#define OPT_WINDOW_WIDTH 1009 -#define OPT_WINDOW_HEIGHT 1010 -#define OPT_WINDOW_BORDERLESS 1011 -#define OPT_MAX_FPS 1012 -#define OPT_SERVE 1013 +char** +str_split(const char *a_str, const char a_delim) { + char** result = 0; + size_t count = 0; + char* tmp = (char*)a_str; + char str[100]; + strncpy(str, a_str, sizeof(str)); + char* last_comma = 0; + char delim[2]; + delim[0] = a_delim; + delim[1] = 0; + + /* Count how many elements will be extracted. */ + while (*tmp) { + if (a_delim == *tmp) { + count++; + last_comma = tmp; + } + tmp++; + } + + /* Add space for trailing token. */ + count += last_comma < (str + strlen(str) - 1); + + /* Add space for terminating null string so caller + knows where the list of returned strings ends. */ + count++; + + result = malloc(sizeof(char*) * count); + + if (result) { + size_t idx = 0; + char* token = strtok(str, delim); + + while (token) { + assert(idx < count); + *(result + idx++) = strdup(token); + token = strtok(0, delim); + } + + assert(idx == count - 1); + *(result + idx) = 0; + } + + return result; +} + +bool +check_if_ip_valid(char *ip) { + int num, dots = 0; + char* ptr; + + if (ip == NULL) + return 0; + + ptr = strtok(ip, "."); //Cut the string using dot as delimiter + + if (ptr == NULL) + return false; + + while (ptr) { + long value: + if (!parse_integer(ptr, &value)) //Check whether the substring is holding only number or not + return false; + num = atoi(ptr); //Convert substring to number + if (num >= 0 && num <= 255) { + ptr = strtok(NULL, "."); //Cut the next part of the string + if (ptr != NULL) + dots++; //Increase the dot count + } else { + return false; + } + } + + if (dots != 3) + return false; + + return true; +} + +static bool +parse_serve_args(const char *optarg, char **s_protocol, uint32_t *s_ip, uint16_t *s_port) { + bool protocol_valid = false; + bool ip_valid = false; + bool port_valid = false; + + char* protocol = NULL; + char* ip = NULL; + uint32_t ip_value; + char* port = NULL; + + char** values; + values = str_split(optarg, ':'); + + if (values) { + protocol = *values; + ip = *(values + 1); + port = *(values + 2); + } + + free(values); + + //Check if the choosen protocol is allowed + if (!strcmp(protocol, "tcp")) { + protocol_valid = true; + } else { + LOGE("Unexpected protocol: $s (expected tcp)", protocol); + return false; + } + + //Check if the choosen ip is valid + if (!strcmp(ip, "localhost")) { + ip_value = 0x7F000001; + ip_valid = true; + } else if (check_if_ip_valid(ip)) { + ip_valid = true; + } else { + LOGE("Unexpected ip address (expected \"localhost\" or 255.255.255.255 format)"); + return false; + } + + //Check if the choosen port is valid + long port_value = 0; + port_valid = parse_interger_arg(port, &port_value, false, 0, 0xFFFF, "port"); + + //Check if everything is valid + if (!protocol_valid || !ip_valid || !port_valid) { + LOGE("Unexpected argument format: $s (expected [tcp]:[ip or \"localhost\"]:[port])", optarg); + return false; + } + + *s_protocol = protocol; + *s_ip = (uint32_t)ip_value; + *s_port = (uint16_t)port_value; + + return true; +} + +#define OPT_RENDER_EXPIRED_FRAMES 1000 +#define OPT_WINDOW_TITLE 1001 +#define OPT_PUSH_TARGET 1002 +#define OPT_ALWAYS_ON_TOP 1003 +#define OPT_CROP 1004 +#define OPT_RECORD_FORMAT 1005 +#define OPT_PREFER_TEXT 1006 +#define OPT_WINDOW_X 1007 +#define OPT_WINDOW_Y 1008 +#define OPT_WINDOW_WIDTH 1009 +#define OPT_WINDOW_HEIGHT 1010 +#define OPT_WINDOW_BORDERLESS 1011 +#define OPT_MAX_FPS 1012 +#define OPT_LOCK_VIDEO_ORIENTATION 1013 +#define OPT_DISPLAY_ID 1014 +#define OPT_ROTATION 1015 +#define OPT_RENDER_DRIVER 1016 +#define OPT_NO_MIPMAPS 1017 +#define OPT_SERVE 1018 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { static const struct option long_options[] = { - {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, - {"bit-rate", required_argument, NULL, 'b'}, - {"crop", required_argument, NULL, OPT_CROP}, - {"fullscreen", no_argument, NULL, 'f'}, - {"help", no_argument, NULL, 'h'}, - {"max-fps", required_argument, NULL, OPT_MAX_FPS}, - {"max-size", required_argument, NULL, 'm'}, - {"no-control", no_argument, NULL, 'n'}, - {"no-display", no_argument, NULL, 'N'}, - {"port", required_argument, NULL, 'p'}, - {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, - {"record", required_argument, NULL, 'r'}, - {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, - {"render-expired-frames", no_argument, NULL, - OPT_RENDER_EXPIRED_FRAMES}, - {"serial", required_argument, NULL, 's'}, - {"serve", required_argument, NULL, OPT_SERVE}, - {"show-touches", no_argument, NULL, 't'}, - {"turn-screen-off", no_argument, NULL, 'S'}, - {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, - {"version", no_argument, NULL, 'v'}, - {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, - {"window-x", required_argument, NULL, OPT_WINDOW_X}, - {"window-y", required_argument, NULL, OPT_WINDOW_Y}, - {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, - {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, - {"window-borderless", no_argument, NULL, - OPT_WINDOW_BORDERLESS}, - {NULL, 0, NULL, 0 }, + {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, + {"bit-rate", required_argument, NULL, 'b'}, + {"crop", required_argument, NULL, OPT_CROP}, + {"display", required_argument, NULL, OPT_DISPLAY_ID}, + {"fullscreen", no_argument, NULL, 'f'}, + {"help", no_argument, NULL, 'h'}, + {"lock-video-orientation", required_argument, NULL, + OPT_LOCK_VIDEO_ORIENTATION}, + {"max-fps", required_argument, NULL, OPT_MAX_FPS}, + {"max-size", required_argument, NULL, 'm'}, + {"no-control", no_argument, NULL, 'n'}, + {"no-display", no_argument, NULL, 'N'}, + {"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS}, + {"port", required_argument, NULL, 'p'}, + {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, + {"record", required_argument, NULL, 'r'}, + {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, + {"render-driver", required_argument, NULL, OPT_RENDER_DRIVER}, + {"render-expired-frames", no_argument, NULL, + OPT_RENDER_EXPIRED_FRAMES}, + {"rotation", required_argument, NULL, OPT_ROTATION}, + {"serial", required_argument, NULL, 's'}, + {"serve", required_argument, NULL, OPT_SERVE} + {"show-touches", no_argument, NULL, 't'}, + {"turn-screen-off", no_argument, NULL, 'S'}, + {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, + {"version", no_argument, NULL, 'v'}, + {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, + {"window-x", required_argument, NULL, OPT_WINDOW_X}, + {"window-y", required_argument, NULL, OPT_WINDOW_Y}, + {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, + {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, + {"window-borderless", no_argument, NULL, + OPT_WINDOW_BORDERLESS}, + {NULL, 0, NULL, 0 }, }; struct scrcpy_options *opts = &args->opts; @@ -552,6 +673,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_CROP: opts->crop = optarg; break; + case OPT_DISPLAY_ID: + if (!parse_display_id(optarg, &opts->display_id)) { + return false; + } + break; case 'f': opts->fullscreen = true; break; @@ -576,6 +702,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } break; + case OPT_LOCK_VIDEO_ORIENTATION: + if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) { + return false; + } + break; case 'n': opts->control = false; break; @@ -583,7 +714,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { opts->display = false; break; case 'p': - if (!parse_port(optarg, &opts->port)) { + if (!parse_port_range(optarg, &opts->port_range)) { return false; } break; @@ -593,18 +724,16 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case 's': opts->serial = optarg; break; + case 'S': + opts->turn_screen_off = true; + break; case OPT_SERVE: if (!parse_serve_args(optarg, &opts->serve_protocol, &opts->serve_ip, &opts->serve_port)) { return false; - } else { + } + else { opts->serve = true; } - /*LOGI("protocol is %s", opts->serve_protocol); - LOGI("ip value is %d", opts->serve_ip); - LOGI("port is %d", opts->serve_port);*/ - break; - case 'S': - opts->turn_screen_off = true; break; case 't': opts->show_touches = true; @@ -653,6 +782,17 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_PREFER_TEXT: opts->prefer_text = true; break; + case OPT_ROTATION: + if (!parse_rotation(optarg, &opts->rotation)) { + return false; + } + break; + case OPT_RENDER_DRIVER: + opts->render_driver = optarg; + break; + case OPT_NO_MIPMAPS: + opts->mipmaps = false; + break; default: // getopt prints the error message on stderr return false; @@ -664,11 +804,6 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } - if (!opts->display && opts->fullscreen) { - LOGE("-f/--fullscreen-window is incompatible with -N/--no-display"); - return false; - } - int index = optind; if (index < argc) { LOGE("Unexpected additional argument: %s", argv[index]); diff --git a/app/src/command.c b/app/src/command.c index 63afccb4..81047b7a 100644 --- a/app/src/command.c +++ b/app/src/command.c @@ -4,9 +4,6 @@ #include #include #include -#include -#include -#include #include "config.h" #include "common.h" @@ -58,6 +55,32 @@ argv_to_string(const char *const *argv, char *buf, size_t bufsize) { return idx; } +static void +show_adb_installation_msg() { +#ifndef __WINDOWS__ + static const struct { + const char *binary; + const char *command; + } pkg_managers[] = { + {"apt", "apt install adb"}, + {"apt-get", "apt-get install adb"}, + {"brew", "brew cask install android-platform-tools"}, + {"dnf", "dnf install android-tools"}, + {"emerge", "emerge dev-util/android-tools"}, + {"pacman", "pacman -S android-tools"}, + }; + for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) { + if (cmd_search(pkg_managers[i].binary)) { + LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command); + return; + } + } +#endif + + LOGI("You may download and install 'adb' from " + "https://developer.android.com/studio/releases/platform-tools"); +} + static void show_adb_err_msg(enum process_result err, const char *const argv[]) { char buf[512]; @@ -71,6 +94,7 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) { LOGE("Command not found: %s", buf); LOGE("(make 'adb' accessible from your PATH or define its full" "path in the ADB environment variable)"); + show_adb_installation_msg(); break; case PROCESS_SUCCESS: // do nothing @@ -205,14 +229,3 @@ process_check_success(process_t proc, const char *name) { } return true; } - -bool -is_regular_file(const char *path) { - struct stat path_stat; - int r = stat(path, &path_stat); - if (r) { - perror("stat"); - return false; - } - return S_ISREG(path_stat.st_mode); -} diff --git a/app/src/command.h b/app/src/command.h index 9fc81c1c..28f9fbcf 100644 --- a/app/src/command.h +++ b/app/src/command.h @@ -43,6 +43,11 @@ enum process_result { PROCESS_ERROR_MISSING_BINARY, }; +#ifndef __WINDOWS__ +bool +cmd_search(const char *file); +#endif + enum process_result cmd_execute(const char *const argv[], process_t *process); diff --git a/app/src/common.h b/app/src/common.h index e5cbe953..4cbf1d74 100644 --- a/app/src/common.h +++ b/app/src/common.h @@ -27,4 +27,9 @@ struct position { struct point point; }; +struct port_range { + uint16_t first; + uint16_t last; +}; + #endif diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 45113139..252a3425 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -45,8 +45,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buffer_write32be(&buf[6], msg->inject_keycode.metastate); return 10; case CONTROL_MSG_TYPE_INJECT_TEXT: { - size_t len = write_string(msg->inject_text.text, - CONTROL_MSG_TEXT_MAX_LENGTH, &buf[1]); + size_t len = + write_string(msg->inject_text.text, + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]); return 1 + len; } case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 49a159a6..e132fc6b 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -10,7 +10,7 @@ #include "android/keycodes.h" #include "common.h" -#define CONTROL_MSG_TEXT_MAX_LENGTH 300 +#define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 #define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093 #define CONTROL_MSG_SERIALIZED_MAX_SIZE \ (3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) diff --git a/app/src/event_converter.c b/app/src/event_converter.c index 80ead615..1054dcf9 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -94,6 +94,23 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, MAP(SDLK_UP, AKEYCODE_DPAD_UP); } + if (!(mod & (KMOD_NUM | KMOD_SHIFT))) { + // Handle Numpad events when Num Lock is disabled + // If SHIFT is pressed, a text event will be sent instead + switch(from) { + MAP(SDLK_KP_0, AKEYCODE_INSERT); + MAP(SDLK_KP_1, AKEYCODE_MOVE_END); + MAP(SDLK_KP_2, AKEYCODE_DPAD_DOWN); + MAP(SDLK_KP_3, AKEYCODE_PAGE_DOWN); + MAP(SDLK_KP_4, AKEYCODE_DPAD_LEFT); + MAP(SDLK_KP_6, AKEYCODE_DPAD_RIGHT); + MAP(SDLK_KP_7, AKEYCODE_MOVE_HOME); + MAP(SDLK_KP_8, AKEYCODE_DPAD_UP); + MAP(SDLK_KP_9, AKEYCODE_PAGE_UP); + MAP(SDLK_KP_PERIOD, AKEYCODE_FORWARD_DEL); + } + } + if (prefer_text) { // do not forward alpha and space key events return false; diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 58c62d55..b4dd8b9b 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -23,7 +23,7 @@ fps_counter_init(struct fps_counter *counter) { } counter->thread = NULL; - SDL_AtomicSet(&counter->started, 0); + atomic_init(&counter->started, 0); // no need to initialize the other fields, they are unused until started return true; @@ -35,6 +35,16 @@ fps_counter_destroy(struct fps_counter *counter) { SDL_DestroyMutex(counter->mutex); } +static inline bool +is_started(struct fps_counter *counter) { + return atomic_load_explicit(&counter->started, memory_order_acquire); +} + +static inline void +set_started(struct fps_counter *counter, bool started) { + atomic_store_explicit(&counter->started, started, memory_order_release); +} + // must be called with mutex locked static void display_fps(struct fps_counter *counter) { @@ -70,10 +80,10 @@ run_fps_counter(void *data) { mutex_lock(counter->mutex); while (!counter->interrupted) { - while (!counter->interrupted && !SDL_AtomicGet(&counter->started)) { + while (!counter->interrupted && !is_started(counter)) { cond_wait(counter->state_cond, counter->mutex); } - while (!counter->interrupted && SDL_AtomicGet(&counter->started)) { + while (!counter->interrupted && is_started(counter)) { uint32_t now = SDL_GetTicks(); check_interval_expired(counter, now); @@ -96,7 +106,7 @@ fps_counter_start(struct fps_counter *counter) { counter->nr_skipped = 0; mutex_unlock(counter->mutex); - SDL_AtomicSet(&counter->started, 1); + set_started(counter, true); cond_signal(counter->state_cond); // counter->thread is always accessed from the same thread, no need to lock @@ -114,13 +124,13 @@ fps_counter_start(struct fps_counter *counter) { void fps_counter_stop(struct fps_counter *counter) { - SDL_AtomicSet(&counter->started, 0); + set_started(counter, false); cond_signal(counter->state_cond); } bool fps_counter_is_started(struct fps_counter *counter) { - return SDL_AtomicGet(&counter->started); + return is_started(counter); } void @@ -145,7 +155,7 @@ fps_counter_join(struct fps_counter *counter) { void fps_counter_add_rendered_frame(struct fps_counter *counter) { - if (!SDL_AtomicGet(&counter->started)) { + if (!is_started(counter)) { return; } @@ -158,7 +168,7 @@ fps_counter_add_rendered_frame(struct fps_counter *counter) { void fps_counter_add_skipped_frame(struct fps_counter *counter) { - if (!SDL_AtomicGet(&counter->started)) { + if (!is_started(counter)) { return; } diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index 1c56bb01..52157172 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -1,9 +1,9 @@ #ifndef FPSCOUNTER_H #define FPSCOUNTER_H +#include #include #include -#include #include #include @@ -16,7 +16,7 @@ struct fps_counter { // atomic so that we can check without locking the mutex // if the FPS counter is disabled, we don't want to lock unnecessarily - SDL_atomic_t started; + atomic_bool started; // the following fields are protected by the mutex bool interrupted; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 8c4c230a..14ee7e40 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -221,6 +221,18 @@ rotate_device(struct controller *controller) { } } +static void +rotate_client_left(struct screen *screen) { + unsigned new_rotation = (screen->rotation + 1) % 4; + screen_set_rotation(screen, new_rotation); +} + +static void +rotate_client_right(struct screen *screen) { + unsigned new_rotation = (screen->rotation + 3) % 4; + screen_set_rotation(screen, new_rotation); +} + void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { @@ -351,6 +363,16 @@ input_manager_process_key(struct input_manager *im, action_volume_up(controller, action); } return; + case SDLK_LEFT: + if (cmd && !shift && down) { + rotate_client_left(im->screen); + } + return; + case SDLK_RIGHT: + if (cmd && !shift && down) { + rotate_client_right(im->screen); + } + return; case SDLK_c: if (control && cmd && !shift && !repeat && down) { request_device_clipboard(controller); @@ -427,8 +449,8 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.position.screen_size = screen->frame_size; - to->inject_touch_event.position.point.x = from->x; - to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.position.point = + screen_convert_to_frame_coords(screen, from->x, from->y); to->inject_touch_event.pressure = 1.f; to->inject_touch_event.buttons = convert_mouse_buttons(from->state); @@ -463,13 +485,13 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, return false; } - struct size frame_size = screen->frame_size; - to->inject_touch_event.pointer_id = from->fingerId; - to->inject_touch_event.position.screen_size = frame_size; + to->inject_touch_event.position.screen_size = screen->frame_size; // SDL touch event coordinates are normalized in the range [0; 1] - to->inject_touch_event.position.point.x = from->x * frame_size.width; - to->inject_touch_event.position.point.y = from->y * frame_size.height; + float x = from->x * screen->content_size.width; + float y = from->y * screen->content_size.height; + to->inject_touch_event.position.point = + screen_convert_to_frame_coords(screen, x, y); to->inject_touch_event.pressure = from->pressure; to->inject_touch_event.buttons = 0; return true; @@ -489,8 +511,8 @@ input_manager_process_touch(struct input_manager *im, static bool is_outside_device_screen(struct input_manager *im, int x, int y) { - return x < 0 || x >= im->screen->frame_size.width || - y < 0 || y >= im->screen->frame_size.height; + return x < 0 || x >= im->screen->content_size.width || + y < 0 || y >= im->screen->content_size.height; } static bool @@ -504,8 +526,8 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.position.screen_size = screen->frame_size; - to->inject_touch_event.position.point.x = from->x; - to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.position.point = + screen_convert_to_frame_coords(screen, from->x, from->y); to->inject_touch_event.pressure = 1.f; to->inject_touch_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button)); diff --git a/app/src/opengl.c b/app/src/opengl.c new file mode 100644 index 00000000..da05c082 --- /dev/null +++ b/app/src/opengl.c @@ -0,0 +1,56 @@ +#include "opengl.h" + +#include +#include +#include "SDL2/SDL.h" + +void +sc_opengl_init(struct sc_opengl *gl) { + gl->GetString = SDL_GL_GetProcAddress("glGetString"); + assert(gl->GetString); + + gl->TexParameterf = SDL_GL_GetProcAddress("glTexParameterf"); + assert(gl->TexParameterf); + + gl->TexParameteri = SDL_GL_GetProcAddress("glTexParameteri"); + assert(gl->TexParameteri); + + // optional + gl->GenerateMipmap = SDL_GL_GetProcAddress("glGenerateMipmap"); + + const char *version = (const char *) gl->GetString(GL_VERSION); + assert(version); + gl->version = version; + +#define OPENGL_ES_PREFIX "OpenGL ES " + /* starts with "OpenGL ES " */ + gl->is_opengles = !strncmp(gl->version, OPENGL_ES_PREFIX, + sizeof(OPENGL_ES_PREFIX) - 1); + if (gl->is_opengles) { + /* skip the prefix */ + version += sizeof(PREFIX) - 1; + } + + int r = sscanf(version, "%d.%d", &gl->version_major, &gl->version_minor); + if (r != 2) { + // failed to parse the version + gl->version_major = 0; + gl->version_minor = 0; + } +} + +bool +sc_opengl_version_at_least(struct sc_opengl *gl, + int minver_major, int minver_minor, + int minver_es_major, int minver_es_minor) +{ + if (gl->is_opengles) { + return gl->version_major > minver_es_major + || (gl->version_major == minver_es_major + && gl->version_minor >= minver_es_minor); + } + + return gl->version_major > minver_major + || (gl->version_major == minver_major + && gl->version_minor >= minver_minor); +} diff --git a/app/src/opengl.h b/app/src/opengl.h new file mode 100644 index 00000000..f0a89a14 --- /dev/null +++ b/app/src/opengl.h @@ -0,0 +1,36 @@ +#ifndef SC_OPENGL_H +#define SC_OPENGL_H + +#include +#include + +#include "config.h" + +struct sc_opengl { + const char *version; + bool is_opengles; + int version_major; + int version_minor; + + const GLubyte * + (*GetString)(GLenum name); + + void + (*TexParameterf)(GLenum target, GLenum pname, GLfloat param); + + void + (*TexParameteri)(GLenum target, GLenum pname, GLint param); + + void + (*GenerateMipmap)(GLenum target); +}; + +void +sc_opengl_init(struct sc_opengl *gl); + +bool +sc_opengl_version_at_least(struct sc_opengl *gl, + int minver_major, int minver_minor, + int minver_es_major, int minver_es_minor); + +#endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index b7138fa5..54d3133b 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -49,7 +49,7 @@ static struct input_manager input_manager = { // init SDL and set appropriate hints static bool -sdl_init_and_configure(bool display) { +sdl_init_and_configure(bool display, const char *render_driver) { uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS; if (SDL_Init(flags)) { LOGC("Could not initialize SDL: %s", SDL_GetError()); @@ -62,9 +62,13 @@ sdl_init_and_configure(bool display) { return true; } - // Use the best available scale quality - if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) { - LOGW("Could not enable bilinear filtering"); + if (render_driver && !SDL_SetHint(SDL_HINT_RENDER_DRIVER, render_driver)) { + LOGW("Could not set render driver"); + } + + // Linear filtering + if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { + LOGW("Could not enable linear filtering"); } #ifdef SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH @@ -107,9 +111,10 @@ static int event_watcher(void *data, SDL_Event *event) { (void) data; if (event->type == SDL_WINDOWEVENT - && event->window.event == SDL_WINDOWEVENT_RESIZED) { - // called from another thread, not very safe, but it's a workaround! - screen_render(&screen); + && event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + // In practice, it seems to always be called from the same thread in + // that specific case. Anyway, it's just a workaround. + screen_handle_window_event(&screen, &event->window); } return 0; } @@ -282,11 +287,13 @@ scrcpy(const struct scrcpy_options *options) { bool record = !!options->record_filename; struct server_params params = { .crop = options->crop, - .local_port = options->port, + .port_range = options->port_range, .max_size = options->max_size, .bit_rate = options->bit_rate, .max_fps = options->max_fps, + .lock_video_orientation = options->lock_video_orientation, .control = options->control, + .display_id = options->display_id, }; if (!server_start(&server, options->serial, ¶ms)) { return false; @@ -309,9 +316,8 @@ scrcpy(const struct scrcpy_options *options) { bool stream_started = false; bool controller_initialized = false; bool controller_started = false; - //bool serve_initialized = false; - if (!sdl_init_and_configure(options->display)) { + if (!sdl_init_and_configure(options->display, options->render_driver)) { goto end; } @@ -366,11 +372,8 @@ scrcpy(const struct scrcpy_options *options) { recorder_initialized = true; } - av_log_set_callback(av_log_callback); - - struct serve *serv = NULL; - if (options->serve) - { + struct serve* serv = NULL; + if (options->serve) { serve_init(&serve, options->serve_protocol, options->serve_ip, options->serve_port); if (!serve_start(&serve)) { @@ -379,6 +382,8 @@ scrcpy(const struct scrcpy_options *options) { serv = &serve; } + av_log_set_callback(av_log_callback); + stream_init(&stream, server.video_socket, dec, rec, serv); // now we consumed the header values, the socket receives the video stream @@ -408,7 +413,8 @@ scrcpy(const struct scrcpy_options *options) { options->always_on_top, options->window_x, options->window_y, options->window_width, options->window_height, - options->window_borderless)) { + options->window_borderless, + options->rotation, options-> mipmaps)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 6908cdf8..61c372e5 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -5,6 +5,7 @@ #include #include "config.h" +#include "common.h" #include "input_manager.h" #include "recorder.h" @@ -13,16 +14,23 @@ struct scrcpy_options { const char *crop; const char *record_filename; const char *window_title; - const char *push_target; + const char *push_target; + const char *render_driver; + const char *serve_protocol; enum recorder_format record_format; - uint16_t port; + struct port_range port_range; uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; - int16_t window_x; - int16_t window_y; + int8_t lock_video_orientation; + uint8_t rotation; + int16_t window_x; // WINDOW_POSITION_UNDEFINED for "auto" + int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; uint16_t window_height; + uint16_t display_id; + uint32_t serve_ip; + uint16_t serve_port; bool show_touches; bool fullscreen; bool always_on_top; @@ -32,9 +40,7 @@ struct scrcpy_options { bool render_expired_frames; bool prefer_text; bool window_borderless; - char *serve_protocol; - uint32_t serve_ip; - uint16_t serve_port; + bool mipmaps; bool serve; }; @@ -44,15 +50,25 @@ struct scrcpy_options { .record_filename = NULL, \ .window_title = NULL, \ .push_target = NULL, \ + .render_driver = NULL, \ + .serve_protocol = NULL, \ .record_format = RECORDER_FORMAT_AUTO, \ - .port = DEFAULT_LOCAL_PORT, \ + .port_range = { \ + .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ + .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ + }, \ .max_size = DEFAULT_MAX_SIZE, \ .bit_rate = DEFAULT_BIT_RATE, \ .max_fps = 0, \ - .window_x = -1, \ - .window_y = -1, \ + .lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \ + .rotation = 0, \ + .window_x = WINDOW_POSITION_UNDEFINED, \ + .window_y = WINDOW_POSITION_UNDEFINED, \ .window_width = 0, \ .window_height = 0, \ + .display_id = 0, \ + .serve_ip = 0, \ + .serve_port = 0, \ .show_touches = false, \ .fullscreen = false, \ .always_on_top = false, \ @@ -62,9 +78,7 @@ struct scrcpy_options { .render_expired_frames = false, \ .prefer_text = false, \ .window_borderless = false, \ - .serve_protocol = NULL, \ - .serve_ip = 0, \ - .serve_port = 0, \ + .mipmaps = true, \ .serve = false, \ } diff --git a/app/src/screen.c b/app/src/screen.c index beb10754..0af8de83 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -15,6 +15,19 @@ #define DISPLAY_MARGINS 96 +static inline struct size +get_rotated_size(struct size size, int rotation) { + struct size rotated_size; + if (rotation & 1) { + rotated_size.width = size.height; + rotated_size.height = size.width; + } else { + rotated_size.width = size.width; + rotated_size.height = size.height; + } + return rotated_size; +} + // get the window size in a struct size static struct size get_window_size(SDL_Window *window) { @@ -80,8 +93,8 @@ get_preferred_display_bounds(struct size *bounds) { // - it keeps the aspect ratio // - it scales down to make it fit in the display_size static struct size -get_optimal_size(struct size current_size, struct size frame_size) { - if (frame_size.width == 0 || frame_size.height == 0) { +get_optimal_size(struct size current_size, struct size content_size) { + if (content_size.width == 0 || content_size.height == 0) { // avoid division by 0 return current_size; } @@ -100,14 +113,21 @@ get_optimal_size(struct size current_size, struct size frame_size) { h = MIN(current_size.height, display_size.height); } - bool keep_width = frame_size.width * h > frame_size.height * w; + if (h == w * content_size.height / content_size.width + || w == h * content_size.width / content_size.height) { + // The size is already optimal, if we ignore rounding errors due to + // integer window dimensions + return (struct size) {w, h}; + } + + bool keep_width = content_size.width * h > content_size.height * w; if (keep_width) { // remove black borders on top and bottom - h = frame_size.height * w / frame_size.width; + h = content_size.height * w / content_size.width; } else { // remove black borders on left and right (or none at all if it already // fits) - w = frame_size.width * h / frame_size.height; + w = content_size.width * h / content_size.height; } // w and h must fit into 16 bits @@ -117,33 +137,33 @@ get_optimal_size(struct size current_size, struct size frame_size) { // same as get_optimal_size(), but read the current size from the window static inline struct size -get_optimal_window_size(const struct screen *screen, struct size frame_size) { +get_optimal_window_size(const struct screen *screen, struct size content_size) { struct size windowed_size = get_windowed_window_size(screen); - return get_optimal_size(windowed_size, frame_size); + return get_optimal_size(windowed_size, content_size); } // initially, there is no current size, so use the frame size as current size // req_width and req_height, if not 0, are the sizes requested by the user static inline struct size -get_initial_optimal_size(struct size frame_size, uint16_t req_width, +get_initial_optimal_size(struct size content_size, uint16_t req_width, uint16_t req_height) { struct size window_size; if (!req_width && !req_height) { - window_size = get_optimal_size(frame_size, frame_size); + window_size = get_optimal_size(content_size, content_size); } else { if (req_width) { window_size.width = req_width; } else { // compute from the requested height - window_size.width = (uint32_t) req_height * frame_size.width - / frame_size.height; + window_size.width = (uint32_t) req_height * content_size.width + / content_size.height; } if (req_height) { window_size.height = req_height; } else { // compute from the requested width - window_size.height = (uint32_t) req_width * frame_size.height - / frame_size.width; + window_size.height = (uint32_t) req_width * content_size.height + / content_size.width; } } return window_size; @@ -155,21 +175,48 @@ screen_init(struct screen *screen) { } static inline SDL_Texture * -create_texture(SDL_Renderer *renderer, struct size frame_size) { - return SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, - SDL_TEXTUREACCESS_STREAMING, - frame_size.width, frame_size.height); +create_texture(struct screen *screen) { + SDL_Renderer *renderer = screen->renderer; + struct size size = screen->frame_size; + SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, + SDL_TEXTUREACCESS_STREAMING, + size.width, size.height); + if (!texture) { + return NULL; + } + + if (screen->mipmaps) { + struct sc_opengl *gl = &screen->gl; + + SDL_GL_BindTexture(texture, NULL, NULL); + + // Enable trilinear filtering for downscaling + gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, + GL_LINEAR_MIPMAP_LINEAR); + gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -.5f); + + SDL_GL_UnbindTexture(texture); + } + + return texture; } bool screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top, int16_t window_x, int16_t window_y, uint16_t window_width, - uint16_t window_height, bool window_borderless) { + uint16_t window_height, bool window_borderless, + uint8_t rotation, bool mipmaps) { screen->frame_size = frame_size; + screen->rotation = rotation; + if (rotation) { + LOGI("Initial display rotation set to %u", rotation); + } + struct size content_size = get_rotated_size(frame_size, screen->rotation); + screen->content_size = content_size; struct size window_size = - get_initial_optimal_size(frame_size, window_width, window_height); + get_initial_optimal_size(content_size, window_width, window_height); uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; #ifdef HIDPI_SUPPORT window_flags |= SDL_WINDOW_ALLOW_HIGHDPI; @@ -186,8 +233,10 @@ screen_init_rendering(struct screen *screen, const char *window_title, window_flags |= SDL_WINDOW_BORDERLESS; } - int x = window_x != -1 ? window_x : (int) SDL_WINDOWPOS_UNDEFINED; - int y = window_y != -1 ? window_y : (int) SDL_WINDOWPOS_UNDEFINED; + int x = window_x != WINDOW_POSITION_UNDEFINED + ? window_x : (int) SDL_WINDOWPOS_UNDEFINED; + int y = window_y != WINDOW_POSITION_UNDEFINED + ? window_y : (int) SDL_WINDOWPOS_UNDEFINED; screen->window = SDL_CreateWindow(window_title, x, y, window_size.width, window_size.height, window_flags); @@ -204,13 +253,44 @@ screen_init_rendering(struct screen *screen, const char *window_title, return false; } - if (SDL_RenderSetLogicalSize(screen->renderer, frame_size.width, - frame_size.height)) { + SDL_RendererInfo renderer_info; + int r = SDL_GetRendererInfo(screen->renderer, &renderer_info); + const char *renderer_name = r ? NULL : renderer_info.name; + LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)"); + + if (SDL_RenderSetLogicalSize(screen->renderer, content_size.width, + content_size.height)) { LOGE("Could not set renderer logical size: %s", SDL_GetError()); screen_destroy(screen); return false; } + // starts with "opengl" + screen->use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); + if (screen->use_opengl) { + struct sc_opengl *gl = &screen->gl; + sc_opengl_init(gl); + + LOGI("OpenGL version: %s", gl->version); + + if (mipmaps) { + bool supports_mipmaps = + sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */ + 2, 0 /* OpenGL ES 2.0+ */); + if (supports_mipmaps) { + LOGI("Trilinear filtering enabled"); + screen->mipmaps = true; + } else { + LOGW("Trilinear filtering disabled " + "(OpenGL 3.0+ or ES 2.0+ required)"); + } + } else { + LOGI("Trilinear filtering disabled"); + } + } else { + LOGW("Trilinear filtering disabled (not an OpenGL renderer)"); + } + SDL_Surface *icon = read_xpm(icon_xpm); if (icon) { SDL_SetWindowIcon(screen->window, icon); @@ -221,7 +301,7 @@ screen_init_rendering(struct screen *screen, const char *window_title, LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, frame_size.height); - screen->texture = create_texture(screen->renderer, frame_size); + screen->texture = create_texture(screen); if (!screen->texture) { LOGC("Could not create texture: %s", SDL_GetError()); screen_destroy(screen); @@ -251,13 +331,51 @@ screen_destroy(struct screen *screen) { } } +void +screen_set_rotation(struct screen *screen, unsigned rotation) { + assert(rotation < 4); + if (rotation == screen->rotation) { + return; + } + + struct size old_content_size = screen->content_size; + struct size new_content_size = + get_rotated_size(screen->frame_size, rotation); + + if (SDL_RenderSetLogicalSize(screen->renderer, + new_content_size.width, + new_content_size.height)) { + LOGE("Could not set renderer logical size: %s", SDL_GetError()); + return; + } + + struct size windowed_size = get_windowed_window_size(screen); + struct size target_size = { + .width = (uint32_t) windowed_size.width * new_content_size.width + / old_content_size.width, + .height = (uint32_t) windowed_size.height * new_content_size.height + / old_content_size.height, + }; + target_size = get_optimal_size(target_size, new_content_size); + set_window_size(screen, target_size); + + screen->content_size = new_content_size; + screen->rotation = rotation; + LOGI("Display rotation set to %u", rotation); + + screen_render(screen); +} + // recreate the texture and resize the window if the frame size has changed static bool prepare_for_frame(struct screen *screen, struct size new_frame_size) { if (screen->frame_size.width != new_frame_size.width || screen->frame_size.height != new_frame_size.height) { - if (SDL_RenderSetLogicalSize(screen->renderer, new_frame_size.width, - new_frame_size.height)) { + struct size new_content_size = + get_rotated_size(new_frame_size, screen->rotation); + if (SDL_RenderSetLogicalSize(screen->renderer, + new_content_size.width, + new_content_size.height)) { LOGE("Could not set renderer logical size: %s", SDL_GetError()); return false; } @@ -265,21 +383,23 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) { // frame dimension changed, destroy texture SDL_DestroyTexture(screen->texture); + struct size content_size = screen->content_size; struct size windowed_size = get_windowed_window_size(screen); struct size target_size = { - (uint32_t) windowed_size.width * new_frame_size.width - / screen->frame_size.width, - (uint32_t) windowed_size.height * new_frame_size.height - / screen->frame_size.height, + (uint32_t) windowed_size.width * new_content_size.width + / content_size.width, + (uint32_t) windowed_size.height * new_content_size.height + / content_size.height, }; - target_size = get_optimal_size(target_size, new_frame_size); + target_size = get_optimal_size(target_size, new_content_size); set_window_size(screen, target_size); screen->frame_size = new_frame_size; + screen->content_size = new_content_size; LOGI("New texture: %" PRIu16 "x%" PRIu16, screen->frame_size.width, screen->frame_size.height); - screen->texture = create_texture(screen->renderer, new_frame_size); + screen->texture = create_texture(screen); if (!screen->texture) { LOGC("Could not create texture: %s", SDL_GetError()); return false; @@ -296,6 +416,13 @@ update_texture(struct screen *screen, const AVFrame *frame) { frame->data[0], frame->linesize[0], frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]); + + if (screen->mipmaps) { + assert(screen->use_opengl); + SDL_GL_BindTexture(screen->texture, NULL, NULL); + screen->gl.GenerateMipmap(GL_TEXTURE_2D); + SDL_GL_UnbindTexture(screen->texture); + } } bool @@ -317,7 +444,28 @@ screen_update_frame(struct screen *screen, struct video_buffer *vb) { void screen_render(struct screen *screen) { SDL_RenderClear(screen->renderer); - SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL); + if (screen->rotation == 0) { + SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL); + } else { + // rotation in RenderCopyEx() is clockwise, while screen->rotation is + // counterclockwise (to be consistent with --lock-video-orientation) + int cw_rotation = (4 - screen->rotation) % 4; + double angle = 90 * cw_rotation; + + SDL_Rect *dstrect = NULL; + SDL_Rect rect; + if (screen->rotation & 1) { + struct size size = screen->content_size; + rect.x = (size.width - size.height) / 2; + rect.y = (size.height - size.width) / 2; + rect.w = size.height; + rect.h = size.width; + dstrect = ▭ + } + + SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect, + angle, NULL, 0); + } SDL_RenderPresent(screen->renderer); } @@ -348,9 +496,10 @@ screen_resize_to_fit(struct screen *screen) { } struct size optimal_size = - get_optimal_window_size(screen, screen->frame_size); + get_optimal_window_size(screen, screen->content_size); SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); - LOGD("Resized to optimal size"); + LOGD("Resized to optimal size: %ux%u", optimal_size.width, + optimal_size.height); } void @@ -364,9 +513,10 @@ screen_resize_to_pixel_perfect(struct screen *screen) { screen->maximized = false; } - SDL_SetWindowSize(screen->window, screen->frame_size.width, - screen->frame_size.height); - LOGD("Resized to pixel-perfect"); + struct size content_size = screen->content_size; + SDL_SetWindowSize(screen->window, content_size.width, content_size.height); + LOGD("Resized to pixel-perfect: %ux%u", content_size.width, + content_size.height); } void @@ -410,3 +560,33 @@ screen_handle_window_event(struct screen *screen, break; } } + +struct point +screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { + unsigned rotation = screen->rotation; + assert(rotation < 4); + + int32_t w = screen->content_size.width; + int32_t h = screen->content_size.height; + struct point result; + switch (rotation) { + case 0: + result.x = x; + result.y = y; + break; + case 1: + result.x = h - y; + result.y = x; + break; + case 2: + result.x = w - x; + result.y = h - y; + break; + default: + assert(rotation == 3); + result.x = y; + result.y = w - x; + break; + } + return result; +} diff --git a/app/src/screen.h b/app/src/screen.h index 2346ff15..85514279 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -7,6 +7,9 @@ #include "config.h" #include "common.h" +#include "opengl.h" + +#define WINDOW_POSITION_UNDEFINED (-0x8000) struct video_buffer; @@ -14,24 +17,36 @@ struct screen { SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; + bool use_opengl; + struct sc_opengl gl; struct size frame_size; + struct size content_size; // rotated frame_size // The window size the last time it was not maximized or fullscreen. struct size windowed_window_size; // Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be // able to revert the size to its non-maximized value. struct size windowed_window_size_backup; + // client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise) + unsigned rotation; bool has_frame; bool fullscreen; bool maximized; bool no_window; + bool mipmaps; }; #define SCREEN_INITIALIZER { \ .window = NULL, \ .renderer = NULL, \ .texture = NULL, \ + .use_opengl = false, \ + .gl = {0}, \ .frame_size = { \ - .width = 0, \ + .width = 0, \ + .height = 0, \ + }, \ + .content_size = { \ + .width = 0, \ .height = 0, \ }, \ .windowed_window_size = { \ @@ -42,10 +57,12 @@ struct screen { .width = 0, \ .height = 0, \ }, \ + .rotation = 0, \ .has_frame = false, \ .fullscreen = false, \ .maximized = false, \ .no_window = false, \ + .mipmaps = false, \ } // initialize default values @@ -53,11 +70,13 @@ void screen_init(struct screen *screen); // initialize screen, create window, renderer and texture (window is hidden) +// window_x and window_y accept WINDOW_POSITION_UNDEFINED bool screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top, int16_t window_x, int16_t window_y, uint16_t window_width, - uint16_t window_height, bool window_borderless); + uint16_t window_height, bool window_borderless, + uint8_t rotation, bool mipmaps); // show the window void @@ -87,8 +106,17 @@ screen_resize_to_fit(struct screen *screen); void screen_resize_to_pixel_perfect(struct screen *screen); +// set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise) +void +screen_set_rotation(struct screen *screen, unsigned rotation); + // react to window events void screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event); +// convert point from window coordinates to frame coordinates +// x and y are expressed in pixels +struct point +screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y); + #endif diff --git a/app/src/serve.c b/app/src/serve.c index 8d40290e..5de0c40f 100644 --- a/app/src/serve.c +++ b/app/src/serve.c @@ -13,89 +13,47 @@ void serve_init(struct serve* serve, char *protocol, uint32_t ip, uint16_t port) { - serve->protocol = protocol; - serve->ip = ip; - serve->port = port; + serve->protocol = protocol; + serve->ip = ip; + serve->port = port; } -//static int -//run_serve(void *data) { -// struct serve* serve = data; -// -// socket_t Listensocket; -// socket_t ClientSocket; -// -// Listensocket = net_listen(serve->ip, serve->port, 1); -// if (Listensocket == INVALID_SOCKET) { -// LOGI("Listen Error"); -// net_close(Listensocket); -// return 0; -// } -// -// for (;;) { -// ClientSocket = net_accept(Listensocket); -// if (ClientSocket == INVALID_SOCKET) { -// LOGI("Client Error"); -// net_close(Listensocket); -// return 0; -// } -// LOGI("Client found"); -// -// net_close(Listensocket); -// -// serve->socket = ClientSocket; -// -// if (serve->stopped) -// { -// break; -// } -// } -// -// LOGD("Serve thread ended"); -// return 0; -//} - bool serve_start(struct serve* serve) { - LOGD("Starting serve thread"); + LOGD("Starting serve thread"); - socket_t Listensocket; - socket_t ClientSocket; + socket_t Listensocket; + socket_t ClientSocket; - Listensocket = net_listen(serve->ip, serve->port, 1); - if (Listensocket == INVALID_SOCKET) { - LOGI("Listen Error"); - net_close(Listensocket); - return 0; - } + Listensocket = net_listen(serve->ip, serve->port, 1); + if (Listensocket == INVALID_SOCKET) { + LOGI("Listen error"); + net_close(Listensocket); + return 0; + } - ClientSocket = net_accept(Listensocket); - if (ClientSocket == INVALID_SOCKET) { - LOGI("Client Error"); - net_close(Listensocket); - return 0; - } - LOGI("Client found"); + ClientSocket = net_accept(Listensocket); + if (ClientSocket == INVALID_SOCKET) { + LOGI("Client error"); + net_close(Listensocket); + return 0; + } - net_close(Listensocket); + LOGI("Client found"); - serve->socket = ClientSocket; + net_close(Listensocket); - /*serve->thread = SDL_CreateThread(run_serve, "serve", serve); - if (!serve->thread) { - LOGC("Could not start stream thread"); - return false; - }*/ - return true; + serve->socket = ClientSocket; + + return true; } bool serve_push(struct serve* serve, const AVPacket packet) { - if (net_send(serve->socket, packet.data, packet.size) == SOCKET_ERROR) - { - LOGI("Client lost"); - net_close(serve->socket); - return false; - } - return true; -} + if (net_send(serve->socket, packet.data, packet.size) == SOCKET_ERROR) { + LOGI("Client lost"); + net_close(serve->socket); + return false; + } + return true; +} \ No newline at end of file diff --git a/app/src/server.c b/app/src/server.c index ff167aeb..b102f0c2 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -5,12 +5,15 @@ #include #include #include +#include #include +#include #include "config.h" #include "command.h" #include "util/log.h" #include "util/net.h" +#include "util/str_util.h" #define SOCKET_NAME "scrcpy" #define SERVER_FILENAME "scrcpy-server" @@ -18,20 +21,39 @@ #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME #define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" -static const char * +static char * get_server_path(void) { +#ifdef __WINDOWS__ + const wchar_t *server_path_env = _wgetenv(L"SCRCPY_SERVER_PATH"); +#else const char *server_path_env = getenv("SCRCPY_SERVER_PATH"); +#endif if (server_path_env) { - LOGD("Using SCRCPY_SERVER_PATH: %s", server_path_env); // if the envvar is set, use it - return server_path_env; +#ifdef __WINDOWS__ + char *server_path = utf8_from_wide_char(server_path_env); +#else + char *server_path = SDL_strdup(server_path_env); +#endif + if (!server_path) { + LOGE("Could not allocate memory"); + return NULL; + } + LOGD("Using SCRCPY_SERVER_PATH: %s", server_path); + return server_path; } #ifndef PORTABLE LOGD("Using server: " DEFAULT_SERVER_PATH); + char *server_path = SDL_strdup(DEFAULT_SERVER_PATH); + if (!server_path) { + LOGE("Could not allocate memory"); + return NULL; + } // the absolute path is hardcoded - return DEFAULT_SERVER_PATH; + return server_path; #else + // use scrcpy-server in the same directory as the executable char *executable_path = get_executable_path(); if (!executable_path) { @@ -67,12 +89,17 @@ get_server_path(void) { static bool push_server(const char *serial) { - const char *server_path = get_server_path(); + char *server_path = get_server_path(); + if (!server_path) { + return false; + } if (!is_regular_file(server_path)) { LOGE("'%s' does not exist or is not a regular file\n", server_path); + SDL_free(server_path); return false; } process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH); + SDL_free(server_path); return process_check_success(process, "adb push"); } @@ -100,17 +127,6 @@ disable_tunnel_forward(const char *serial, uint16_t local_port) { return process_check_success(process, "adb forward --remove"); } -static bool -enable_tunnel(struct server *server) { - if (enable_tunnel_reverse(server->serial, server->local_port)) { - return true; - } - - LOGW("'adb reverse' failed, fallback to 'adb forward'"); - server->tunnel_forward = true; - return enable_tunnel_forward(server->serial, server->local_port); -} - static bool disable_tunnel(struct server *server) { if (server->tunnel_forward) { @@ -119,21 +135,125 @@ disable_tunnel(struct server *server) { return disable_tunnel_reverse(server->serial); } +static socket_t +listen_on_port(uint16_t port) { +#define IPV4_LOCALHOST 0x7F000001 + return net_listen(IPV4_LOCALHOST, port, 1); +} + +static bool +enable_tunnel_reverse_any_port(struct server *server, + struct port_range port_range) { + uint16_t port = port_range.first; + for (;;) { + if (!enable_tunnel_reverse(server->serial, port)) { + // the command itself failed, it will fail on any port + return false; + } + + // At the application level, the device part is "the server" because it + // serves video stream and control. However, at the network level, the + // client listens and the server connects to the client. That way, the + // client can listen before starting the server app, so there is no + // need to try to connect until the server socket is listening on the + // device. + server->server_socket = listen_on_port(port); + if (server->server_socket != INVALID_SOCKET) { + // success + server->local_port = port; + return true; + } + + // failure, disable tunnel and try another port + if (!disable_tunnel_reverse(server->serial)) { + LOGW("Could not remove reverse tunnel on port %" PRIu16, port); + } + + // check before incrementing to avoid overflow on port 65535 + if (port < port_range.last) { + LOGW("Could not listen on port %" PRIu16", retrying on %" PRIu16, + port, (uint16_t) (port + 1)); + port++; + continue; + } + + if (port_range.first == port_range.last) { + LOGE("Could not listen on port %" PRIu16, port_range.first); + } else { + LOGE("Could not listen on any port in range %" PRIu16 ":%" PRIu16, + port_range.first, port_range.last); + } + return false; + } +} + +static bool +enable_tunnel_forward_any_port(struct server *server, + struct port_range port_range) { + server->tunnel_forward = true; + uint16_t port = port_range.first; + for (;;) { + if (enable_tunnel_forward(server->serial, port)) { + // success + server->local_port = port; + return true; + } + + if (port < port_range.last) { + LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16, + port, port + 1); + port++; + continue; + } + + if (port_range.first == port_range.last) { + LOGE("Could not forward port %" PRIu16, port_range.first); + } else { + LOGE("Could not forward any port in range %" PRIu16 ":%" PRIu16, + port_range.first, port_range.last); + } + return false; + } +} + +static bool +enable_tunnel_any_port(struct server *server, struct port_range port_range) { + if (enable_tunnel_reverse_any_port(server, port_range)) { + return true; + } + + // if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to + // "adb forward", so the app socket is the client + + LOGW("'adb reverse' failed, fallback to 'adb forward'"); + return enable_tunnel_forward_any_port(server, port_range); +} + static process_t execute_server(struct server *server, const struct server_params *params) { char max_size_string[6]; char bit_rate_string[11]; char max_fps_string[6]; + char lock_video_orientation_string[3]; + char display_id_string[6]; sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); sprintf(max_fps_string, "%"PRIu16, params->max_fps); + sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation); + sprintf(display_id_string, "%"PRIu16, params->display_id); const char *const cmd[] = { "shell", "CLASSPATH=" DEVICE_SERVER_PATH, "app_process", #ifdef SERVER_DEBUGGER # define SERVER_DEBUGGER_PORT "5005" +# ifdef SERVER_DEBUGGER_METHOD_NEW + /* Android 9 and above */ + "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,server=y,address=" +# else + /* Android 8 and below */ "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" +# endif SERVER_DEBUGGER_PORT, #endif "/", // unused @@ -142,10 +262,12 @@ execute_server(struct server *server, const struct server_params *params) { max_size_string, bit_rate_string, max_fps_string, + lock_video_orientation_string, server->tunnel_forward ? "true" : "false", params->crop ? params->crop : "-", "true", // always send frame meta (packet boundaries + timestamp) params->control ? "true" : "false", + display_id_string, }; #ifdef SERVER_DEBUGGER LOGI("Server debugger waiting for a client on device port " @@ -161,13 +283,6 @@ execute_server(struct server *server, const struct server_params *params) { return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); } -#define IPV4_LOCALHOST 0x7F000001 - -static socket_t -listen_on_port(uint16_t port) { - return net_listen(IPV4_LOCALHOST, port, 1); -} - static socket_t connect_and_read_byte(uint16_t port) { socket_t socket = net_connect(IPV4_LOCALHOST, port); @@ -203,14 +318,12 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { } static void -close_socket(socket_t *socket) { - assert(*socket != INVALID_SOCKET); - net_shutdown(*socket, SHUT_RDWR); - if (!net_close(*socket)) { +close_socket(socket_t socket) { + assert(socket != INVALID_SOCKET); + net_shutdown(socket, SHUT_RDWR); + if (!net_close(socket)) { LOGW("Could not close socket"); - return; } - *socket = INVALID_SOCKET; } void @@ -218,10 +331,26 @@ server_init(struct server *server) { *server = (struct server) SERVER_INITIALIZER; } +static int +run_wait_server(void *data) { + struct server *server = data; + cmd_simple_wait(server->process, NULL); // ignore exit code + // no need for synchronization, server_socket is initialized before this + // thread was created + if (server->server_socket != INVALID_SOCKET + && !atomic_flag_test_and_set(&server->server_socket_closed)) { + // On Linux, accept() is unblocked by shutdown(), but on Windows, it is + // unblocked by closesocket(). Therefore, call both (close_socket()). + close_socket(server->server_socket); + } + LOGD("Server terminated"); + return 0; +} + bool server_start(struct server *server, const char *serial, const struct server_params *params) { - server->local_port = params->local_port; + server->port_range = params->port_range; if (serial) { server->serial = SDL_strdup(serial); @@ -231,49 +360,50 @@ server_start(struct server *server, const char *serial, } if (!push_server(serial)) { - SDL_free(server->serial); - return false; + goto error1; } - if (!enable_tunnel(server)) { - SDL_free(server->serial); - return false; - } - - // if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to - // "adb forward", so the app socket is the client - if (!server->tunnel_forward) { - // At the application level, the device part is "the server" because it - // serves video stream and control. However, at the network level, the - // client listens and the server connects to the client. That way, the - // client can listen before starting the server app, so there is no - // need to try to connect until the server socket is listening on the - // device. - - server->server_socket = listen_on_port(params->local_port); - if (server->server_socket == INVALID_SOCKET) { - LOGE("Could not listen on port %" PRIu16, params->local_port); - disable_tunnel(server); - SDL_free(server->serial); - return false; - } + if (!enable_tunnel_any_port(server, params->port_range)) { + goto error1; } // server will connect to our server socket server->process = execute_server(server, params); - if (server->process == PROCESS_NONE) { - if (!server->tunnel_forward) { - close_socket(&server->server_socket); - } - disable_tunnel(server); - SDL_free(server->serial); - return false; + goto error2; + } + + // If the server process dies before connecting to the server socket, then + // the client will be stuck forever on accept(). To avoid the problem, we + // must be able to wake up the accept() call when the server dies. To keep + // things simple and multiplatform, just spawn a new thread waiting for the + // server process and calling shutdown()/close() on the server socket if + // necessary to wake up any accept() blocking call. + server->wait_server_thread = + SDL_CreateThread(run_wait_server, "wait-server", server); + if (!server->wait_server_thread) { + cmd_terminate(server->process); + cmd_simple_wait(server->process, NULL); // ignore exit code + goto error2; } server->tunnel_enabled = true; return true; + +error2: + if (!server->tunnel_forward) { + bool was_closed = + atomic_flag_test_and_set(&server->server_socket_closed); + // the thread is not started, the flag could not be already set + assert(!was_closed); + (void) was_closed; + close_socket(server->server_socket); + } + disable_tunnel(server); +error1: + SDL_free(server->serial); + return false; } bool @@ -291,7 +421,11 @@ server_connect_to(struct server *server) { } // we don't need the server socket anymore - close_socket(&server->server_socket); + if (!atomic_flag_test_and_set(&server->server_socket_closed)) { + // close it from here + close_socket(server->server_socket); + // otherwise, it is closed by run_wait_server() + } } else { uint32_t attempts = 100; uint32_t delay = 100; // ms @@ -318,29 +452,27 @@ server_connect_to(struct server *server) { void server_stop(struct server *server) { - if (server->server_socket != INVALID_SOCKET) { - close_socket(&server->server_socket); + if (server->server_socket != INVALID_SOCKET + && !atomic_flag_test_and_set(&server->server_socket_closed)) { + close_socket(server->server_socket); } if (server->video_socket != INVALID_SOCKET) { - close_socket(&server->video_socket); + close_socket(server->video_socket); } if (server->control_socket != INVALID_SOCKET) { - close_socket(&server->control_socket); + close_socket(server->control_socket); } assert(server->process != PROCESS_NONE); - if (!cmd_terminate(server->process)) { - LOGW("Could not terminate server"); - } - - cmd_simple_wait(server->process, NULL); // ignore exit code - LOGD("Server terminated"); + cmd_terminate(server->process); if (server->tunnel_enabled) { // ignore failure disable_tunnel(server); } + + SDL_WaitThread(server->wait_server_thread, NULL); } void diff --git a/app/src/server.h b/app/src/server.h index 0cb1ab3a..a2ecdefc 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -1,42 +1,56 @@ #ifndef SERVER_H #define SERVER_H +#include #include #include +#include #include "config.h" #include "command.h" +#include "common.h" #include "util/net.h" struct server { char *serial; process_t process; + SDL_Thread *wait_server_thread; + atomic_flag server_socket_closed; socket_t server_socket; // only used if !tunnel_forward socket_t video_socket; socket_t control_socket; - uint16_t local_port; + struct port_range port_range; + uint16_t local_port; // selected from port_range bool tunnel_enabled; bool tunnel_forward; // use "adb forward" instead of "adb reverse" }; -#define SERVER_INITIALIZER { \ - .serial = NULL, \ - .process = PROCESS_NONE, \ - .server_socket = INVALID_SOCKET, \ - .video_socket = INVALID_SOCKET, \ +#define SERVER_INITIALIZER { \ + .serial = NULL, \ + .process = PROCESS_NONE, \ + .wait_server_thread = NULL, \ + .server_socket_closed = ATOMIC_FLAG_INIT, \ + .server_socket = INVALID_SOCKET, \ + .video_socket = INVALID_SOCKET, \ .control_socket = INVALID_SOCKET, \ - .local_port = 0, \ - .tunnel_enabled = false, \ - .tunnel_forward = false, \ + .port_range = { \ + .first = 0, \ + .last = 0, \ + }, \ + .local_port = 0, \ + .tunnel_enabled = false, \ + .tunnel_forward = false, \ } struct server_params { const char *crop; - uint16_t local_port; + struct port_range port_range; uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; + int8_t lock_video_orientation; bool control; + uint16_t display_id; }; // init default values diff --git a/app/src/stream.c b/app/src/stream.c index b91fb2fe..a7cee436 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -7,7 +7,6 @@ #include #include #include -#include #include "config.h" #include "compat.h" @@ -15,10 +14,8 @@ #include "events.h" #include "recorder.h" #include "serve.h" -#include "stream.h" #include "util/buffer_util.h" #include "util/log.h" -#include "util/net.h" #define BUFSIZE 0x10000 @@ -26,295 +23,291 @@ #define NO_PTS UINT64_C(-1) static bool -stream_recv_packet(struct stream* stream, AVPacket* packet) { - // The video stream contains raw packets, without time information. When we - // record, we retrieve the timestamps separately, from a "meta" header - // added by the server before each raw packet. - // - // The "meta" header length is 12 bytes: - // [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ... - // <-------------> <-----> <-----------------------------... - // PTS packet raw packet - // size - // - // It is followed by bytes containing the packet/frame. +stream_recv_packet(struct stream *stream, AVPacket *packet) { + // The video stream contains raw packets, without time information. When we + // record, we retrieve the timestamps separately, from a "meta" header + // added by the server before each raw packet. + // + // The "meta" header length is 12 bytes: + // [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ... + // <-------------> <-----> <-----------------------------... + // PTS packet raw packet + // size + // + // It is followed by bytes containing the packet/frame. - uint8_t header[HEADER_SIZE]; - ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE); - if (r < HEADER_SIZE) { - return false; - } + uint8_t header[HEADER_SIZE]; + ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE); + if (r < HEADER_SIZE) { + return false; + } - uint64_t pts = buffer_read64be(header); - uint32_t len = buffer_read32be(&header[8]); - assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0); - assert(len); + uint64_t pts = buffer_read64be(header); + uint32_t len = buffer_read32be(&header[8]); + assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0); + assert(len); - if (av_new_packet(packet, len)) { - LOGE("Could not allocate packet"); - return false; - } + if (av_new_packet(packet, len)) { + LOGE("Could not allocate packet"); + return false; + } - r = net_recv_all(stream->socket, packet->data, len); - if (r < 0 || ((uint32_t)r) < len) { - av_packet_unref(packet); - return false; - } + r = net_recv_all(stream->socket, packet->data, len); + if (r < 0 || ((uint32_t) r) < len) { + av_packet_unref(packet); + return false; + } - packet->pts = pts != NO_PTS ? (int64_t)pts : AV_NOPTS_VALUE; + packet->pts = pts != NO_PTS ? (int64_t) pts : AV_NOPTS_VALUE; - return true; + return true; } static void notify_stopped(void) { - SDL_Event stop_event; - stop_event.type = EVENT_STREAM_STOPPED; - SDL_PushEvent(&stop_event); + SDL_Event stop_event; + stop_event.type = EVENT_STREAM_STOPPED; + SDL_PushEvent(&stop_event); } static bool -process_config_packet(struct stream* stream, AVPacket* packet) { - if (stream->recorder && !recorder_push(stream->recorder, packet)) { - LOGE("Could not send config packet to recorder"); - return false; - } - return true; -} - - - -static bool -process_frame(struct stream* stream, AVPacket* packet) { - if (stream->decoder && !decoder_push(stream->decoder, packet)) { - return false; - } - - if (stream->recorder) { - packet->dts = packet->pts; - - if (!recorder_push(stream->recorder, packet)) { - LOGE("Could not send packet to recorder"); - return false; - } - } - - if (stream->serve && !serve_push(stream->serve, *packet)) { - LOGE("Could not serve packet"); - return false; - } - - return true; +process_config_packet(struct stream *stream, AVPacket *packet) { + if (stream->recorder && !recorder_push(stream->recorder, packet)) { + LOGE("Could not send config packet to recorder"); + return false; + } + return true; } static bool -stream_parse(struct stream* stream, AVPacket* packet) { - uint8_t* in_data = packet->data; - int in_len = packet->size; - uint8_t* out_data = NULL; - int out_len = 0; - int r = av_parser_parse2(stream->parser, stream->codec_ctx, - &out_data, &out_len, in_data, in_len, - AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1); +process_frame(struct stream *stream, AVPacket *packet) { + if (stream->decoder && !decoder_push(stream->decoder, packet)) { + return false; + } - // PARSER_FLAG_COMPLETE_FRAMES is set - assert(r == in_len); - (void)r; - assert(out_len == in_len); + if (stream->recorder) { + packet->dts = packet->pts; - if (stream->parser->key_frame == 1) { - packet->flags |= AV_PKT_FLAG_KEY; - } + if (!recorder_push(stream->recorder, packet)) { + LOGE("Could not send packet to recorder"); + return false; + } + } - bool ok = process_frame(stream, packet); - if (!ok) { - LOGE("Could not process frame"); - return false; - } + if (stream->serve) { + packet->dts = packet->pts; - return true; + if (!serve_push(stream->serve, packet) { + LOGE("Could not serve packet"); + return false; + } + } + + return true; } static bool -stream_push_packet(struct stream* stream, AVPacket* packet) { - bool is_config = packet->pts == AV_NOPTS_VALUE; +stream_parse(struct stream *stream, AVPacket *packet) { + uint8_t *in_data = packet->data; + int in_len = packet->size; + uint8_t *out_data = NULL; + int out_len = 0; + int r = av_parser_parse2(stream->parser, stream->codec_ctx, + &out_data, &out_len, in_data, in_len, + AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1); - // A config packet must not be decoded immetiately (it contains no - // frame); instead, it must be concatenated with the future data packet. - if (stream->has_pending || is_config) { - size_t offset; - if (stream->has_pending) { - offset = stream->pending.size; - if (av_grow_packet(&stream->pending, packet->size)) { - LOGE("Could not grow packet"); - return false; - } - } - else { - offset = 0; - if (av_new_packet(&stream->pending, packet->size)) { - LOGE("Could not create packet"); - return false; - } - stream->has_pending = true; - } + // PARSER_FLAG_COMPLETE_FRAMES is set + assert(r == in_len); + (void) r; + assert(out_len == in_len); - memcpy(stream->pending.data + offset, packet->data, packet->size); + if (stream->parser->key_frame == 1) { + packet->flags |= AV_PKT_FLAG_KEY; + } - if (!is_config) { - // prepare the concat packet to send to the decoder - stream->pending.pts = packet->pts; - stream->pending.dts = packet->dts; - stream->pending.flags = packet->flags; - packet = &stream->pending; - } - } + bool ok = process_frame(stream, packet); + if (!ok) { + LOGE("Could not process frame"); + return false; + } - if (is_config) { - // config packet - bool ok = process_config_packet(stream, packet); - if (!ok) { - return false; - } - } - else { - // data packet - bool ok = stream_parse(stream, packet); + return true; +} - if (stream->has_pending) { - // the pending packet must be discarded (consumed or error) - stream->has_pending = false; - av_packet_unref(&stream->pending); - } +static bool +stream_push_packet(struct stream *stream, AVPacket *packet) { + bool is_config = packet->pts == AV_NOPTS_VALUE; - if (!ok) { - return false; - } - } - return true; + // A config packet must not be decoded immetiately (it contains no + // frame); instead, it must be concatenated with the future data packet. + if (stream->has_pending || is_config) { + size_t offset; + if (stream->has_pending) { + offset = stream->pending.size; + if (av_grow_packet(&stream->pending, packet->size)) { + LOGE("Could not grow packet"); + return false; + } + } else { + offset = 0; + if (av_new_packet(&stream->pending, packet->size)) { + LOGE("Could not create packet"); + return false; + } + stream->has_pending = true; + } + + memcpy(stream->pending.data + offset, packet->data, packet->size); + + if (!is_config) { + // prepare the concat packet to send to the decoder + stream->pending.pts = packet->pts; + stream->pending.dts = packet->dts; + stream->pending.flags = packet->flags; + packet = &stream->pending; + } + } + + if (is_config) { + // config packet + bool ok = process_config_packet(stream, packet); + if (!ok) { + return false; + } + } else { + // data packet + bool ok = stream_parse(stream, packet); + + if (stream->has_pending) { + // the pending packet must be discarded (consumed or error) + stream->has_pending = false; + av_packet_unref(&stream->pending); + } + + if (!ok) { + return false; + } + } + return true; } static int -run_stream(void* data) { - struct stream* stream = data; +run_stream(void *data) { + struct stream *stream = data; - AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_H264); - if (!codec) { - LOGE("H.264 decoder not found"); - goto end; - } + AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); + if (!codec) { + LOGE("H.264 decoder not found"); + goto end; + } - stream->codec_ctx = avcodec_alloc_context3(codec); - if (!stream->codec_ctx) { - LOGC("Could not allocate codec context"); - goto end; - } + stream->codec_ctx = avcodec_alloc_context3(codec); + if (!stream->codec_ctx) { + LOGC("Could not allocate codec context"); + goto end; + } - if (stream->decoder && !decoder_open(stream->decoder, codec)) { - LOGE("Could not open decoder"); - goto finally_free_codec_ctx; - } + if (stream->decoder && !decoder_open(stream->decoder, codec)) { + LOGE("Could not open decoder"); + goto finally_free_codec_ctx; + } - if (stream->recorder) { - if (!recorder_open(stream->recorder, codec)) { - LOGE("Could not open recorder"); - goto finally_close_decoder; - } + if (stream->recorder) { + if (!recorder_open(stream->recorder, codec)) { + LOGE("Could not open recorder"); + goto finally_close_decoder; + } - if (!recorder_start(stream->recorder)) { - LOGE("Could not start recorder"); - goto finally_close_recorder; - } - } + if (!recorder_start(stream->recorder)) { + LOGE("Could not start recorder"); + goto finally_close_recorder; + } + } - stream->parser = av_parser_init(AV_CODEC_ID_H264); - if (!stream->parser) { - LOGE("Could not initialize parser"); - goto finally_stop_and_join_recorder; - } + stream->parser = av_parser_init(AV_CODEC_ID_H264); + if (!stream->parser) { + LOGE("Could not initialize parser"); + goto finally_stop_and_join_recorder; + } - // We must only pass complete frames to av_parser_parse2()! - // It's more complicated, but this allows to reduce the latency by 1 frame! - stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES; + // We must only pass complete frames to av_parser_parse2()! + // It's more complicated, but this allows to reduce the latency by 1 frame! + stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES; - for (;;) { - AVPacket packet; - bool ok = stream_recv_packet(stream, &packet); + for (;;) { + AVPacket packet; + bool ok = stream_recv_packet(stream, &packet); + if (!ok) { + // end of stream + break; + } - if (!ok) { - // end of stream - break; - } + ok = stream_push_packet(stream, &packet); + av_packet_unref(&packet); + if (!ok) { + // cannot process packet (error already logged) + break; + } + } - ok = stream_push_packet(stream, &packet); + LOGD("End of frames"); - av_packet_unref(&packet); - - if (!ok) { - // cannot process packet (error already logged) - break; - } - } + if (stream->has_pending) { + av_packet_unref(&stream->pending); + } - LOGD("End of frames"); - - if (stream->has_pending) { - av_packet_unref(&stream->pending); - } - - av_parser_close(stream->parser); + av_parser_close(stream->parser); finally_stop_and_join_recorder: - if (stream->recorder) { - recorder_stop(stream->recorder); - LOGI("Finishing recording..."); - recorder_join(stream->recorder); - } + if (stream->recorder) { + recorder_stop(stream->recorder); + LOGI("Finishing recording..."); + recorder_join(stream->recorder); + } finally_close_recorder: - if (stream->recorder) { - recorder_close(stream->recorder); - } + if (stream->recorder) { + recorder_close(stream->recorder); + } finally_close_decoder: - if (stream->decoder) { - decoder_close(stream->decoder); - } + if (stream->decoder) { + decoder_close(stream->decoder); + } finally_free_codec_ctx: - avcodec_free_context(&stream->codec_ctx); + avcodec_free_context(&stream->codec_ctx); end: - notify_stopped(); - return 0; + notify_stopped(); + return 0; } void -stream_init(struct stream* stream, socket_t socket, - struct decoder* decoder, struct recorder* recorder, struct serve* serve) { - stream->socket = socket; - stream->decoder = decoder; - stream->recorder = recorder; - stream->serve = serve; - stream->has_pending = false; +stream_init(struct stream *stream, socket_t socket, + struct decoder *decoder, struct recorder *recorder, struct serve *serve) { + stream->socket = socket; + stream->decoder = decoder, + stream->recorder = recorder; + stream->serve = serve; + stream->has_pending = false; } bool -stream_start(struct stream* stream) { - LOGD("Starting stream thread"); +stream_start(struct stream *stream) { + LOGD("Starting stream thread"); - stream->thread = SDL_CreateThread(run_stream, "stream", stream); - if (!stream->thread) { - LOGC("Could not start stream thread"); - return false; - } - return true; + stream->thread = SDL_CreateThread(run_stream, "stream", stream); + if (!stream->thread) { + LOGC("Could not start stream thread"); + return false; + } + return true; } void -stream_stop(struct stream* stream) { - if (stream->decoder) { - decoder_interrupt(stream->decoder); - } +stream_stop(struct stream *stream) { + if (stream->decoder) { + decoder_interrupt(stream->decoder); + } } void -stream_join(struct stream* stream) { - SDL_WaitThread(stream->thread, NULL); +stream_join(struct stream *stream) { + SDL_WaitThread(stream->thread, NULL); } - diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index fbcf2355..64a54e71 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -14,12 +14,51 @@ #include #include #include +#include +#include #include +#include #include #include #include "util/log.h" +bool +cmd_search(const char *file) { + char *path = getenv("PATH"); + if (!path) + return false; + path = strdup(path); + if (!path) + return false; + + bool ret = false; + size_t file_len = strlen(file); + char *saveptr; + for (char *dir = strtok_r(path, ":", &saveptr); dir; + dir = strtok_r(NULL, ":", &saveptr)) { + size_t dir_len = strlen(dir); + char *fullpath = malloc(dir_len + file_len + 2); + if (!fullpath) + continue; + memcpy(fullpath, dir, dir_len); + fullpath[dir_len] = '/'; + memcpy(fullpath + dir_len + 1, file, file_len + 1); + + struct stat sb; + bool fullpath_executable = stat(fullpath, &sb) == 0 && + sb.st_mode & S_IXUSR; + free(fullpath); + if (fullpath_executable) { + ret = true; + break; + } + } + + free(path); + return ret; +} + enum process_result cmd_execute(const char *const argv[], pid_t *pid) { int fd[2]; @@ -127,3 +166,14 @@ get_executable_path(void) { return NULL; #endif } + +bool +is_regular_file(const char *path) { + struct stat path_stat; + + if (stat(path, &path_stat)) { + perror("stat"); + return false; + } + return S_ISREG(path_stat.st_mode); +} diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c index 55edaf8f..105234b0 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/command.c @@ -1,5 +1,7 @@ #include "command.h" +#include + #include "config.h" #include "util/log.h" #include "util/str_util.h" @@ -90,3 +92,22 @@ get_executable_path(void) { buf[len] = '\0'; return utf8_from_wide_char(buf); } + +bool +is_regular_file(const char *path) { + wchar_t *wide_path = utf8_to_wide_char(path); + if (!wide_path) { + LOGC("Could not allocate wide char string"); + return false; + } + + struct _stat path_stat; + int r = _wstat(wide_path, &path_stat); + SDL_free(wide_path); + + if (r) { + perror("stat"); + return false; + } + return S_ISREG(path_stat.st_mode); +} diff --git a/app/src/util/net.c b/app/src/util/net.c index bf4389dd..efce6fa9 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -1,6 +1,7 @@ #include "net.h" #include +#include #include "config.h" #include "log.h" @@ -115,3 +116,32 @@ bool net_shutdown(socket_t socket, int how) { return !shutdown(socket, how); } + +bool +net_init(void) { +#ifdef __WINDOWS__ + WSADATA wsa; + int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; + if (res < 0) { + LOGC("WSAStartup failed with error %d", res); + return false; + } +#endif + return true; +} + +void +net_cleanup(void) { +#ifdef __WINDOWS__ + WSACleanup(); +#endif +} + +bool +net_close(socket_t socket) { +#ifdef __WINDOWS__ + return !closesocket(socket); +#else + return !close(socket); +#endif +} diff --git a/app/src/util/str_util.c b/app/src/util/str_util.c index 4d175407..ce0498a5 100644 --- a/app/src/util/str_util.c +++ b/app/src/util/str_util.c @@ -81,6 +81,35 @@ parse_integer(const char *s, long *out) { return true; } +size_t +parse_integers(const char *s, const char sep, size_t max_items, long *out) { + size_t count = 0; + char *endptr; + do { + errno = 0; + long value = strtol(s, &endptr, 0); + if (errno == ERANGE) { + return 0; + } + + if (endptr == s || (*endptr != sep && *endptr != '\0')) { + return 0; + } + + out[count++] = value; + if (*endptr == sep) { + if (count >= max_items) { + // max items already reached, could not accept a new item + return 0; + } + // parse the next token during the next iteration + s = endptr + 1; + } + } while (*endptr != '\0'); + + return count; +} + bool parse_integer_with_suffix(const char *s, long *out) { char *endptr; diff --git a/app/src/util/str_util.h b/app/src/util/str_util.h index 8d9b990c..c7f26cdb 100644 --- a/app/src/util/str_util.h +++ b/app/src/util/str_util.h @@ -31,6 +31,11 @@ strquote(const char *src); bool parse_integer(const char *s, long *out); +// parse s as integers separated by sep (for example '1234:2000') +// returns the number of integers on success, 0 on failure +size_t +parse_integers(const char *s, const char sep, size_t max_items, long *out); + // parse s as an integer into value // like parse_integer(), but accept 'k'/'K' (x1000) and 'm'/'M' (x1000000) as // suffix diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 539c3c94..c5d95633 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -48,9 +48,10 @@ static void test_options(void) { "--fullscreen", "--max-fps", "30", "--max-size", "1024", + "--lock-video-orientation", "2", // "--no-control" is not compatible with "--turn-screen-off" // "--no-display" is not compatible with "--fulscreen" - "--port", "1234", + "--port", "1234:1236", "--push-target", "/sdcard/Movies", "--record", "file", "--record-format", "mkv", @@ -78,7 +79,9 @@ static void test_options(void) { assert(opts->fullscreen); assert(opts->max_fps == 30); assert(opts->max_size == 1024); - assert(opts->port == 1234); + assert(opts->lock_video_orientation == 2); + assert(opts->port_range.first == 1234); + assert(opts->port_range.last == 1236); assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->record_filename, "file")); assert(opts->record_format == RECORDER_FORMAT_MKV); diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index d6f556f3..4dc79018 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -49,20 +49,20 @@ static void test_serialize_inject_text(void) { static void test_serialize_inject_text_long(void) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; - char text[CONTROL_MSG_TEXT_MAX_LENGTH + 1]; + char text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH + 1]; memset(text, 'a', sizeof(text)); - text[CONTROL_MSG_TEXT_MAX_LENGTH] = '\0'; + text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; msg.inject_text.text = text; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 3 + CONTROL_MSG_TEXT_MAX_LENGTH); + assert(size == 3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); - unsigned char expected[3 + CONTROL_MSG_TEXT_MAX_LENGTH]; + unsigned char expected[3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT; expected[1] = 0x01; expected[2] = 0x2c; // text length (16 bits) - memset(&expected[3], 'a', CONTROL_MSG_TEXT_MAX_LENGTH); + memset(&expected[3], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); assert(!memcmp(buf, expected, sizeof(expected))); } diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index 200e0f63..a88bca0e 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -187,6 +187,55 @@ static void test_parse_integer(void) { assert(!ok); // out-of-range } +static void test_parse_integers(void) { + long values[5]; + + size_t count = parse_integers("1234", ':', 5, values); + assert(count == 1); + assert(values[0] == 1234); + + count = parse_integers("1234:5678", ':', 5, values); + assert(count == 2); + assert(values[0] == 1234); + assert(values[1] == 5678); + + count = parse_integers("1234:5678", ':', 2, values); + assert(count == 2); + assert(values[0] == 1234); + assert(values[1] == 5678); + + count = parse_integers("1234:-5678", ':', 2, values); + assert(count == 2); + assert(values[0] == 1234); + assert(values[1] == -5678); + + count = parse_integers("1:2:3:4:5", ':', 5, values); + assert(count == 5); + assert(values[0] == 1); + assert(values[1] == 2); + assert(values[2] == 3); + assert(values[3] == 4); + assert(values[4] == 5); + + count = parse_integers("1234:5678", ':', 1, values); + assert(count == 0); // max_items == 1 + + count = parse_integers("1:2:3:4:5", ':', 3, values); + assert(count == 0); // max_items == 3 + + count = parse_integers(":1234", ':', 5, values); + assert(count == 0); // invalid + + count = parse_integers("1234:", ':', 5, values); + assert(count == 0); // invalid + + count = parse_integers("1234:", ':', 1, values); + assert(count == 0); // invalid, even when max_items == 1 + + count = parse_integers("1234::5678", ':', 5, values); + assert(count == 0); // invalid +} + static void test_parse_integer_with_suffix(void) { long value; bool ok = parse_integer_with_suffix("1234", &value); @@ -249,6 +298,7 @@ int main(void) { test_strquote(); test_utf8_truncate(); test_parse_integer(); + test_parse_integers(); test_parse_integer_with_suffix(); return 0; } diff --git a/build.gradle b/build.gradle index b6ec625d..94862d2e 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' + classpath 'com.android.tools.build:gradle:3.6.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 798814d9..812d060b 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -129,11 +129,6 @@ page at http://checkstyle.sourceforge.net/config.html --> - - - - - diff --git a/cross_win32.txt b/cross_win32.txt index d13af0e2..7b420690 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -15,6 +15,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win32-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win32-dev' -prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32' +prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win32-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win32-dev' +prebuilt_sdl2 = 'SDL2-2.0.12/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index 09f387e1..cb9e0954 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -15,6 +15,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win64-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win64-dev' -prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32' +prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win64-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win64-dev' +prebuilt_sdl2 = 'SDL2-2.0.12/x86_64-w64-mingw32' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf0..490fda85 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 430dfabc..a4b44297 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 8e25e6c1..2fe81a7d 100755 --- a/gradlew +++ b/gradlew @@ -125,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -154,19 +154,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -175,14 +175,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 24467a14..9109989e 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" diff --git a/meson.build b/meson.build index 412c9c51..49093453 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('scrcpy', 'c', - version: '1.12.1', - meson_version: '>= 0.37', + version: '1.13', + meson_version: '>= 0.48', default_options: [ 'c_std=c11', 'warning_level=2', diff --git a/meson_options.txt b/meson_options.txt index 4cf4a8bf..c213e7dd 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -6,3 +6,4 @@ option('prebuilt_server', type: 'string', description: 'Path of the prebuilt ser option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') +option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")') diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 892af6c7..088cee92 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -10,29 +10,29 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb prepare-ffmpeg-shared-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.1-win32-shared.zip \ - 9208255f409410d95147151d7e829b5699bf8d91bfe1e81c3f470f47c2fa66d2 \ - ffmpeg-4.2.1-win32-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.2-win32-shared.zip \ + ab5d603aaa54de360db2c2ffe378c82376b9343ea1175421dd644639aa07ee31 \ + ffmpeg-4.2.2-win32-shared prepare-ffmpeg-dev-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.1-win32-dev.zip \ - c3469e6c5f031cbcc8cba88dee92d6548c5c6b6ff14f4097f18f72a92d0d70c4 \ - ffmpeg-4.2.1-win32-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.2-win32-dev.zip \ + 8d224be567a2950cad4be86f4aabdd045bfa52ad758e87c72cedd278613bc6c8 \ + ffmpeg-4.2.2-win32-dev prepare-ffmpeg-shared-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.1-win64-shared.zip \ - 55063d3cf750a75485c7bf196031773d81a1b25d0980c7db48ecfc7701a42331 \ - ffmpeg-4.2.1-win64-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.2-win64-shared.zip \ + 5aedf268952b7d9f6541dbfcb47cd86a7e7881a3b7ba482fd3bc4ca33bda7bf5 \ + ffmpeg-4.2.2-win64-shared prepare-ffmpeg-dev-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.1-win64-dev.zip \ - 5af393be5f25c0a71aa29efce768e477c35347f7f8e0d9696767d5b9d405b74e \ - ffmpeg-4.2.1-win64-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.2-win64-dev.zip \ + f4885f859c5b0d6663c2a0a4c1cf035b1c60b146402790b796bd3ad84f4f3ca2 \ + ffmpeg-4.2.2-win64-dev prepare-sdl2: - @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \ - a90a7cddaec4996f4d7be6d80c57ec69b062e132bffc513965f99217f603274a \ - SDL2-2.0.10 + @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \ + e614a60f797e35ef9f3f96aef3dc6a1d786de3cc7ca6216f97e435c0b6aafc46 \ + SDL2-2.0.12 prepare-adb: @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \ diff --git a/server/build.gradle b/server/build.gradle index 539a97b8..3fa16519 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 14 - versionName "1.12.1" + versionCode 15 + versionName "1.13" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index c117d89c..06fc0d75 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=1.12.1 +SCRCPY_VERSION_NAME=1.13 PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} diff --git a/server/meson.build b/server/meson.build index 4ba481d5..984daf3b 100644 --- a/server/meson.build +++ b/server/meson.build @@ -3,7 +3,9 @@ prebuilt_server = get_option('prebuilt_server') if prebuilt_server == '' custom_target('scrcpy-server', - build_always: true, # gradle is responsible for tracking source changes + # gradle is responsible for tracking source changes + build_by_default: true, + build_always_stale: true, output: 'scrcpy-server', command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], console: true, diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 726b5659..1c081058 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -8,14 +8,13 @@ import java.nio.charset.StandardCharsets; public class ControlMessageReader { - private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; - private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17; - private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 21; - private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; - private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; + static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; + static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; + static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; + static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; - public static final int TEXT_MAX_LENGTH = 300; public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; + public static final int INJECT_TEXT_MAX_LENGTH = 300; private static final int RAW_BUFFER_SIZE = 1024; private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; @@ -122,7 +121,6 @@ public class ControlMessageReader { return ControlMessage.createInjectText(text); } - @SuppressWarnings("checkstyle:MagicNumber") private ControlMessage parseInjectTouchEvent() { if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { return null; @@ -172,12 +170,10 @@ public class ControlMessageReader { return new Position(x, y, screenWidth, screenHeight); } - @SuppressWarnings("checkstyle:MagicNumber") private static int toUnsigned(short value) { return value & 0xffff; } - @SuppressWarnings("checkstyle:MagicNumber") private static int toUnsigned(byte value) { return value & 0xff; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index dc0fa67b..32a18e16 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -47,7 +47,6 @@ public class Controller { } } - @SuppressWarnings("checkstyle:MagicNumber") public void control() throws IOException { // on start, power on the device if (!device.isScreenOn()) { @@ -76,19 +75,29 @@ public class Controller { ControlMessage msg = connection.receiveControlMessage(); switch (msg.getType()) { case ControlMessage.TYPE_INJECT_KEYCODE: - injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); + if (device.supportsInputEvents()) { + injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); + } break; case ControlMessage.TYPE_INJECT_TEXT: - injectText(msg.getText()); + if (device.supportsInputEvents()) { + injectText(msg.getText()); + } break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); + if (device.supportsInputEvents()) { + injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); + } break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: - injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + if (device.supportsInputEvents()) { + injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + } break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: - pressBackOrTurnScreenOn(); + if (device.supportsInputEvents()) { + pressBackOrTurnScreenOn(); + } break; case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: device.expandNotificationPanel(); @@ -104,7 +113,9 @@ public class Controller { device.setClipboardText(msg.getText()); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: - device.setScreenPowerMode(msg.getAction()); + if (device.supportsInputEvents()) { + device.setScreenPowerMode(msg.getAction()); + } break; case ControlMessage.TYPE_ROTATE_DEVICE: device.rotateDevice(); diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index a725d83d..0ec43040 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -84,7 +84,6 @@ public final class DesktopConnection implements Closeable { controlSocket.close(); } - @SuppressWarnings("checkstyle:MagicNumber") private void send(String deviceName, int width, int height) throws IOException { byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4]; diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 9448098a..0c43dd34 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.WindowManager; @@ -25,13 +26,36 @@ public final class Device { private ScreenInfo screenInfo; private RotationListener rotationListener; + /** + * Logical display identifier + */ + private final int displayId; + + /** + * The surface flinger layer stack associated with this logical display + */ + private final int layerStack; + + private final boolean supportsInputEvents; + public Device(Options options) { - screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize()); + displayId = options.getDisplayId(); + DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + int[] displayIds = serviceManager.getDisplayManager().getDisplayIds(); + throw new InvalidDisplayIdException(displayId, displayIds); + } + + int displayInfoFlags = displayInfo.getFlags(); + + screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); + layerStack = displayInfo.getLayerStack(); + registerRotationWatcher(new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) throws RemoteException { synchronized (Device.this) { - screenInfo = screenInfo.withRotation(rotation); + screenInfo = screenInfo.withDeviceRotation(rotation); // notify if (rotationListener != null) { @@ -39,89 +63,69 @@ public final class Device { } } } - }); + }, displayId); + + if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { + Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); + } + + // main display or any display on Android >= Q + supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + if (!supportsInputEvents) { + Ln.w("Input events are not supported for secondary displays before Android 10"); + } } public synchronized ScreenInfo getScreenInfo() { return screenInfo; } - private ScreenInfo computeScreenInfo(Rect crop, int maxSize) { - DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); - boolean rotated = (displayInfo.getRotation() & 1) != 0; - Size deviceSize = displayInfo.getSize(); - Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); - if (crop != null) { - if (rotated) { - // the crop (provided by the user) is expressed in the natural orientation - crop = flipRect(crop); - } - if (!contentRect.intersect(crop)) { - // intersect() changes contentRect so that it is intersected with crop - Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); - contentRect = new Rect(); // empty - } - } - - Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); - return new ScreenInfo(contentRect, videoSize, rotated); - } - - private static String formatCrop(Rect rect) { - return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; - } - - @SuppressWarnings("checkstyle:MagicNumber") - private static Size computeVideoSize(int w, int h, int maxSize) { - // Compute the video size and the padding of the content inside this video. - // Principle: - // - scale down the great side of the screen to maxSize (if necessary); - // - scale down the other side so that the aspect ratio is preserved; - // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) - w &= ~7; // in case it's not a multiple of 8 - h &= ~7; - if (maxSize > 0) { - if (BuildConfig.DEBUG && maxSize % 8 != 0) { - throw new AssertionError("Max size must be a multiple of 8"); - } - boolean portrait = h > w; - int major = portrait ? h : w; - int minor = portrait ? w : h; - if (major > maxSize) { - int minorExact = minor * maxSize / major; - // +4 to round the value to the nearest multiple of 8 - minor = (minorExact + 4) & ~7; - major = maxSize; - } - w = portrait ? minor : major; - h = portrait ? major : minor; - } - return new Size(w, h); + public int getLayerStack() { + return layerStack; } public Point getPhysicalPoint(Position position) { // it hides the field on purpose, to read it with a lock @SuppressWarnings("checkstyle:HiddenField") ScreenInfo screenInfo = getScreenInfo(); // read with synchronization - Size videoSize = screenInfo.getVideoSize(); - Size clientVideoSize = position.getScreenSize(); - if (!videoSize.equals(clientVideoSize)) { + + // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation + Size unlockedVideoSize = screenInfo.getUnlockedVideoSize(); + + int reverseVideoRotation = screenInfo.getReverseVideoRotation(); + // reverse the video rotation to apply the events + Position devicePosition = position.rotate(reverseVideoRotation); + + Size clientVideoSize = devicePosition.getScreenSize(); + if (!unlockedVideoSize.equals(clientVideoSize)) { // The client sends a click relative to a video with wrong dimensions, // the device may have been rotated since the event was generated, so ignore the event return null; } Rect contentRect = screenInfo.getContentRect(); - Point point = position.getPoint(); - int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); - int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); - return new Point(scaledX, scaledY); + Point point = devicePosition.getPoint(); + int convertedX = contentRect.left + point.getX() * contentRect.width() / unlockedVideoSize.getWidth(); + int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight(); + return new Point(convertedX, convertedY); } public static String getDeviceName() { return Build.MODEL; } + public boolean supportsInputEvents() { + return supportsInputEvents; + } + public boolean injectInputEvent(InputEvent inputEvent, int mode) { + if (!supportsInputEvents()) { + throw new AssertionError("Could not inject input event if !supportsInputEvents()"); + } + + if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) { + return false; + } + return serviceManager.getInputManager().injectInputEvent(inputEvent, mode); } @@ -129,8 +133,8 @@ public final class Device { return serviceManager.getPowerManager().isScreenOn(); } - public void registerRotationWatcher(IRotationWatcher rotationWatcher) { - serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher); + public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { + serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); } public synchronized void setRotationListener(RotationListener rotationListener) { @@ -154,8 +158,10 @@ public final class Device { } public void setClipboardText(String text) { - serviceManager.getClipboardManager().setText(text); - Ln.i("Device clipboard set"); + boolean ok = serviceManager.getClipboardManager().setText(text); + if (ok) { + Ln.i("Device clipboard set"); + } } /** @@ -167,8 +173,10 @@ public final class Device { Ln.e("Could not get built-in display"); return; } - SurfaceControl.setDisplayPowerMode(d, mode); - Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + boolean ok = SurfaceControl.setDisplayPowerMode(d, mode); + if (ok) { + Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + } } /** @@ -191,8 +199,4 @@ public final class Device { wm.thawRotation(); } } - - static Rect flipRect(Rect crop) { - return new Rect(crop.top, crop.left, crop.bottom, crop.right); - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java index e2a3a1a2..6c7f3634 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -13,7 +13,6 @@ public class DeviceMessageWriter { private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); - @SuppressWarnings("checkstyle:MagicNumber") public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { buffer.clear(); buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD); diff --git a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java index 639869b5..4b8036f8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java @@ -1,12 +1,24 @@ package com.genymobile.scrcpy; public final class DisplayInfo { + private final int displayId; private final Size size; private final int rotation; + private final int layerStack; + private final int flags; - public DisplayInfo(Size size, int rotation) { + public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; + + public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) { + this.displayId = displayId; this.size = size; this.rotation = rotation; + this.layerStack = layerStack; + this.flags = flags; + } + + public int getDisplayId() { + return displayId; } public Size getSize() { @@ -16,5 +28,13 @@ public final class DisplayInfo { public int getRotation() { return rotation; } + + public int getLayerStack() { + return layerStack; + } + + public int getFlags() { + return flags; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java b/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java new file mode 100644 index 00000000..81e3b903 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java @@ -0,0 +1,21 @@ +package com.genymobile.scrcpy; + +public class InvalidDisplayIdException extends RuntimeException { + + private final int displayId; + private final int[] availableDisplayIds; + + public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) { + super("There is no display having id " + displayId); + this.displayId = displayId; + this.availableDisplayIds = availableDisplayIds; + } + + public int getDisplayId() { + return displayId; + } + + public int[] getAvailableDisplayIds() { + return availableDisplayIds; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 5b993f30..2a4bba33 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -6,10 +6,12 @@ public class Options { private int maxSize; private int bitRate; private int maxFps; + private int lockedVideoOrientation; private boolean tunnelForward; private Rect crop; private boolean sendFrameMeta; // send PTS so that the client may record properly private boolean control; + private int displayId; public int getMaxSize() { return maxSize; @@ -35,6 +37,14 @@ public class Options { this.maxFps = maxFps; } + public int getLockedVideoOrientation() { + return lockedVideoOrientation; + } + + public void setLockedVideoOrientation(int lockedVideoOrientation) { + this.lockedVideoOrientation = lockedVideoOrientation; + } + public boolean isTunnelForward() { return tunnelForward; } @@ -66,4 +76,12 @@ public class Options { public void setControl(boolean control) { this.control = control; } + + public int getDisplayId() { + return displayId; + } + + public void setDisplayId(int displayId) { + this.displayId = displayId; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/Position.java index b46d2f73..e9b6d8a2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Position.java +++ b/server/src/main/java/com/genymobile/scrcpy/Position.java @@ -23,6 +23,19 @@ public class Position { return screenSize; } + public Position rotate(int rotation) { + switch (rotation) { + case 1: + return new Position(new Point(screenSize.getHeight() - point.getY(), point.getX()), screenSize.rotate()); + case 2: + return new Position(new Point(screenSize.getWidth() - point.getX(), screenSize.getHeight() - point.getY()), screenSize); + case 3: + return new Position(new Point(point.getY(), screenSize.getWidth() - point.getX()), screenSize.rotate()); + default: + return this; + } + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index c9a37f84..fc1a25b1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -6,7 +6,6 @@ import android.graphics.Rect; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; -import android.os.Build; import android.os.IBinder; import android.view.Surface; @@ -19,6 +18,7 @@ public class ScreenEncoder implements Device.RotationListener { private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms + private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder"; private static final int NO_PTS = -1; @@ -27,19 +27,13 @@ public class ScreenEncoder implements Device.RotationListener { private int bitRate; private int maxFps; - private int iFrameInterval; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; - this.iFrameInterval = iFrameInterval; - } - - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { - this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); } @Override @@ -55,19 +49,26 @@ public class ScreenEncoder implements Device.RotationListener { Workarounds.prepareMainLooper(); Workarounds.fillAppInfo(); - MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval); + MediaFormat format = createFormat(bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); device.setRotationListener(this); boolean alive; try { do { MediaCodec codec = createCodec(); IBinder display = createDisplay(); - Rect contentRect = device.getScreenInfo().getContentRect(); - Rect videoRect = device.getScreenInfo().getVideoSize().toRect(); + ScreenInfo screenInfo = device.getScreenInfo(); + Rect contentRect = screenInfo.getContentRect(); + // include the locked video orientation + Rect videoRect = screenInfo.getVideoSize().toRect(); + // does not include the locked video orientation + Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); + int videoRotation = screenInfo.getVideoRotation(); + int layerStack = device.getLayerStack(); + setSize(format, videoRect.width(), videoRect.height()); configure(codec, format); Surface surface = codec.createInputSurface(); - setDisplaySurface(display, surface, contentRect, videoRect); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); codec.start(); try { alive = encode(codec, fd); @@ -135,13 +136,12 @@ public class ScreenEncoder implements Device.RotationListener { } private static MediaCodec createCodec() throws IOException { - return MediaCodec.createEncoderByType("video/avc"); + return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); } - @SuppressWarnings("checkstyle:MagicNumber") private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) { MediaFormat format = new MediaFormat(); - format.setString(MediaFormat.KEY_MIME, "video/avc"); + format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); // must be present to configure the encoder, but does not impact the actual frame rate, which is variable format.setInteger(MediaFormat.KEY_FRAME_RATE, 60); @@ -150,11 +150,10 @@ public class ScreenEncoder implements Device.RotationListener { // display the very first frame, and recover from bad quality when no new frames format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs if (maxFps > 0) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - format.setFloat(MediaFormat.KEY_MAX_FPS_TO_ENCODER, maxFps); - } else { - Ln.w("Max FPS is only supported since Android 10, the option has been ignored"); - } + // The key existed privately before Android 10: + // + // + format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps); } return format; } @@ -172,12 +171,12 @@ public class ScreenEncoder implements Device.RotationListener { format.setInteger(MediaFormat.KEY_HEIGHT, height); } - private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) { + private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) { SurfaceControl.openTransaction(); try { SurfaceControl.setDisplaySurface(display, surface); - SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect); - SurfaceControl.setDisplayLayerStack(display, 0); + SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); + SurfaceControl.setDisplayLayerStack(display, layerStack); } finally { SurfaceControl.closeTransaction(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java index f2fce1d6..10acfb50 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -3,29 +3,161 @@ package com.genymobile.scrcpy; import android.graphics.Rect; public final class ScreenInfo { + /** + * Device (physical) size, possibly cropped + */ private final Rect contentRect; // device size, possibly cropped - private final Size videoSize; - private final boolean rotated; - public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) { + /** + * Video size, possibly smaller than the device size, already taking the device rotation and crop into account. + *

+ * However, it does not include the locked video orientation. + */ + private final Size unlockedVideoSize; + + /** + * Device rotation, related to the natural device orientation (0, 1, 2 or 3) + */ + private final int deviceRotation; + + /** + * The locked video orientation (-1: disabled, 0: normal, 1: 90° CCW, 2: 180°, 3: 90° CW) + */ + private final int lockedVideoOrientation; + + public ScreenInfo(Rect contentRect, Size unlockedVideoSize, int deviceRotation, int lockedVideoOrientation) { this.contentRect = contentRect; - this.videoSize = videoSize; - this.rotated = rotated; + this.unlockedVideoSize = unlockedVideoSize; + this.deviceRotation = deviceRotation; + this.lockedVideoOrientation = lockedVideoOrientation; } public Rect getContentRect() { return contentRect; } - public Size getVideoSize() { - return videoSize; + /** + * Return the video size as if locked video orientation was not set. + * + * @return the unlocked video size + */ + public Size getUnlockedVideoSize() { + return unlockedVideoSize; } - public ScreenInfo withRotation(int rotation) { - boolean newRotated = (rotation & 1) != 0; - if (rotated == newRotated) { + /** + * Return the actual video size if locked video orientation is set. + * + * @return the actual video size + */ + public Size getVideoSize() { + if (getVideoRotation() % 2 == 0) { + return unlockedVideoSize; + } + + return unlockedVideoSize.rotate(); + } + + public int getDeviceRotation() { + return deviceRotation; + } + + public ScreenInfo withDeviceRotation(int newDeviceRotation) { + if (newDeviceRotation == deviceRotation) { return this; } - return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated); + // true if changed between portrait and landscape + boolean orientationChanged = (deviceRotation + newDeviceRotation) % 2 != 0; + Rect newContentRect; + Size newUnlockedVideoSize; + if (orientationChanged) { + newContentRect = flipRect(contentRect); + newUnlockedVideoSize = unlockedVideoSize.rotate(); + } else { + newContentRect = contentRect; + newUnlockedVideoSize = unlockedVideoSize; + } + return new ScreenInfo(newContentRect, newUnlockedVideoSize, newDeviceRotation, lockedVideoOrientation); + } + + public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) { + int rotation = displayInfo.getRotation(); + Size deviceSize = displayInfo.getSize(); + Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); + if (crop != null) { + if (rotation % 2 != 0) { // 180s preserve dimensions + // the crop (provided by the user) is expressed in the natural orientation + crop = flipRect(crop); + } + if (!contentRect.intersect(crop)) { + // intersect() changes contentRect so that it is intersected with crop + Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); + contentRect = new Rect(); // empty + } + } + + Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); + return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation); + } + + private static String formatCrop(Rect rect) { + return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; + } + + private static Size computeVideoSize(int w, int h, int maxSize) { + // Compute the video size and the padding of the content inside this video. + // Principle: + // - scale down the great side of the screen to maxSize (if necessary); + // - scale down the other side so that the aspect ratio is preserved; + // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) + w &= ~7; // in case it's not a multiple of 8 + h &= ~7; + if (maxSize > 0) { + if (BuildConfig.DEBUG && maxSize % 8 != 0) { + throw new AssertionError("Max size must be a multiple of 8"); + } + boolean portrait = h > w; + int major = portrait ? h : w; + int minor = portrait ? w : h; + if (major > maxSize) { + int minorExact = minor * maxSize / major; + // +4 to round the value to the nearest multiple of 8 + minor = (minorExact + 4) & ~7; + major = maxSize; + } + w = portrait ? minor : major; + h = portrait ? major : minor; + } + return new Size(w, h); + } + + private static Rect flipRect(Rect crop) { + return new Rect(crop.top, crop.left, crop.bottom, crop.right); + } + + /** + * Return the rotation to apply to the device rotation to get the requested locked video orientation + * + * @return the rotation offset + */ + public int getVideoRotation() { + if (lockedVideoOrientation == -1) { + // no offset + return 0; + } + return (deviceRotation + 4 - lockedVideoOrientation) % 4; + } + + /** + * Return the rotation to apply to the requested locked video orientation to get the device rotation + * + * @return the (reverse) rotation offset + */ + public int getReverseVideoRotation() { + if (lockedVideoOrientation == -1) { + // no offset + return 0; + } + return (lockedVideoOrientation + 4 - deviceRotation) % 4; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 56b738fb..a6f7a78c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -16,6 +16,7 @@ public final class Server { } private static void scrcpy(Options options) throws IOException { + Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); final Device device = new Device(options); boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { @@ -67,7 +68,6 @@ public final class Server { }).start(); } - @SuppressWarnings("checkstyle:MagicNumber") private static Options createOptions(String... args) { if (args.length < 1) { throw new IllegalArgumentException("Missing client version"); @@ -76,11 +76,11 @@ public final class Server { String clientVersion = args[0]; if (!clientVersion.equals(BuildConfig.VERSION_NAME)) { throw new IllegalArgumentException( - "The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")"); + "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - if (args.length != 8) { - throw new IllegalArgumentException("Expecting 8 parameters"); + if (args.length != 10) { + throw new IllegalArgumentException("Expecting 10 parameters"); } Options options = new Options(); @@ -94,23 +94,28 @@ public final class Server { int maxFps = Integer.parseInt(args[3]); options.setMaxFps(maxFps); + int lockedVideoOrientation = Integer.parseInt(args[4]); + options.setLockedVideoOrientation(lockedVideoOrientation); + // use "adb forward" instead of "adb tunnel"? (so the server must listen) - boolean tunnelForward = Boolean.parseBoolean(args[4]); + boolean tunnelForward = Boolean.parseBoolean(args[5]); options.setTunnelForward(tunnelForward); - Rect crop = parseCrop(args[5]); + Rect crop = parseCrop(args[6]); options.setCrop(crop); - boolean sendFrameMeta = Boolean.parseBoolean(args[6]); + boolean sendFrameMeta = Boolean.parseBoolean(args[7]); options.setSendFrameMeta(sendFrameMeta); - boolean control = Boolean.parseBoolean(args[7]); + boolean control = Boolean.parseBoolean(args[8]); options.setControl(control); + int displayId = Integer.parseInt(args[9]); + options.setDisplayId(displayId); + return options; } - @SuppressWarnings("checkstyle:MagicNumber") private static Rect parseCrop(String crop) { if ("-".equals(crop)) { return null; @@ -135,7 +140,6 @@ public final class Server { } } - @SuppressWarnings("checkstyle:MagicNumber") private static void suggestFix(Throwable e) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (e instanceof MediaCodec.CodecException) { @@ -147,6 +151,16 @@ public final class Server { } } } + if (e instanceof InvalidDisplayIdException) { + InvalidDisplayIdException idie = (InvalidDisplayIdException) e; + int[] displayIds = idie.getAvailableDisplayIds(); + if (displayIds != null && displayIds.length > 0) { + Ln.e("Try to use one of the available display ids:"); + for (int id : displayIds) { + Ln.e(" scrcpy --display " + id); + } + } + } } public static void main(String... args) throws Exception { diff --git a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java index 199fc8c1..dac05466 100644 --- a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java @@ -5,7 +5,6 @@ public final class StringUtils { // not instantiable } - @SuppressWarnings("checkstyle:MagicNumber") public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) { int len = utf8.length; if (len <= maxLength) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index b1b81903..351cc574 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -28,7 +28,7 @@ public final class Workarounds { Looper.prepareMainLooper(); } - @SuppressLint("PrivateApi") + @SuppressLint("PrivateApi,DiscouragedPrivateApi") public static void fillAppInfo() { try { // ActivityThread activityThread = new ActivityThread(); @@ -73,7 +73,7 @@ public final class Workarounds { mInitialApplicationField.set(activityThread, app); } catch (Throwable throwable) { // this is a workaround, so failing is not an error - Ln.w("Could not fill app info: " + throwable.getMessage()); + Ln.d("Could not fill app info: " + throwable.getMessage()); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index 592bdf6b..ade23032 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -74,13 +74,15 @@ public class ClipboardManager { } } - public void setText(CharSequence text) { + public boolean setText(CharSequence text) { try { Method method = getSetPrimaryClipMethod(); ClipData clipData = ClipData.newPlainText(null, text); setPrimaryClip(method, manager, clipData); + return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); + return false; } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index 568afacd..cedb3f47 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -12,15 +12,28 @@ public final class DisplayManager { this.manager = manager; } - public DisplayInfo getDisplayInfo() { + public DisplayInfo getDisplayInfo(int displayId) { try { - Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0); + Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); + if (displayInfo == null) { + return null; + } Class cls = displayInfo.getClass(); // width and height already take the rotation into account int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo); int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo); int rotation = cls.getDeclaredField("rotation").getInt(displayInfo); - return new DisplayInfo(new Size(width, height), rotation); + int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); + int flags = cls.getDeclaredField("flags").getInt(displayInfo); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public int[] getDisplayIds() { + try { + return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager); } catch (Exception e) { throw new AssertionError(e); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 44fa613b..e17b5a17 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -17,6 +17,8 @@ public final class InputManager { private final IInterface manager; private Method injectInputEventMethod; + private static Method setDisplayIdMethod; + public InputManager(IInterface manager) { this.manager = manager; } @@ -37,4 +39,22 @@ public final class InputManager { return false; } } + + private static Method getSetDisplayIdMethod() throws NoSuchMethodException { + if (setDisplayIdMethod == null) { + setDisplayIdMethod = InputEvent.class.getMethod("setDisplayId", int.class); + } + return setDisplayIdMethod; + } + + public static boolean setDisplayId(InputEvent inputEvent, int displayId) { + try { + Method method = getSetDisplayIdMethod(); + method.invoke(inputEvent, displayId); + return true; + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Cannot associate a display id to the input event", e); + return false; + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index 0b625c92..cc567837 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -6,7 +6,7 @@ import android.os.IInterface; import java.lang.reflect.Method; -@SuppressLint("PrivateApi") +@SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class ServiceManager { private final Method getServiceMethod; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 227bbc85..8fbb860b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -121,12 +121,14 @@ public final class SurfaceControl { return setDisplayPowerModeMethod; } - public static void setDisplayPowerMode(IBinder displayToken, int mode) { + public static boolean setDisplayPowerMode(IBinder displayToken, int mode) { try { Method method = getSetDisplayPowerModeMethod(); method.invoke(null, displayToken, mode); + return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); + return false; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index cc687cd5..faa366a5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -93,13 +93,13 @@ public final class WindowManager { } } - public void registerRotationWatcher(IRotationWatcher rotationWatcher) { + public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { try { Class cls = manager.getClass(); try { // display parameter added since this commit: // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1 - cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0); + cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId); } catch (NoSuchMethodException e) { // old version cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 5e663bb9..202126a5 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -28,6 +28,9 @@ public class ControlMessageReaderTest { dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_KEYCODE_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); @@ -63,7 +66,7 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); - byte[] text = new byte[ControlMessageReader.TEXT_MAX_LENGTH]; + byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH]; Arrays.fill(text, (byte) 'a'); dos.writeShort(text.length); dos.write(text); @@ -77,7 +80,6 @@ public class ControlMessageReaderTest { } @Test - @SuppressWarnings("checkstyle:MagicNumber") public void testParseTouchEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); @@ -95,6 +97,9 @@ public class ControlMessageReaderTest { byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_TOUCH_EVENT_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); @@ -110,7 +115,6 @@ public class ControlMessageReaderTest { } @Test - @SuppressWarnings("checkstyle:MagicNumber") public void testParseScrollEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); @@ -126,6 +130,9 @@ public class ControlMessageReaderTest { byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_SCROLL_EVENT_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); @@ -233,6 +240,9 @@ public class ControlMessageReaderTest { byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); diff --git a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java index 7d89ee64..89799c5e 100644 --- a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java @@ -8,7 +8,6 @@ import java.nio.charset.StandardCharsets; public class StringUtilsTest { @Test - @SuppressWarnings("checkstyle:MagicNumber") public void testUtf8Truncate() { String s = "aÉbÔc"; byte[] utf8 = s.getBytes(StandardCharsets.UTF_8);