diff --git a/BUILD.md b/BUILD.md index 3fef0f23..8801e5fc 100644 --- a/BUILD.md +++ b/BUILD.md @@ -224,7 +224,7 @@ sudo ninja -Cx install # without sudo on Windows This installs two files: - `/usr/local/bin/scrcpy` - - `/usr/local/share/scrcpy/scrcpy-server.jar` + - `/usr/local/share/scrcpy/scrcpy-server` Just remove them to "uninstall" the application. @@ -233,17 +233,17 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.10.jar`][direct-scrcpy-server] - _(SHA-256: cbeb1a4e046f1392c1dc73c3ccffd7f86dec4636b505556ea20929687a119390)_ + - [`scrcpy-server-v1.11`][direct-scrcpy-server] + _(SHA-256: ff3a454012e91d9185cfe8ca7691cea16c43a7dcc08e92fa47ab9f0ea675abd1)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-server-v1.10.jar +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-server-v1.11 Download the prebuilt server somewhere, and specify its path during the Meson configuration: ```bash meson x --buildtype release --strip -Db_lto=true \ - -Dprebuilt_server=/path/to/scrcpy-server.jar + -Dprebuilt_server=/path/to/scrcpy-server ninja -Cx sudo ninja -Cx install ``` diff --git a/DEVELOP.md b/DEVELOP.md index dea8137d..92c3ce87 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -3,7 +3,7 @@ ## Overview This application is composed of two parts: - - the server (`scrcpy-server.jar`), to be executed on the device, + - the server (`scrcpy-server`), to be executed on the device, - the client (the `scrcpy` binary), executed on the host computer. The client is responsible to push the server to the device and start its @@ -49,7 +49,7 @@ application may not replace the server just before the client executes it._ Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing `classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle build system, the server is built to an (unsigned) APK (renamed to -`scrcpy-server.jar`). +`scrcpy-server`). [dex]: https://en.wikipedia.org/wiki/Dalvik_(software) [apk]: https://en.wikipedia.org/wiki/Android_application_package @@ -268,3 +268,33 @@ For more details, go read the code! If you find a bug, or have an awesome idea to implement, please discuss and contribute ;-) + + +### Debug the server + +The server is pushed to the device by the client on startup. + +To debug it, enable the server debugger during configuration: + +```bash +meson x -Dserver_debugger=true +# or, if x is already configured +meson configure x -Dserver_debugger=true +``` + +Then recompile. + +When you start scrcpy, it will start a debugger on port 5005 on the device. +Redirect that port to the computer: + +```bash +adb forward tcp:5005 tcp:5005 +``` + +In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on +`+`, _Remote_, and fill the form: + + - Host: `localhost` + - Port: `5005` + +Then click on _Debug_. diff --git a/FAQ.ko.md b/FAQ.ko.md new file mode 100644 index 00000000..6cc1a1d9 --- /dev/null +++ b/FAQ.ko.md @@ -0,0 +1,84 @@ +# 자주하는 질문 (FAQ) + +다음은 자주 제보되는 문제들과 그들의 현황입니다. + + +### Window 운영체제에서, 디바이스가 발견되지 않습니다. + +가장 흔한 제보는 `adb`에 발견되지 않는 디바이스 혹은 권한 관련 문제입니다. +다음 명령어를 호출하여 모든 것들에 이상이 없는지 확인하세요: + + adb devices + +Window는 당신의 디바이스를 감지하기 위해 [drivers]가 필요할 수도 있습니다. + +[drivers]: https://developer.android.com/studio/run/oem-usb.html + + +### 내 디바이스의 미러링만 가능하고, 디바이스와 상호작용을 할 수 없습니다. + +일부 디바이스에서는, [simulating input]을 허용하기 위해서 한가지 옵션을 활성화해야 할 수도 있습니다. +개발자 옵션에서 (developer options) 다음을 활성화 하세요: + +> **USB debugging (Security settings)** +> _권한 부여와 USB 디버깅을 통한 simulating input을 허용한다_ + +[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +### 마우스 클릭이 다른 곳에 적용됩니다. + +Mac 운영체제에서, HiDPI support 와 여러 스크린 창이 있는 경우, 입력 위치가 잘못 파악될 수 있습니다. +[issue 15]를 참고하세요. + +[issue 15]: https://github.com/Genymobile/scrcpy/issues/15 + +차선책은 HiDPI support을 비활성화 하고 build하는 방법입니다: + +```bash +meson x --buildtype release -Dhidpi_support=false +``` + +하지만, 동영상은 낮은 해상도로 재생될 것 입니다. + + +### HiDPI display의 화질이 낮습니다. + +Windows에서는, [scaling behavior] 환경을 설정해야 할 수도 있습니다. + +> `scrcpy.exe` > Properties > Compatibility > Change high DPI settings > +> Override high DPI scaling behavior > Scaling performed by: _Application_. + +[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 + + +### KWin compositor가 실행되지 않습니다 + +Plasma Desktop에서는,_scrcpy_ 가 실행중에는 compositor가 비활성화 됩니다. + +차석책으로는, ["Block compositing"를 비활성화하세요][kwin]. + +[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 + + +###비디오 스트림을 열 수 없는 에러가 발생합니다.(Could not open video stream). + +여러가지 원인이 있을 수 있습니다. 가장 흔한 원인은 디바이스의 하드웨어 인코더(hardware encoder)가 +주어진 해상도를 인코딩할 수 없는 경우입니다. + +``` +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 +``` + +더 낮은 해상도로 시도 해보세요: + +``` +scrcpy -m 1920 +scrcpy -m 1024 +scrcpy -m 800 +``` diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index c07cb24f..2b30dcb5 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -3,7 +3,7 @@ # # Here, "portable" means that the client and server binaries are expected to be # anywhere, but in the same directory, instead of well-defined separate -# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server.jar). +# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server). # # In particular, this implies to change the location from where the client push # the server to the device. @@ -97,14 +97,14 @@ build-win64-noconsole: prepare-deps-win64 dist-win32: build-server build-win32 build-win32-noconsole mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/" + cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -112,14 +112,14 @@ dist-win32: build-server build-win32 build-win32-noconsole dist-win64: build-server build-win64 build-win64-noconsole mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN64_TARGET_DIR)/" + cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" diff --git a/README.ko.md b/README.ko.md new file mode 100644 index 00000000..564acae7 --- /dev/null +++ b/README.ko.md @@ -0,0 +1,498 @@ +# scrcpy (v1.11) + +This document will be updated frequently along with the original Readme file +이 문서는 원어 리드미 파일의 업데이트에 따라 종종 업데이트 될 것입니다 + + 이 어플리케이션은 UBS ( 혹은 [TCP/IP][article-tcpip] ) 로 연결된 Android 디바이스를 화면에 보여주고 관리하는 것을 제공합니다. + _GNU/Linux_, _Windows_ 와 _macOS_ 상에서 작동합니다. + (아래 설명에서 디바이스는 안드로이드 핸드폰을 의미합니다.) + +[article-tcpip]:https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ + +![screenshot](https://github.com/Genymobile/scrcpy/blob/master/assets/screenshot-debian-600.jpg?raw=true) + +주요 기능은 다음과 같습니다. + + - **가벼움** (기본적이며 디바이스의 화면만을 보여줌) + - **뛰어난 성능** (30~60fps) + - **높은 품질** (1920×1080 이상의 해상도) + - **빠른 반응 속도** ([35~70ms][lowlatency]) + - **짧은 부팅 시간** (첫 사진을 보여주는데 최대 1초 소요됨) + - **장치 설치와는 무관함** (디바이스에 설치하지 않아도 됨) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## 요구사항 + +안드로이드 장치는 최소 API 21 (Android 5.0) 을 필요로 합니다. + +디바이스에 [adb debugging][enable-adb]이 가능한지 확인하십시오. + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + +어떤 디바이스에서는, 키보드와 마우스를 사용하기 위해서 [추가 옵션][control] 이 필요하기도 합니다. + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## 앱 설치하기 + + +### Linux (리눅스) + +리눅스 상에서는 보통 [어플을 직접 설치][BUILD] 해야합니다. 어렵지 않으므로 걱정하지 않아도 됩니다. + +[BUILD]:https://github.com/Genymobile/scrcpy/blob/master/BUILD.md + +[Snap] 패키지가 가능합니다 : [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +Arch Linux에서, [AUR] 패키지가 가능합니다 : [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +Gentoo에서 ,[Ebuild] 가 가능합니다 : [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + + +### Windows (윈도우) + +윈도우 상에서, 간단하게 설치하기 위해 종속성이 있는 사전 구축된 아카이브가 제공됩니다 (`adb` 포함) : +해당 파일은 Readme원본 링크를 통해서 다운로드가 가능합니다. + - [`scrcpy-win`][direct-win] + +[direct-win]: https://github.com/Genymobile/scrcpy/blob/master/README.md#windows + + +[어플을 직접 설치][BUILD] 할 수도 있습니다. + + +### macOS (맥 OS) + +이 어플리케이션은 아래 사항을 따라 설치한다면 [Homebrew] 에서도 사용 가능합니다 : + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +`PATH` 로부터 접근 가능한 `adb` 가 필요합니다. 아직 설치하지 않았다면 다음을 따라 설치해야 합니다 : + +```bash +brew cask install android-platform-tools +``` + +[어플을 직접 설치][BUILD] 할 수도 있습니다. + + +## 실행 + +안드로이드 디바이스를 연결하고 실행하십시오: + +```bash +scrcpy +``` + +다음과 같이 명령창 옵션 기능도 제공합니다. + +```bash +scrcpy --help +``` + +## 기능 + +### 캡쳐 환경 설정 + + +###사이즈 재정의 + +가끔씩 성능을 향상시키기위해 안드로이드 디바이스를 낮은 해상도에서 미러링하는 것이 유용할 때도 있습니다. + +너비와 높이를 제한하기 위해 특정 값으로 지정할 수 있습니다 (e.g. 1024) : + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # 축약 버전 +``` + +이 외의 크기도 디바이스의 가로 세로 비율이 유지된 상태에서 계산됩니다. +이러한 방식으로 디바이스 상에서 1920×1080 는 모니터 상에서1024×576로 미러링될 것 입니다. + + +### bit-rate 변경 + +기본 bit-rate 는 8 Mbps입니다. 비디오 bit-rate 를 변경하기 위해선 다음과 같이 입력하십시오 (e.g. 2 Mbps로 변경): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # 축약 버전 +``` + +###프레임 비율 제한 + +안드로이드 버전 10이상의 디바이스에서는, 다음의 명령어로 캡쳐 화면의 프레임 비율을 제한할 수 있습니다: + +```bash +scrcpy --max-fps 15 +``` + + +### Crop (잘라내기) + +디바이스 화면은 화면의 일부만 미러링하기 위해 잘라질 것입니다. + +예를 들어, *Oculus Go* 의 한 쪽 눈만 미러링할 때 유용합니다 : + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) +scrcpy -c 1224:1440:0:0 # 축약 버전 +``` + +만약 `--max-size` 도 지정하는 경우, 잘라낸 다음에 재정의된 크기가 적용될 것입니다. + + +### 화면 녹화 + +미러링하는 동안 화면 녹화를 할 수 있습니다 : + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +녹화하는 동안 미러링을 멈출 수 있습니다 : + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# Ctrl+C 로 녹화를 중단할 수 있습니다. +# 윈도우 상에서 Ctrl+C 는 정상정으로 종료되지 않을 수 있으므로, 디바이스 연결을 해제하십시오. +``` + +"skipped frames" 은 모니터 화면에 보여지지 않았지만 녹화되었습니다 ( 성능 문제로 인해 ). 프레임은 디바이스 상에서 _타임 스탬프 ( 어느 시점에 데이터가 존재했다는 사실을 증명하기 위해 특정 위치에 시각을 표시 )_ 되었으므로, [packet delay +variation] 은 녹화된 파일에 영향을 끼치지 않습니다. + +[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation + +## 연결 + +### 무선연결 + +_Scrcpy_ 장치와 정보를 주고받기 위해 `adb` 를 사용합니다. `adb` 는 TCIP/IP 를 통해 디바이스와 [연결][connect] 할 수 있습니다 : + +1. 컴퓨터와 디바이스를 동일한 Wi-Fi 에 연결합니다. +2. 디바이스의 IP address 를 확인합니다 (설정 → 내 기기 → 상태 / 혹은 인터넷에 '내 IP'검색 시 확인 가능합니다. ). +3. TCP/IP 를 통해 디바이스에서 adb 를 사용할 수 있게 합니다: `adb tcpip 5555`. +4. 디바이스 연결을 해제합니다. +5. adb 를 통해 디바이스에 연결을 합니다\: `adb connect DEVICE_IP:5555` _(`DEVICE_IP` 대신)_. +6. `scrcpy` 실행합니다. + +다음은 bit-rate 와 해상도를 줄이는데 유용합니다 : + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # 축약 버전 +``` + +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless + + + +### 여러 디바이스 사용 가능 + +만약에 여러 디바이스들이 `adb devices` 목록에 표시되었다면, _serial_ 을 명시해야합니다: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # 축약 버전 +``` + +_scrcpy_ 로 여러 디바이스를 연결해 사용할 수 있습니다. + + +#### SSH tunnel + +떨어져 있는 디바이스와 연결하기 위해서는, 로컬 `adb` client와 떨어져 있는 `adb` 서버를 연결해야 합니다. (디바이스와 클라이언트가 동일한 버전의 _adb_ protocol을 사용할 경우에 제공됩니다.): + +```bash +adb kill-server # 5037의 로컬 local adb server를 중단 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# 실행 유지 +``` + +다른 터미널에서는 : + +```bash +scrcpy +``` + +무선 연결과 동일하게, 화질을 줄이는 것이 나을 수 있습니다: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +## Window에서의 배치 + +### 맞춤형 window 제목 + +기본적으로, window의 이름은 디바이스의 모델명 입니다. +다음의 명령어를 통해 변경하세요. + +```bash +scrcpy --window-title 'My device' +``` + + +### 배치와 크기 + +초기 window창의 배치와 크기는 다음과 같이 설정할 수 있습니다: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + + +### 경계 없애기 + +윈도우 장식(경계선 등)을 다음과 같이 제거할 수 있습니다: + +```bash +scrcpy --window-borderless +``` + +### 항상 모든 윈도우 위에 실행창 고정 + +이 어플리케이션의 윈도우 창은 다음의 명령어로 다른 window 위에 디스플레이 할 수 있습니다: + +```bash +scrcpy --always-on-top +scrcpy -T # 축약 버전 +``` + +### 전체 화면 + +이 어플리케이션은 전체화면으로 바로 시작할 수 있습니다. + +```bash +scrcpy --fullscreen +scrcpy -f # short version +``` + +전체 화면은 `Ctrl`+`f`키로 끄거나 켤 수 있습니다. + + +## 다른 미러링 옵션 + +### 읽기 전용(Read-only) + +권한을 제한하기 위해서는 (디바이스와 관련된 모든 것: 입력 키, 마우스 이벤트 , 파일의 드래그 앤 드랍(drag&drop)): + +```bash +scrcpy --no-control +scrcpy -n +``` + +### 화면 끄기 + +미러링을 실행하는 와중에 디바이스의 화면을 끌 수 있게 하기 위해서는 +다음의 커맨드 라인 옵션을(command line option) 입력하세요: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +혹은 `Ctrl`+`o`을 눌러 언제든지 디바이스의 화면을 끌 수 있습니다. + +다시 화면을 켜기 위해서는`POWER` (혹은 `Ctrl`+`p`)를 누르세요. + + +### 유효기간이 지난 프레임 제공 (Render expired frames) + +디폴트로, 대기시간을 최소화하기 위해 _scrcpy_ 는 항상 마지막으로 디코딩된 프레임을 제공합니다 +과거의 프레임은 하나씩 삭제합니다. + +모든 프레임을 강제로 렌더링하기 위해서는 (대기 시간이 증가될 수 있습니다) +다음의 명령어를 사용하세요: + +```bash +scrcpy --render-expired-frames +``` + + +### 화면에 터치 나타내기 + +발표를 할 때, 물리적인 기기에 한 물리적 터치를 나타내는 것이 유용할 수 있습니다. + +안드로이드 운영체제는 이런 기능을 _Developers options_에서 제공합니다. + +_Scrcpy_ 는 이런 기능을 시작할 때와 종료할 때 옵션으로 제공합니다. + +```bash +scrcpy --show-touches +scrcpy -t +``` + +화면에 _물리적인 터치만_ 나타나는 것에 유의하세요 (손가락을 디바이스에 대는 행위). + + +### 입력 제어 + +#### 복사-붙여넣기 + +컴퓨터와 디바이스 양방향으로 클립보드를 복사하는 것이 가능합니다: + + - `Ctrl`+`c` 디바이스의 클립보드를 컴퓨터로 복사합니다; + - `Ctrl`+`Shift`+`v` 컴퓨터의 클립보드를 디바이스로 복사합니다; + - `Ctrl`+`v` 컴퓨터의 클립보드를 text event 로써 _붙여넣습니다_ ( 그러나, ASCII 코드가 아닌 경우 실행되지 않습니다 ) + +#### 텍스트 삽입 우선 순위 + +텍스트를 입력할 때 생성되는 두 가지의 [events][textevents] 가 있습니다: + - _key events_, 키가 눌려있는 지에 대한 신호; + - _text events_, 텍스트가 입력되었는지에 대한 신호. + +기본적으로, 글자들은 key event 를 이용해 입력되기 때문에, 키보드는 게임에서처럼 처리합니다 ( 보통 WASD 키에 대해서 ). + +그러나 이는 [issues 를 발생][prefertext]시킵니다. 이와 관련된 문제를 접할 경우, 아래와 같이 피할 수 있습니다: + +```bash +scrcpy --prefer-text +``` + +( 그러나 이는 게임에서의 처리를 중단할 수 있습니다 ) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +### 파일 드랍 + +### APK 실행하기 + +APK를 실행하기 위해서는, APK file(파일명이`.apk`로 끝나는 파일)을 드래그하고 _scrcpy_ window에 드랍하세요 (drag and drop) + +시각적인 피드백은 없고,log 하나가 콘솔에 출력될 것입니다. + +### 디바이스에 파일 push하기 + +디바이스의`/sdcard/`에 파일을 push하기 위해서는, +APK파일이 아닌 파일을_scrcpy_ window에 드래그하고 드랍하세요.(drag and drop). + +시각적인 피드백은 없고,log 하나가 콘솔에 출력될 것입니다. + +해당 디렉토리는 시작할 때 변경이 가능합니다: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` + +### 오디오의 전달 + +_scrcpy_는 오디오를 직접 전달해주지 않습니다. [USBaudio] (Linux-only)를 사용하세요. + +추가적으로 [issue #14]를 참고하세요. + +[USBaudio]: https://github.com/rom1v/usbaudio +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + +## 단축키 + + | 실행내용 | 단축키 | 단축키 (macOS) + | -------------------------------------- |:----------------------------- |:----------------------------- + | 전체화면 모드로 전환 | `Ctrl`+`f` | `Cmd`+`f` + | window를 1:1비율로 전환하기(픽셀 맞춤) | `Ctrl`+`g` | `Cmd`+`g` + | 검은 공백 제거 위한 window 크기 조정 | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ + |`HOME` 클릭 | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ + | `BACK` 클릭 | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_ + | `APP_SWITCH` 클릭 | `Ctrl`+`s` | `Cmd`+`s` + | `MENU` 클릭 | `Ctrl`+`m` | `Ctrl`+`m` + | `VOLUME_UP` 클릭 | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_ + | `VOLUME_DOWN` 클릭 | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_ + | `POWER` 클릭 | `Ctrl`+`p` | `Cmd`+`p` + | 전원 켜기 | _Right-click²_ | _Right-click²_ + | 미러링 중 디바이스 화면 끄기 | `Ctrl`+`o` | `Cmd`+`o` + | 알림 패널 늘리기 | `Ctrl`+`n` | `Cmd`+`n` + | 알림 패널 닫기 | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` + | 디바이스의 clipboard 컴퓨터로 복사하기 | `Ctrl`+`c` | `Cmd`+`c` + | 컴퓨터의 clipboard 디바이스에 붙여넣기 | `Ctrl`+`v` | `Cmd`+`v` + | Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` + | Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i` + +_¹검은 공백을 제거하기 위해서는 그 부분을 더블 클릭하세요_ +_²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상태에서는 뒤로 돌아갑니다. + +## 맞춤 경로 (custom path) + +특정한 _adb_ binary를 사용하기 위해서는, 그것의 경로를 환경변수로 설정하세요. +`ADB`: + + ADB=/path/to/adb scrcpy + +`scrcpy-server.jar`파일의 경로에 오버라이드 하기 위해서는, 그것의 경로를 `SCRCPY_SERVER_PATH`에 저장하세요. + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + + +## _scrcpy_ 인 이유? + +한 동료가 [gnirehtet]와 같이 발음하기 어려운 이름을 찾을 수 있는지 도발했습니다. + +[`strcpy`] 는 **str**ing을 copy하고; `scrcpy`는 **scr**een을 copy합니다. + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + + +## 빌드하는 방법? + +[BUILD]을 참고하세요. + +[BUILD]: BUILD.md + +## 흔한 issue + +[FAQ](FAQ.md)을 참고하세요. + + +## 개발자들 + +[developers page]를 참고하세요. + +[developers page]: DEVELOP.md + + +## 라이선스 + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-2019 Romain Vimont + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + 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. + +## 관련 글 (articles) + +- [scrcpy 소개][article-intro] +- [무선으로 연결하는 Scrcpy][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/README.md b/README.md index f698cb4c..677e7a1c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.10) +# scrcpy (v1.11) This application provides display and control of Android devices connected on USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. @@ -62,13 +62,13 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. For Windows, for simplicity, prebuilt archives with all the dependencies (including `adb`) are available: - - [`scrcpy-win32-v1.10.zip`][direct-win32] - _(SHA-256: f98b400b3764404b33b212e9762dd6f1593ddb766c1480fc2609c94768e4a8e1)_ - - [`scrcpy-win64-v1.10.zip`][direct-win64] - _(SHA-256: 95de34575d873c7e95dfcfb5e74d0f6af4f70b2a5bc6fde0f48d1a05480e3a44)_ + - [`scrcpy-win32-v1.11.zip`][direct-win32] + _(SHA-256: f25ed46e6f3e81e0ff9b9b4df7fe1a4bbd13f8396b7391be0a488b64c675b41e)_ + - [`scrcpy-win64-v1.11.zip`][direct-win64] + _(SHA-256: 3802c9ea0307d437947ff150ec65e53990b0beaacd0c8d0bed19c7650ce141bd)_ -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-win32-v1.10.zip -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-win64-v1.10.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-win32-v1.11.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-win64-v1.11.zip You can also [build the app manually][BUILD]. @@ -108,8 +108,9 @@ scrcpy --help ## Features +### Capture configuration -### Reduce size +#### Reduce size Sometimes, it is useful to mirror an Android device at a lower definition to increase performance. @@ -125,7 +126,7 @@ The other dimension is computed to that the device aspect ratio is preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. -### Change bit-rate +#### Change bit-rate The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps): @@ -134,8 +135,15 @@ scrcpy --bit-rate 2M scrcpy -b 2M # short version ``` +#### Limit frame rate -### Crop +On devices with Android >= 10, the capture frame rate can be limited: + +```bash +scrcpy --max-fps 15 +``` + +#### Crop The device screen may be cropped to mirror only part of the screen. @@ -143,35 +151,12 @@ This is useful for example to mirror only one eye of the Oculus Go: ```bash scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) -scrcpy -c 1224:1440:0:0 # short version ``` If `--max-size` is also specified, resizing is applied after cropping. -### Wireless - -_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a -device over TCP/IP: - -1. Connect the device to the same Wi-Fi as your computer. -2. Get your device IP address (in Settings → About phone → Status). -3. Enable adb over TCP/IP on your device: `adb tcpip 5555`. -4. Unplug your device. -5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_. -6. Run `scrcpy` as usual. - -It may be useful to decrease the bit-rate and the definition: - -```bash -scrcpy --bit-rate 2M --max-size 800 -scrcpy -b2M -m800 # short version -``` - -[connect]: https://developer.android.com/studio/command-line/adb.html#wireless - - -### Record screen +### Recording It is possible to record the screen while mirroring: @@ -196,7 +181,31 @@ variation] does not impact the recorded file. [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation -### Multi-devices +### Connection + +#### Wireless + +_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a +device over TCP/IP: + +1. Connect the device to the same Wi-Fi as your computer. +2. Get your device IP address (in Settings → About phone → Status). +3. Enable adb over TCP/IP on your device: `adb tcpip 5555`. +4. Unplug your device. +5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_. +6. Run `scrcpy` as usual. + +It may be useful to decrease the bit-rate and the definition: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # short version +``` + +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### Multi-devices If several devices are listed in `adb devices`, you must specify the _serial_: @@ -207,8 +216,65 @@ scrcpy -s 0123456789abcdef # short version You can start several instances of _scrcpy_ for several devices. +#### SSH tunnel -### Fullscreen +To connect to a remote device, it is possible to connect a local `adb` client to +a remote `adb` server (provided they use the same version of the _adb_ +protocol): + +```bash +adb kill-server # kill the local adb server on 5037 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# keep this open +``` + +From another terminal: + +```bash +scrcpy +``` + +Like for wireless connections, it may be useful to reduce quality: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### Window configuration + +#### Title + +By default, the window title is the device model. It can be changed: + +```bash +scrcpy --window-title 'My device' +``` + +#### Position and size + +The initial window position and size may be specified: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### Borderless + +To disable window decorations: + +```bash +scrcpy --window-borderless +``` + +#### Always on top + +To keep the scrcpy window always on top: + +```bash +scrcpy --always-on-top +``` + +#### Fullscreen The app may be started directly in fullscreen: @@ -220,17 +286,45 @@ scrcpy -f # short version Fullscreen can then be toggled dynamically with `Ctrl`+`f`. -### Always on top +### Other mirroring options -The window of app can always be above others by: +#### Read-only + +To disable controls (everything which can interact with the device: input keys, +mouse events, drag&drop files): ```bash -scrcpy --always-on-top -scrcpy -T # short version +scrcpy --no-control +scrcpy -n ``` +#### Turn screen off -### Show touches +It is possible to turn the device screen off while mirroring on start with a +command-line option: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +Or by pressing `Ctrl`+`o` at any time. + +To turn it back on, press `POWER` (or `Ctrl`+`p`). + +#### Render expired frames + +By default, to minimize latency, _scrcpy_ always renders the last decoded frame +available, and drops any previous one. + +To force the rendering of all frames (at a cost of a possible increased +latency), use: + +```bash +scrcpy --render-expired-frames +``` + +#### Show touches For presentations, it may be useful to show physical touches (on the physical device). @@ -247,7 +341,43 @@ scrcpy -t Note that it only shows _physical_ touches (with the finger on the device). -### Install APK +### Input control + +#### Copy-paste + +It is possible to synchronize clipboards between the computer and the device, in +both directions: + + - `Ctrl`+`c` copies the device clipboard to the computer clipboard; + - `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard; + - `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but + breaks non-ASCII characters). + +#### Text injection preference + +There are two kinds of [events][textevents] generated when typing text: + - _key events_, signaling that a key is pressed or released; + - _text events_, signaling that a text has been entered. + +By default, letters are injected using key events, so that the keyboard behaves +as expected in games (typically for WASD keys). + +But this may [cause issues][prefertext]. If you encounter such a problem, you +can avoid it by: + +```bash +scrcpy --prefer-text +``` + +(but this will break keyboard behavior in games) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +### File drop + +#### Install APK To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_ window. @@ -255,7 +385,7 @@ window. There is no visual feedback, a log is printed to the console. -### Push file to device +#### Push file to device To push a file to `/sdcard/` on the device, drag & drop a (non-APK) file to the _scrcpy_ window. @@ -268,53 +398,8 @@ The target directory can be changed on start: scrcpy --push-target /sdcard/foo/bar/ ``` -### Read-only -To disable controls (everything which can interact with the device: input keys, -mouse events, drag&drop files): - -```bash -scrcpy --no-control -scrcpy -n -``` - -### Turn screen off - -It is possible to turn the device screen off while mirroring on start with a -command-line option: - -```bash -scrcpy --turn-screen-off -scrcpy -S -``` - -Or by pressing `Ctrl`+`o` at any time. - -To turn it back on, press `POWER` (or `Ctrl`+`p`). - - -### Render expired frames - -By default, to minimize latency, _scrcpy_ always renders the last decoded frame -available, and drops any previous one. - -To force the rendering of all frames (at a cost of a possible increased -latency), use: - -```bash -scrcpy --render-expired-frames -``` - -### Custom window title - -By default, the window title is the device model. It can be changed: - -```bash -scrcpy --window-title 'My device' -``` - - -### Forward audio +### Audio forwarding Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only). @@ -358,7 +443,7 @@ To use a specific _adb_ binary, configure its path in the environment variable ADB=/path/to/adb scrcpy -To override the path of the `scrcpy-server.jar` file, configure its path in +To override the path of the `scrcpy-server` file, configure its path in `SCRCPY_SERVER_PATH`. [useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 diff --git a/app/meson.build b/app/meson.build index 34f642d4..159ae695 100644 --- a/app/meson.build +++ b/app/meson.build @@ -93,7 +93,7 @@ conf.set_quoted('SCRCPY_VERSION', meson.project_version()) # the prefix used during configuration (meson --prefix=PREFIX) conf.set_quoted('PREFIX', get_option('prefix')) -# build a "portable" version (with scrcpy-server.jar accessible from the same +# build a "portable" version (with scrcpy-server accessible from the same # directory as the executable) conf.set('PORTABLE', get_option('portable')) @@ -115,15 +115,16 @@ conf.set('HIDPI_SUPPORT', get_option('hidpi_support')) # disable console on Windows conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole')) +# run a server debugger and wait for a client to be attached +conf.set('SERVER_DEBUGGER', get_option('server_debugger')) + configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') if get_option('windows_noconsole') - c_args = [ '-mwindows' ] - link_args = [ '-mwindows' ] + link_args = [ '-Wl,--subsystem,windows' ] else - c_args = [] link_args = [] endif @@ -131,7 +132,7 @@ executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true, - c_args: c_args, + c_args: [], link_args: link_args) install_man('scrcpy.1') diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 67db3569..c77fd985 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -15,6 +15,10 @@ provides display and control of Android devices connected on USB (or over TCP/IP .SH OPTIONS +.TP +.B \-\-always\-on\-top +Make scrcpy window always on top (above other windows). + .TP .BI "\-b, \-\-bit\-rate " value Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). @@ -22,7 +26,7 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are Default is 8000000. .TP -.BI "\-c, \-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy +.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy Crop the device screen on the server. The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any @@ -33,14 +37,14 @@ value is computed on the cropped size. .B \-f, \-\-fullscreen Start in fullscreen. -.TP -.BI "\-F, \-\-record\-format " format -Force recording format (either mp4 or mkv). - .TP .B \-h, \-\-help Print this help. +.TP +.BI "\-\-max\-fps " value +Limit the framerate of screen capture (only supported on devices with Android >= 10). + .TP .BI "\-m, \-\-max\-size " value Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. @@ -61,6 +65,13 @@ Set the TCP port the client listens on. Default is 27183. +.TP +.B \-\-prefer\-text +Inject alpha characters and space as text events instead of key events. + +This avoids issues when combining multiple keys to enter special characters, +but breaks the expected behavior of alpha keys in games (typically WASD). + .TP .BI "\-\-push\-target " path Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push". @@ -73,9 +84,13 @@ Record screen to .IR file . The format is determined by the -.B \-F/\-\-record\-format +.B \-\-record\-format option if set, or by the file extension (.mp4 or .mkv). +.TP +.BI "\-\-record\-format " format +Force recording format (either mp4 or mkv). + .TP .B \-\-render\-expired\-frames By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency. @@ -94,18 +109,41 @@ Enable "show touches" on start, disable on quit. It only shows physical touches (not clicks from scrcpy). -.TP -.B \-T, \-\-always\-on\-top -Make scrcpy window always on top (above other windows). - .TP .B \-v, \-\-version Print the version of scrcpy. .TP -.B \-\-window\-title text +.B \-\-window\-borderless +Disable window decorations (display borderless window). + +.TP +.BI "\-\-window\-title " text Set a custom window title. +.TP +.BI "\-\-window\-x " value +Set the initial window horizontal position. + +Default is -1 (automatic).\n + +.TP +.BI "\-\-window\-y " value +Set the initial window vertical position. + +Default is -1 (automatic).\n + +.TP +.BI "\-\-window\-width " value +Set the initial window width. + +Default is 0 (automatic).\n + +.TP +.BI "\-\-window\-height " value +Set the initial window height. + +Default is 0 (automatic).\n .SH SHORTCUTS diff --git a/app/src/buffer_util.h b/app/src/buffer_util.h index 681421f3..262df1dc 100644 --- a/app/src/buffer_util.h +++ b/app/src/buffer_util.h @@ -20,6 +20,12 @@ buffer_write32be(uint8_t *buf, uint32_t value) { buf[3] = value; } +static inline void +buffer_write64be(uint8_t *buf, uint64_t value) { + buffer_write32be(buf, value >> 32); + buffer_write32be(&buf[4], (uint32_t) value); +} + static inline uint16_t buffer_read16be(const uint8_t *buf) { return (buf[0] << 8) | buf[1]; diff --git a/app/src/control_msg.c b/app/src/control_msg.c index fff93592..e042dc5a 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -1,6 +1,7 @@ #include "control_msg.h" #include +#include #include "config.h" #include "buffer_util.h" @@ -24,6 +25,16 @@ write_string(const char *utf8, size_t max_len, unsigned char *buf) { return 2 + len; } +static uint16_t +to_fixed_point_16(float f) { + SDL_assert(f >= 0.0f && f <= 1.0f); + uint32_t u = f * 0x1p16f; // 2^16 + if (u >= 0xffff) { + u = 0xffff; + } + return (uint16_t) u; +} + size_t control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buf[0] = msg->type; @@ -38,11 +49,15 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { CONTROL_MSG_TEXT_MAX_LENGTH, &buf[1]); return 1 + len; } - case CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT: - buf[1] = msg->inject_mouse_event.action; - buffer_write32be(&buf[2], msg->inject_mouse_event.buttons); - write_position(&buf[6], &msg->inject_mouse_event.position); - return 18; + case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: + buf[1] = msg->inject_touch_event.action; + buffer_write64be(&buf[2], msg->inject_touch_event.pointer_id); + write_position(&buf[10], &msg->inject_touch_event.position); + uint16_t pressure = + to_fixed_point_16(msg->inject_touch_event.pressure); + buffer_write16be(&buf[22], pressure); + buffer_write32be(&buf[24], msg->inject_touch_event.buttons); + return 28; case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: write_position(&buf[1], &msg->inject_scroll_event.position); buffer_write32be(&buf[13], diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 308d54a3..2f319d9d 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -15,10 +15,12 @@ #define CONTROL_MSG_SERIALIZED_MAX_SIZE \ (3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) +#define POINTER_ID_MOUSE UINT64_C(-1); + enum control_msg_type { CONTROL_MSG_TYPE_INJECT_KEYCODE, CONTROL_MSG_TYPE_INJECT_TEXT, - CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, + CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, @@ -48,8 +50,10 @@ struct control_msg { struct { enum android_motionevent_action action; enum android_motionevent_buttons buttons; + uint64_t pointer_id; struct position position; - } inject_mouse_event; + float pressure; + } inject_touch_event; struct { struct position position; int32_t hscroll; diff --git a/app/src/event_converter.c b/app/src/event_converter.c index 700e5c50..80ead615 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -5,7 +5,7 @@ #define MAP(FROM, TO) case FROM: *to = TO; return true #define FAIL default: return false -static bool +bool convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { switch (from) { MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN); @@ -33,7 +33,7 @@ autocomplete_metastate(enum android_metastate metastate) { return metastate; } -static enum android_metastate +enum android_metastate convert_meta_state(SDL_Keymod mod) { enum android_metastate metastate = 0; if (mod & KMOD_LSHIFT) { @@ -74,8 +74,9 @@ convert_meta_state(SDL_Keymod mod) { return autocomplete_metastate(metastate); } -static bool -convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { +bool +convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, + bool prefer_text) { switch (from) { MAP(SDLK_RETURN, AKEYCODE_ENTER); MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER); @@ -92,6 +93,12 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN); MAP(SDLK_UP, AKEYCODE_DPAD_UP); } + + if (prefer_text) { + // do not forward alpha and space key events + return false; + } + if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) { return false; } @@ -128,16 +135,7 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { } } -static bool -convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { - switch (from) { - MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); - MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP); - FAIL; - } -} - -static enum android_motionevent_buttons +enum android_motionevent_buttons convert_mouse_buttons(uint32_t state) { enum android_motionevent_buttons buttons = 0; if (state & SDL_BUTTON_LMASK) { @@ -159,67 +157,20 @@ convert_mouse_buttons(uint32_t state) { } bool -convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; - - if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { - return false; +convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { + switch (from) { + MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); + MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP); + FAIL; } - - uint16_t mod = from->keysym.mod; - if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) { - return false; - } - - to->inject_keycode.metastate = convert_meta_state(mod); - - return true; } bool -convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; - - if (!convert_mouse_action(from->type, &to->inject_mouse_event.action)) { - return false; +convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { + switch (from) { + MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE); + MAP(SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN); + MAP(SDL_FINGERUP, AMOTION_EVENT_ACTION_UP); + FAIL; } - - to->inject_mouse_event.buttons = - convert_mouse_buttons(SDL_BUTTON(from->button)); - to->inject_mouse_event.position.screen_size = screen_size; - to->inject_mouse_event.position.point.x = from->x; - to->inject_mouse_event.position.point.y = from->y; - - return true; -} - -bool -convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; - to->inject_mouse_event.action = AMOTION_EVENT_ACTION_MOVE; - to->inject_mouse_event.buttons = convert_mouse_buttons(from->state); - to->inject_mouse_event.position.screen_size = screen_size; - to->inject_mouse_event.position.point.x = from->x; - to->inject_mouse_event.position.point.y = from->y; - - return true; -} - -bool -convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; - - to->inject_scroll_event.position = position; - - int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1; - // SDL behavior seems inconsistent between horizontal and vertical scrolling - // so reverse the horizontal - // - to->inject_scroll_event.hscroll = -mul * from->x; - to->inject_scroll_event.vscroll = mul * from->y; - - return true; } diff --git a/app/src/event_converter.h b/app/src/event_converter.h index e0b24c15..c41887e1 100644 --- a/app/src/event_converter.h +++ b/app/src/event_converter.h @@ -7,32 +7,23 @@ #include "config.h" #include "control_msg.h" -struct complete_mouse_motion_event { - SDL_MouseMotionEvent *mouse_motion_event; - struct size screen_size; -}; +bool +convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to); -struct complete_mouse_wheel_event { - SDL_MouseWheelEvent *mouse_wheel_event; - struct point position; -}; +enum android_metastate +convert_meta_state(SDL_Keymod mod); bool -convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to); +convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, + bool prefer_text); + +enum android_motionevent_buttons +convert_mouse_buttons(uint32_t state); bool -convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, - struct control_msg *to); +convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to); -// the video size may be different from the real device size, so we need the -// size to which the absolute position apply, to scale it accordingly bool -convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, - struct control_msg *to); - -// on Android, a scroll event requires the current mouse position -bool -convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, - struct control_msg *to); +convert_touch_action(SDL_EventType from, enum android_motionevent_action *to); #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index cf2a7519..7d333c1b 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -212,14 +212,17 @@ clipboard_paste(struct controller *controller) { } void -input_manager_process_text_input(struct input_manager *input_manager, +input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { - char c = event->text[0]; - if (isalpha(c) || c == ' ') { - SDL_assert(event->text[1] == '\0'); - // letters and space are handled as raw key event - return; + if (!im->prefer_text) { + char c = event->text[0]; + if (isalpha(c) || c == ' ') { + SDL_assert(event->text[1] == '\0'); + // letters and space are handled as raw key event + return; + } } + struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = SDL_strdup(event->text); @@ -227,14 +230,34 @@ input_manager_process_text_input(struct input_manager *input_manager, LOGW("Could not strdup input text"); return; } - if (!controller_push_msg(input_manager->controller, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { SDL_free(msg.inject_text.text); LOGW("Could not request 'inject text'"); } } +static bool +convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, + bool prefer_text) { + to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; + + if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { + return false; + } + + uint16_t mod = from->keysym.mod; + if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod, + prefer_text)) { + return false; + } + + to->inject_keycode.metastate = convert_meta_state(mod); + + return true; +} + void -input_manager_process_key(struct input_manager *input_manager, +input_manager_process_key(struct input_manager *im, const SDL_KeyboardEvent *event, bool control) { // control: indicates the state of the command-line option --no-control @@ -261,7 +284,7 @@ input_manager_process_key(struct input_manager *input_manager, return; } - struct controller *controller = input_manager->controller; + struct controller *controller = im->controller; // capture all Ctrl events if (ctrl || cmd) { @@ -336,23 +359,23 @@ input_manager_process_key(struct input_manager *input_manager, return; case SDLK_f: if (!shift && cmd && !repeat && down) { - screen_switch_fullscreen(input_manager->screen); + screen_switch_fullscreen(im->screen); } return; case SDLK_x: if (!shift && cmd && !repeat && down) { - screen_resize_to_fit(input_manager->screen); + screen_resize_to_fit(im->screen); } return; case SDLK_g: if (!shift && cmd && !repeat && down) { - screen_resize_to_pixel_perfect(input_manager->screen); + screen_resize_to_pixel_perfect(im->screen); } return; case SDLK_i: if (!shift && cmd && !repeat && down) { struct fps_counter *fps_counter = - input_manager->video_buffer->fps_counter; + im->video_buffer->fps_counter; switch_fps_counter_state(fps_counter); } return; @@ -375,54 +398,129 @@ input_manager_process_key(struct input_manager *input_manager, } struct control_msg msg; - if (convert_input_key(event, &msg)) { + if (convert_input_key(event, &msg, im->prefer_text)) { if (!controller_push_msg(controller, &msg)) { LOGW("Could not request 'inject keycode'"); } } } +static bool +convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; + to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; + to->inject_touch_event.position.screen_size = screen->frame_size; + to->inject_touch_event.position.point.x = from->x; + to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.pressure = 1.f; + to->inject_touch_event.buttons = convert_mouse_buttons(from->state); + + return true; +} + void -input_manager_process_mouse_motion(struct input_manager *input_manager, +input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event) { if (!event->state) { // do not send motion events when no button is pressed return; } + if (event->which == SDL_TOUCH_MOUSEID) { + // simulated from touch events, so it's a duplicate + return; + } struct control_msg msg; - if (convert_mouse_motion(event, input_manager->screen->frame_size, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (convert_mouse_motion(event, im->screen, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse motion event'"); } } } static bool -is_outside_device_screen(struct input_manager *input_manager, int x, int y) -{ - return x < 0 || x >= input_manager->screen->frame_size.width || - y < 0 || y >= input_manager->screen->frame_size.height; +convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + + if (!convert_touch_action(from->type, &to->inject_touch_event.action)) { + return false; + } + + struct size frame_size = screen->frame_size; + + to->inject_touch_event.pointer_id = from->fingerId; + to->inject_touch_event.position.screen_size = frame_size; + // SDL touch event coordinates are normalized in the range [0; 1] + to->inject_touch_event.position.point.x = from->x * frame_size.width; + to->inject_touch_event.position.point.y = from->y * frame_size.height; + to->inject_touch_event.pressure = from->pressure; + to->inject_touch_event.buttons = 0; + return true; } void -input_manager_process_mouse_button(struct input_manager *input_manager, +input_manager_process_touch(struct input_manager *im, + const SDL_TouchFingerEvent *event) { + struct control_msg msg; + if (convert_touch(event, im->screen, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { + LOGW("Could not request 'inject touch event'"); + } + } +} + +static bool +is_outside_device_screen(struct input_manager *im, int x, int y) +{ + return x < 0 || x >= im->screen->frame_size.width || + y < 0 || y >= im->screen->frame_size.height; +} + +static bool +convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + + if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) { + return false; + } + + to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; + to->inject_touch_event.position.screen_size = screen->frame_size; + to->inject_touch_event.position.point.x = from->x; + to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.pressure = 1.f; + to->inject_touch_event.buttons = + convert_mouse_buttons(SDL_BUTTON(from->button)); + + return true; +} + +void +input_manager_process_mouse_button(struct input_manager *im, const SDL_MouseButtonEvent *event, bool control) { + if (event->which == SDL_TOUCH_MOUSEID) { + // simulated from touch events, so it's a duplicate + return; + } if (event->type == SDL_MOUSEBUTTONDOWN) { if (control && event->button == SDL_BUTTON_RIGHT) { - press_back_or_turn_screen_on(input_manager->controller); + press_back_or_turn_screen_on(im->controller); return; } if (control && event->button == SDL_BUTTON_MIDDLE) { - action_home(input_manager->controller, ACTION_DOWN | ACTION_UP); + action_home(im->controller, ACTION_DOWN | ACTION_UP); return; } // double-click on black borders resize to fit the device screen if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { bool outside = - is_outside_device_screen(input_manager, event->x, event->y); + is_outside_device_screen(im, event->x, event->y); if (outside) { - screen_resize_to_fit(input_manager->screen); + screen_resize_to_fit(im->screen); return; } } @@ -434,23 +532,41 @@ input_manager_process_mouse_button(struct input_manager *input_manager, } struct control_msg msg; - if (convert_mouse_button(event, input_manager->screen->frame_size, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (convert_mouse_button(event, im->screen, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse button event'"); } } } -void -input_manager_process_mouse_wheel(struct input_manager *input_manager, - const SDL_MouseWheelEvent *event) { +static bool +convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen, + struct control_msg *to) { struct position position = { - .screen_size = input_manager->screen->frame_size, - .point = get_mouse_point(input_manager->screen), + .screen_size = screen->frame_size, + .point = get_mouse_point(screen), }; + + to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; + + to->inject_scroll_event.position = position; + + int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1; + // SDL behavior seems inconsistent between horizontal and vertical scrolling + // so reverse the horizontal + // + to->inject_scroll_event.hscroll = -mul * from->x; + to->inject_scroll_event.vscroll = mul * from->y; + + return true; +} + +void +input_manager_process_mouse_wheel(struct input_manager *im, + const SDL_MouseWheelEvent *event) { struct control_msg msg; - if (convert_mouse_wheel(event, position, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (convert_mouse_wheel(event, im->screen, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse wheel event'"); } } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 61a0447f..43fc0eeb 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -14,28 +14,33 @@ struct input_manager { struct controller *controller; struct video_buffer *video_buffer; struct screen *screen; + bool prefer_text; }; void -input_manager_process_text_input(struct input_manager *input_manager, +input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event); void -input_manager_process_key(struct input_manager *input_manager, +input_manager_process_key(struct input_manager *im, const SDL_KeyboardEvent *event, bool control); void -input_manager_process_mouse_motion(struct input_manager *input_manager, +input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event); void -input_manager_process_mouse_button(struct input_manager *input_manager, +input_manager_process_touch(struct input_manager *im, + const SDL_TouchFingerEvent *event); + +void +input_manager_process_mouse_button(struct input_manager *im, const SDL_MouseButtonEvent *event, bool control); void -input_manager_process_mouse_wheel(struct input_manager *input_manager, +input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event); #endif diff --git a/app/src/main.c b/app/src/main.c index 41383ed9..8a835bf1 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -14,24 +14,9 @@ #include "recorder.h" struct args { - const char *serial; - const char *crop; - const char *record_filename; - const char *window_title; - const char *push_target; - enum recorder_format record_format; - bool fullscreen; - bool no_control; - bool no_display; + struct scrcpy_options opts; bool help; bool version; - bool show_touches; - uint16_t port; - uint16_t max_size; - uint32_t bit_rate; - bool always_on_top; - bool turn_screen_off; - bool render_expired_frames; }; static void usage(const char *arg0) { @@ -45,12 +30,15 @@ static void usage(const char *arg0) { "\n" "Options:\n" "\n" + " --always-on-top\n" + " Make scrcpy window always on top (above other windows).\n" + "\n" " -b, --bit-rate value\n" " Encode the video at the given bit-rate, expressed in bits/s.\n" " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" " Default is %d.\n" "\n" - " -c, --crop width:height:x:y\n" + " --crop width:height:x:y\n" " Crop the device screen on the server.\n" " The values are expressed in the device natural orientation\n" " (typically, portrait for a phone, landscape for a tablet).\n" @@ -59,12 +47,13 @@ static void usage(const char *arg0) { " -f, --fullscreen\n" " Start in fullscreen.\n" "\n" - " -F, --record-format format\n" - " Force recording format (either mp4 or mkv).\n" - "\n" " -h, --help\n" " Print this help.\n" "\n" + " --max-fps value\n" + " Limit the frame rate of screen capture (only supported on\n" + " devices with Android >= 10).\n" + "\n" " -m, --max-size value\n" " Limit both the width and height of the video to value. The\n" " other dimension is computed so that the device aspect-ratio\n" @@ -82,6 +71,13 @@ static void usage(const char *arg0) { " Set the TCP port the client listens on.\n" " Default is %d.\n" "\n" + " --prefer-text\n" + " Inject alpha characters and space as text events instead of\n" + " key events.\n" + " This avoids issues when combining multiple keys to enter a\n" + " special character, but breaks the expected behavior of alpha\n" + " keys in games (typically WASD).\n" + "\n" " --push-target path\n" " Set the target directory for pushing files to the device by\n" " drag & drop. It is passed as-is to \"adb push\".\n" @@ -89,9 +85,12 @@ static void usage(const char *arg0) { "\n" " -r, --record file.mp4\n" " Record screen to file.\n" - " The format is determined by the -F/--record-format option if\n" + " The format is determined by the --record-format option if\n" " set, or by the file extension (.mp4 or .mkv).\n" "\n" + " --record-format format\n" + " Force recording format (either mp4 or mkv).\n" + "\n" " --render-expired-frames\n" " By default, to minimize latency, scrcpy always renders the\n" " last available decoded frame, and drops any previous ones.\n" @@ -109,15 +108,31 @@ static void usage(const char *arg0) { " Enable \"show touches\" on start, disable on quit.\n" " It only shows physical touches (not clicks from scrcpy).\n" "\n" - " -T, --always-on-top\n" - " Make scrcpy window always on top (above other windows).\n" - "\n" " -v, --version\n" " Print the version of scrcpy.\n" "\n" + " --window-borderless\n" + " Disable window decorations (display borderless window).\n" + "\n" " --window-title text\n" " Set a custom window title.\n" "\n" + " --window-x value\n" + " Set the initial window horizontal position.\n" + " Default is -1 (automatic).\n" + "\n" + " --window-y value\n" + " Set the initial window vertical position.\n" + " Default is -1 (automatic).\n" + "\n" + " --window-width value\n" + " Set the initial window width.\n" + " Default is 0 (automatic).\n" + "\n" + " --window-height value\n" + " Set the initial window width.\n" + " Default is 0 (automatic).\n" + "\n" "Shortcuts:\n" "\n" " " CTRL_OR_CMD "+f\n" @@ -258,11 +273,75 @@ parse_max_size(char *optarg, uint16_t *max_size) { return true; } +static bool +parse_max_fps(const char *optarg, uint16_t *max_fps) { + char *endptr; + if (*optarg == '\0') { + LOGE("Max FPS parameter is empty"); + return false; + } + long value = strtol(optarg, &endptr, 0); + if (*endptr != '\0') { + LOGE("Invalid max FPS: %s", optarg); + return false; + } + if (value & ~0xffff) { + // in practice, it should not be higher than 60 + LOGE("Max FPS value is invalid: %ld", value); + return false; + } + + *max_fps = (uint16_t) value; + return true; +} + +static bool +parse_window_position(char *optarg, int16_t *position) { + char *endptr; + if (*optarg == '\0') { + LOGE("Window position parameter is empty"); + return false; + } + long value = strtol(optarg, &endptr, 0); + if (*endptr != '\0') { + LOGE("Invalid window position: %s", optarg); + return false; + } + if (value < -1 || value > 0x7fff) { + LOGE("Window position must be between -1 and 32767: %ld", value); + return false; + } + + *position = (int16_t) value; + return true; +} + +static bool +parse_window_dimension(char *optarg, uint16_t *dimension) { + char *endptr; + if (*optarg == '\0') { + LOGE("Window dimension parameter is empty"); + return false; + } + long value = strtol(optarg, &endptr, 0); + if (*endptr != '\0') { + LOGE("Invalid window dimension: %s", optarg); + return false; + } + if (value & ~0xffff) { + LOGE("Window position must be between 0 and 65535: %ld", value); + return false; + } + + *dimension = (uint16_t) value; + return true; +} + static bool parse_port(char *optarg, uint16_t *port) { char *endptr; if (*optarg == '\0') { - LOGE("Invalid port parameter is empty"); + LOGE("Port parameter is empty"); return false; } long value = strtol(optarg, &endptr, 0); @@ -312,98 +391,157 @@ guess_record_format(const char *filename) { #define OPT_RENDER_EXPIRED_FRAMES 1000 #define OPT_WINDOW_TITLE 1001 #define OPT_PUSH_TARGET 1002 +#define OPT_ALWAYS_ON_TOP 1003 +#define OPT_CROP 1004 +#define OPT_RECORD_FORMAT 1005 +#define OPT_PREFER_TEXT 1006 +#define OPT_WINDOW_X 1007 +#define OPT_WINDOW_Y 1008 +#define OPT_WINDOW_WIDTH 1009 +#define OPT_WINDOW_HEIGHT 1010 +#define OPT_WINDOW_BORDERLESS 1011 +#define OPT_MAX_FPS 1012 static bool parse_args(struct args *args, int argc, char *argv[]) { static const struct option long_options[] = { - {"always-on-top", no_argument, NULL, 'T'}, + {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, {"bit-rate", required_argument, NULL, 'b'}, - {"crop", required_argument, NULL, 'c'}, + {"crop", required_argument, NULL, OPT_CROP}, {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, + {"max-fps", required_argument, NULL, OPT_MAX_FPS}, {"max-size", required_argument, NULL, 'm'}, {"no-control", no_argument, NULL, 'n'}, {"no-display", no_argument, NULL, 'N'}, {"port", required_argument, NULL, 'p'}, - {"push-target", required_argument, NULL, - OPT_PUSH_TARGET}, + {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"record", required_argument, NULL, 'r'}, - {"record-format", required_argument, NULL, 'F'}, + {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, {"render-expired-frames", no_argument, NULL, - OPT_RENDER_EXPIRED_FRAMES}, + OPT_RENDER_EXPIRED_FRAMES}, {"serial", required_argument, NULL, 's'}, {"show-touches", no_argument, NULL, 't'}, {"turn-screen-off", no_argument, NULL, 'S'}, + {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, {"version", no_argument, NULL, 'v'}, - {"window-title", required_argument, NULL, - OPT_WINDOW_TITLE}, + {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, + {"window-x", required_argument, NULL, OPT_WINDOW_X}, + {"window-y", required_argument, NULL, OPT_WINDOW_Y}, + {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, + {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, + {"window-borderless", no_argument, NULL, + OPT_WINDOW_BORDERLESS}, {NULL, 0, NULL, 0 }, }; + + struct scrcpy_options *opts = &args->opts; + int c; while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, NULL)) != -1) { switch (c) { case 'b': - if (!parse_bit_rate(optarg, &args->bit_rate)) { + if (!parse_bit_rate(optarg, &opts->bit_rate)) { return false; } break; case 'c': - args->crop = optarg; + LOGW("Deprecated option -c. Use --crop instead."); + // fall through + case OPT_CROP: + opts->crop = optarg; break; case 'f': - args->fullscreen = true; + opts->fullscreen = true; break; case 'F': - if (!parse_record_format(optarg, &args->record_format)) { + LOGW("Deprecated option -F. Use --record-format instead."); + // fall through + case OPT_RECORD_FORMAT: + if (!parse_record_format(optarg, &opts->record_format)) { return false; } break; case 'h': args->help = true; break; + case OPT_MAX_FPS: + if (!parse_max_fps(optarg, &opts->max_fps)) { + return false; + } + break; case 'm': - if (!parse_max_size(optarg, &args->max_size)) { + if (!parse_max_size(optarg, &opts->max_size)) { return false; } break; case 'n': - args->no_control = true; + opts->control = false; break; case 'N': - args->no_display = true; + opts->display = false; break; case 'p': - if (!parse_port(optarg, &args->port)) { + if (!parse_port(optarg, &opts->port)) { return false; } break; case 'r': - args->record_filename = optarg; + opts->record_filename = optarg; break; case 's': - args->serial = optarg; + opts->serial = optarg; break; case 'S': - args->turn_screen_off = true; + opts->turn_screen_off = true; break; case 't': - args->show_touches = true; + opts->show_touches = true; break; case 'T': - args->always_on_top = true; + LOGW("Deprecated option -T. Use --always-on-top instead."); + // fall through + case OPT_ALWAYS_ON_TOP: + opts->always_on_top = true; break; case 'v': args->version = true; break; case OPT_RENDER_EXPIRED_FRAMES: - args->render_expired_frames = true; + opts->render_expired_frames = true; break; case OPT_WINDOW_TITLE: - args->window_title = optarg; + opts->window_title = optarg; + break; + case OPT_WINDOW_X: + if (!parse_window_position(optarg, &opts->window_x)) { + return false; + } + break; + case OPT_WINDOW_Y: + if (!parse_window_position(optarg, &opts->window_y)) { + return false; + } + break; + case OPT_WINDOW_WIDTH: + if (!parse_window_dimension(optarg, &opts->window_width)) { + return false; + } + break; + case OPT_WINDOW_HEIGHT: + if (!parse_window_dimension(optarg, &opts->window_height)) { + return false; + } + break; + case OPT_WINDOW_BORDERLESS: + opts->window_borderless = true; break; case OPT_PUSH_TARGET: - args->push_target = optarg; + opts->push_target = optarg; + break; + case OPT_PREFER_TEXT: + opts->prefer_text = true; break; default: // getopt prints the error message on stderr @@ -411,12 +549,12 @@ parse_args(struct args *args, int argc, char *argv[]) { } } - if (args->no_display && !args->record_filename) { + if (!opts->display && !opts->record_filename) { LOGE("-N/--no-display requires screen recording (-r/--record)"); return false; } - if (args->no_display && args->fullscreen) { + if (!opts->display && opts->fullscreen) { LOGE("-f/--fullscreen-window is incompatible with -N/--no-display"); return false; } @@ -427,21 +565,21 @@ parse_args(struct args *args, int argc, char *argv[]) { return false; } - if (args->record_format && !args->record_filename) { + if (opts->record_format && !opts->record_filename) { LOGE("Record format specified without recording"); return false; } - if (args->record_filename && !args->record_format) { - args->record_format = guess_record_format(args->record_filename); - if (!args->record_format) { + if (opts->record_filename && !opts->record_format) { + opts->record_format = guess_record_format(opts->record_filename); + if (!opts->record_format) { LOGE("No format specified for \"%s\" (try with -F mkv)", - args->record_filename); + opts->record_filename); return false; } } - if (args->no_control && args->turn_screen_off) { + if (!opts->control && opts->turn_screen_off) { LOGE("Could not request to turn screen off if control is disabled"); return false; } @@ -458,24 +596,11 @@ main(int argc, char *argv[]) { setbuf(stderr, NULL); #endif struct args args = { - .serial = NULL, - .crop = NULL, - .record_filename = NULL, - .window_title = NULL, - .push_target = NULL, - .record_format = 0, + .opts = SCRCPY_OPTIONS_DEFAULT, .help = false, .version = false, - .show_touches = false, - .port = DEFAULT_LOCAL_PORT, - .max_size = DEFAULT_MAX_SIZE, - .bit_rate = DEFAULT_BIT_RATE, - .always_on_top = false, - .no_control = false, - .no_display = false, - .turn_screen_off = false, - .render_expired_frames = false, }; + if (!parse_args(&args, argc, argv)) { return 1; } @@ -504,25 +629,7 @@ main(int argc, char *argv[]) { SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); #endif - struct scrcpy_options options = { - .serial = args.serial, - .crop = args.crop, - .port = args.port, - .record_filename = args.record_filename, - .window_title = args.window_title, - .push_target = args.push_target, - .record_format = args.record_format, - .max_size = args.max_size, - .bit_rate = args.bit_rate, - .show_touches = args.show_touches, - .fullscreen = args.fullscreen, - .always_on_top = args.always_on_top, - .control = !args.no_control, - .display = !args.no_display, - .turn_screen_off = args.turn_screen_off, - .render_expired_frames = args.render_expired_frames, - }; - int res = scrcpy(&options) ? 0 : 1; + int res = scrcpy(&args.opts) ? 0 : 1; avformat_network_deinit(); // ignore failure diff --git a/app/src/recorder.c b/app/src/recorder.c index 77186350..f6f6fd96 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -135,6 +135,9 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { // recorder->ctx->oformat = (AVOutputFormat *) format; + av_dict_set(&recorder->ctx->metadata, "comment", + "Recorded by scrcpy " SCRCPY_VERSION, 0); + AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); if (!ostream) { avformat_free_context(recorder->ctx); @@ -171,9 +174,14 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { void recorder_close(struct recorder *recorder) { - int ret = av_write_trailer(recorder->ctx); - if (ret < 0) { - LOGE("Failed to write trailer to %s", recorder->filename); + if (recorder->header_written) { + int ret = av_write_trailer(recorder->ctx); + if (ret < 0) { + LOGE("Failed to write trailer to %s", recorder->filename); + recorder->failed = true; + } + } else { + // the recorded file is empty recorder->failed = true; } avio_close(recorder->ctx->pb); @@ -293,8 +301,12 @@ run_recorder(void *data) { continue; } - // we now know the duration of the previous packet - previous->packet.duration = rec->packet.pts - previous->packet.pts; + // config packets have no PTS, we must ignore them + if (rec->packet.pts != AV_NOPTS_VALUE + && previous->packet.pts != AV_NOPTS_VALUE) { + // we now know the duration of the previous packet + previous->packet.duration = rec->packet.pts - previous->packet.pts; + } bool ok = recorder_write(recorder, &previous->packet); record_packet_delete(previous); diff --git a/app/src/recorder.h b/app/src/recorder.h index b1953fcb..4ad77197 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -11,7 +11,8 @@ #include "queue.h" enum recorder_format { - RECORDER_FORMAT_MP4 = 1, + RECORDER_FORMAT_AUTO, + RECORDER_FORMAT_MP4, RECORDER_FORMAT_MKV, }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index defcb751..67f1de16 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -42,6 +42,7 @@ static struct input_manager input_manager = { .controller = &controller, .video_buffer = &video_buffer, .screen = &screen, + .prefer_text = false, // initialized later }; // init SDL and set appropriate hints @@ -143,12 +144,7 @@ handle_event(SDL_Event *event, bool control) { } break; case SDL_WINDOWEVENT: - switch (event->window.event) { - case SDL_WINDOWEVENT_EXPOSED: - case SDL_WINDOWEVENT_SIZE_CHANGED: - screen_render(&screen); - break; - } + screen_handle_window_event(&screen, &event->window); break; case SDL_TEXTINPUT: if (!control) { @@ -181,6 +177,11 @@ handle_event(SDL_Event *event, bool control) { input_manager_process_mouse_button(&input_manager, &event->button, control); break; + case SDL_FINGERMOTION: + case SDL_FINGERDOWN: + case SDL_FINGERUP: + input_manager_process_touch(&input_manager, &event->tfinger); + break; case SDL_DROPFILE: { if (!control) { break; @@ -212,6 +213,7 @@ event_loop(bool display, bool control) { case EVENT_RESULT_STOPPED_BY_USER: return true; case EVENT_RESULT_STOPPED_BY_EOS: + LOGW("Device disconnected"); return false; case EVENT_RESULT_CONTINUE: break; @@ -278,6 +280,7 @@ scrcpy(const struct scrcpy_options *options) { .local_port = options->port, .max_size = options->max_size, .bit_rate = options->bit_rate, + .max_fps = options->max_fps, .control = options->control, }; if (!server_start(&server, options->serial, ¶ms)) { @@ -385,7 +388,10 @@ scrcpy(const struct scrcpy_options *options) { options->window_title ? options->window_title : device_name; if (!screen_init_rendering(&screen, window_title, frame_size, - options->always_on_top)) { + options->always_on_top, options->window_x, + options->window_y, options->window_width, + options->window_height, + options->window_borderless)) { goto end; } @@ -409,6 +415,8 @@ scrcpy(const struct scrcpy_options *options) { show_touches_waited = true; } + input_manager.prefer_text = options->prefer_text; + ret = event_loop(options->display, options->control); LOGD("quit..."); diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 1593fb1e..8723f29f 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -3,9 +3,10 @@ #include #include -#include #include "config.h" +#include "input_manager.h" +#include "recorder.h" struct scrcpy_options { const char *serial; @@ -17,6 +18,11 @@ struct scrcpy_options { uint16_t port; uint16_t max_size; uint32_t bit_rate; + uint16_t max_fps; + int16_t window_x; + int16_t window_y; + uint16_t window_width; + uint16_t window_height; bool show_touches; bool fullscreen; bool always_on_top; @@ -24,8 +30,36 @@ struct scrcpy_options { bool display; bool turn_screen_off; bool render_expired_frames; + bool prefer_text; + bool window_borderless; }; +#define SCRCPY_OPTIONS_DEFAULT { \ + .serial = NULL, \ + .crop = NULL, \ + .record_filename = NULL, \ + .window_title = NULL, \ + .push_target = NULL, \ + .record_format = RECORDER_FORMAT_AUTO, \ + .port = DEFAULT_LOCAL_PORT, \ + .max_size = DEFAULT_LOCAL_PORT, \ + .bit_rate = DEFAULT_BIT_RATE, \ + .max_fps = 0, \ + .window_x = -1, \ + .window_y = -1, \ + .window_width = 0, \ + .window_height = 0, \ + .show_touches = false, \ + .fullscreen = false, \ + .always_on_top = false, \ + .control = true, \ + .display = true, \ + .turn_screen_off = false, \ + .render_expired_frames = false, \ + .prefer_text = false, \ + .window_borderless = false, \ +} + bool scrcpy(const struct scrcpy_options *options); diff --git a/app/src/screen.c b/app/src/screen.c index e34bcf46..ab4d434e 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -16,7 +16,7 @@ // get the window size in a struct size static struct size -get_native_window_size(SDL_Window *window) { +get_window_size(SDL_Window *window) { int width; int height; SDL_GetWindowSize(window, &width, &height); @@ -29,11 +29,20 @@ get_native_window_size(SDL_Window *window) { // get the windowed window size static struct size -get_window_size(const struct screen *screen) { - if (screen->fullscreen) { +get_windowed_window_size(const struct screen *screen) { + if (screen->fullscreen || screen->maximized) { return screen->windowed_window_size; } - return get_native_window_size(screen->window); + return get_window_size(screen->window); +} + +// apply the windowed window size if fullscreen and maximized are disabled +static void +apply_windowed_size(struct screen *screen) { + if (!screen->fullscreen && !screen->maximized) { + SDL_SetWindowSize(screen->window, screen->windowed_window_size.width, + screen->windowed_window_size.height); + } } // set the window size to be applied when fullscreen is disabled @@ -41,12 +50,8 @@ static void set_window_size(struct screen *screen, struct size new_size) { // setting the window size during fullscreen is implementation defined, // so apply the resize only after fullscreen is disabled - if (screen->fullscreen) { - // SDL_SetWindowSize will be called when fullscreen will be disabled - screen->windowed_window_size = new_size; - } else { - SDL_SetWindowSize(screen->window, new_size.width, new_size.height); - } + screen->windowed_window_size = new_size; + apply_windowed_size(screen); } // get the preferred display bounds (i.e. the screen bounds with some margins) @@ -112,14 +117,35 @@ get_optimal_size(struct size current_size, struct size frame_size) { // same as get_optimal_size(), but read the current size from the window static inline struct size get_optimal_window_size(const struct screen *screen, struct size frame_size) { - struct size current_size = get_window_size(screen); - return get_optimal_size(current_size, frame_size); + struct size windowed_size = get_windowed_window_size(screen); + return get_optimal_size(windowed_size, frame_size); } // initially, there is no current size, so use the frame size as current size +// req_width and req_height, if not 0, are the sizes requested by the user static inline struct size -get_initial_optimal_size(struct size frame_size) { - return get_optimal_size(frame_size, frame_size); +get_initial_optimal_size(struct size frame_size, uint16_t req_width, + uint16_t req_height) { + struct size window_size; + if (!req_width && !req_height) { + window_size = get_optimal_size(frame_size, frame_size); + } else { + if (req_width) { + window_size.width = req_width; + } else { + // compute from the requested height + window_size.width = (uint32_t) req_height * frame_size.width + / frame_size.height; + } + if (req_height) { + window_size.height = req_height; + } else { + // compute from the requested width + window_size.height = (uint32_t) req_width * frame_size.height + / frame_size.width; + } + } + return window_size; } void @@ -136,10 +162,13 @@ create_texture(SDL_Renderer *renderer, struct size frame_size) { bool screen_init_rendering(struct screen *screen, const char *window_title, - struct size frame_size, bool always_on_top) { + struct size frame_size, bool always_on_top, + int16_t window_x, int16_t window_y, uint16_t window_width, + uint16_t window_height, bool window_borderless) { screen->frame_size = frame_size; - struct size window_size = get_initial_optimal_size(frame_size); + struct size window_size = + get_initial_optimal_size(frame_size, window_width, window_height); uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; #ifdef HIDPI_SUPPORT window_flags |= SDL_WINDOW_ALLOW_HIGHDPI; @@ -152,9 +181,13 @@ screen_init_rendering(struct screen *screen, const char *window_title, "(compile with SDL >= 2.0.5 to enable it)"); #endif } + if (window_borderless) { + window_flags |= SDL_WINDOW_BORDERLESS; + } - screen->window = SDL_CreateWindow(window_title, SDL_WINDOWPOS_UNDEFINED, - SDL_WINDOWPOS_UNDEFINED, + int x = window_x != -1 ? window_x : SDL_WINDOWPOS_UNDEFINED; + int y = window_y != -1 ? window_y : SDL_WINDOWPOS_UNDEFINED; + screen->window = SDL_CreateWindow(window_title, x, y, window_size.width, window_size.height, window_flags); if (!screen->window) { @@ -194,6 +227,8 @@ screen_init_rendering(struct screen *screen, const char *window_title, return false; } + screen->windowed_window_size = window_size; + return true; } @@ -229,11 +264,11 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) { // frame dimension changed, destroy texture SDL_DestroyTexture(screen->texture); - struct size current_size = get_window_size(screen); + struct size windowed_size = get_windowed_window_size(screen); struct size target_size = { - (uint32_t) current_size.width * new_frame_size.width + (uint32_t) windowed_size.width * new_frame_size.width / screen->frame_size.width, - (uint32_t) current_size.height * new_frame_size.height + (uint32_t) windowed_size.height * new_frame_size.height / screen->frame_size.height, }; target_size = get_optimal_size(target_size, new_frame_size); @@ -287,10 +322,6 @@ screen_render(struct screen *screen) { void screen_switch_fullscreen(struct screen *screen) { - if (!screen->fullscreen) { - // going to fullscreen, store the current windowed window size - screen->windowed_window_size = get_native_window_size(screen->window); - } uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; if (SDL_SetWindowFullscreen(screen->window, new_mode)) { LOGW("Could not switch fullscreen mode: %s", SDL_GetError()); @@ -298,11 +329,7 @@ screen_switch_fullscreen(struct screen *screen) { } screen->fullscreen = !screen->fullscreen; - if (!screen->fullscreen) { - // fullscreen disabled, restore expected windowed window size - SDL_SetWindowSize(screen->window, screen->windowed_window_size.width, - screen->windowed_window_size.height); - } + apply_windowed_size(screen); LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); screen_render(screen); @@ -310,20 +337,75 @@ screen_switch_fullscreen(struct screen *screen) { void screen_resize_to_fit(struct screen *screen) { - if (!screen->fullscreen) { - struct size optimal_size = get_optimal_window_size(screen, - screen->frame_size); - SDL_SetWindowSize(screen->window, optimal_size.width, - optimal_size.height); - LOGD("Resized to optimal size"); + if (screen->fullscreen) { + return; } + + if (screen->maximized) { + SDL_RestoreWindow(screen->window); + screen->maximized = false; + } + + struct size optimal_size = + get_optimal_window_size(screen, screen->frame_size); + SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); + LOGD("Resized to optimal size"); } void screen_resize_to_pixel_perfect(struct screen *screen) { - if (!screen->fullscreen) { - SDL_SetWindowSize(screen->window, screen->frame_size.width, - screen->frame_size.height); - LOGD("Resized to pixel-perfect"); + if (screen->fullscreen) { + return; + } + + if (screen->maximized) { + SDL_RestoreWindow(screen->window); + screen->maximized = false; + } + + SDL_SetWindowSize(screen->window, screen->frame_size.width, + screen->frame_size.height); + LOGD("Resized to pixel-perfect"); +} + +void +screen_handle_window_event(struct screen *screen, + const SDL_WindowEvent *event) { + switch (event->event) { + case SDL_WINDOWEVENT_EXPOSED: + screen_render(screen); + break; + case SDL_WINDOWEVENT_SIZE_CHANGED: + if (!screen->fullscreen && !screen->maximized) { + // Backup the previous size: if we receive the MAXIMIZED event, + // then the new size must be ignored (it's the maximized size). + // We could not rely on the window flags due to race conditions + // (they could be updated asynchronously, at least on X11). + screen->windowed_window_size_backup = + screen->windowed_window_size; + + // Save the windowed size, so that it is available once the + // window is maximized or fullscreen is enabled. + screen->windowed_window_size = get_window_size(screen->window); + } + screen_render(screen); + break; + case SDL_WINDOWEVENT_MAXIMIZED: + // The backup size must be non-nul. + SDL_assert(screen->windowed_window_size_backup.width); + SDL_assert(screen->windowed_window_size_backup.height); + // Revert the last size, it was updated while screen was maximized. + screen->windowed_window_size = screen->windowed_window_size_backup; +#ifdef DEBUG + // Reset the backup to invalid values to detect unexpected usage + screen->windowed_window_size_backup.width = 0; + screen->windowed_window_size_backup.height = 0; +#endif + screen->maximized = true; + break; + case SDL_WINDOWEVENT_RESTORED: + screen->maximized = false; + apply_windowed_size(screen); + break; } } diff --git a/app/src/screen.h b/app/src/screen.h index bc189189..2346ff15 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -15,28 +15,37 @@ struct screen { SDL_Renderer *renderer; SDL_Texture *texture; struct size frame_size; - //used only in fullscreen mode to know the windowed window size + // The window size the last time it was not maximized or fullscreen. struct size windowed_window_size; + // Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be + // able to revert the size to its non-maximized value. + struct size windowed_window_size_backup; bool has_frame; bool fullscreen; + bool maximized; bool no_window; }; -#define SCREEN_INITIALIZER { \ - .window = NULL, \ - .renderer = NULL, \ - .texture = NULL, \ - .frame_size = { \ - .width = 0, \ - .height = 0, \ - }, \ +#define SCREEN_INITIALIZER { \ + .window = NULL, \ + .renderer = NULL, \ + .texture = NULL, \ + .frame_size = { \ + .width = 0, \ + .height = 0, \ + }, \ .windowed_window_size = { \ - .width = 0, \ - .height = 0, \ - }, \ - .has_frame = false, \ - .fullscreen = false, \ - .no_window = false, \ + .width = 0, \ + .height = 0, \ + }, \ + .windowed_window_size_backup = { \ + .width = 0, \ + .height = 0, \ + }, \ + .has_frame = false, \ + .fullscreen = false, \ + .maximized = false, \ + .no_window = false, \ } // initialize default values @@ -46,7 +55,9 @@ screen_init(struct screen *screen); // initialize screen, create window, renderer and texture (window is hidden) bool screen_init_rendering(struct screen *screen, const char *window_title, - struct size frame_size, bool always_on_top); + struct size frame_size, bool always_on_top, + int16_t window_x, int16_t window_y, uint16_t window_width, + uint16_t window_height, bool window_borderless); // show the window void @@ -76,4 +87,8 @@ screen_resize_to_fit(struct screen *screen); void screen_resize_to_pixel_perfect(struct screen *screen); +// react to window events +void +screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event); + #endif diff --git a/app/src/server.c b/app/src/server.c index 85b1b6b8..b37b39d0 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -13,7 +13,7 @@ #include "net.h" #define SOCKET_NAME "scrcpy" -#define SERVER_FILENAME "scrcpy-server.jar" +#define SERVER_FILENAME "scrcpy-server" #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME #define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME @@ -32,7 +32,7 @@ get_server_path(void) { // the absolute path is hardcoded return DEFAULT_SERVER_PATH; #else - // use scrcpy-server.jar in the same directory as the executable + // use scrcpy-server in the same directory as the executable char *executable_path = get_executable_path(); if (!executable_path) { LOGE("Could not get executable path, " @@ -118,21 +118,41 @@ static process_t execute_server(struct server *server, const struct server_params *params) { char max_size_string[6]; char bit_rate_string[11]; + char max_fps_string[6]; sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); + sprintf(max_fps_string, "%"PRIu16, params->max_fps); const char *const cmd[] = { "shell", "CLASSPATH=/data/local/tmp/" SERVER_FILENAME, "app_process", +#ifdef SERVER_DEBUGGER +# define SERVER_DEBUGGER_PORT "5005" + "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" + SERVER_DEBUGGER_PORT, +#endif "/", // unused "com.genymobile.scrcpy.Server", + SCRCPY_VERSION, max_size_string, bit_rate_string, + max_fps_string, server->tunnel_forward ? "true" : "false", params->crop ? params->crop : "-", "true", // always send frame meta (packet boundaries + timestamp) params->control ? "true" : "false", }; +#ifdef SERVER_DEBUGGER + LOGI("Server debugger waiting for a client on device port " + SERVER_DEBUGGER_PORT "..."); + // From the computer, run + // adb forward tcp:5005 tcp:5005 + // Then, from Android Studio: Run > Debug > Edit configurations... + // On the left, click on '+', "Remote", with: + // Host: localhost + // Port: 5005 + // Then click on "Debug" +#endif return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); } @@ -261,7 +281,7 @@ server_connect_to(struct server *server) { server->control_socket = net_accept(server->server_socket); if (server->control_socket == INVALID_SOCKET) { - // the video_socket will be clean up on destroy + // the video_socket will be cleaned up on destroy return false; } diff --git a/app/src/server.h b/app/src/server.h index 2140d8ab..f46ced19 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -35,6 +35,7 @@ struct server_params { uint16_t local_port; uint16_t max_size; uint32_t bit_rate; + uint16_t max_fps; bool control; }; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index c0c501f2..83ab011f 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -67,35 +67,39 @@ static void test_serialize_inject_text_long(void) { assert(!memcmp(buf, expected, sizeof(expected))); } -static void test_serialize_inject_mouse_event(void) { +static void test_serialize_inject_touch_event(void) { struct control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, - .inject_mouse_event = { + .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + .inject_touch_event = { .action = AMOTION_EVENT_ACTION_DOWN, - .buttons = AMOTION_EVENT_BUTTON_PRIMARY, + .pointer_id = 0x1234567887654321L, .position = { .point = { - .x = 260, - .y = 1026, + .x = 100, + .y = 200, }, .screen_size = { .width = 1080, .height = 1920, }, }, + .pressure = 1.0f, + .buttons = AMOTION_EVENT_BUTTON_PRIMARY, }, }; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 18); + assert(size == 28); const unsigned char expected[] = { - CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, + CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, 0x00, // AKEY_EVENT_ACTION_DOWN - 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY - 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 + 0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21, // pointer id + 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200 0x04, 0x38, 0x07, 0x80, // 1080 1920 + 0xff, 0xff, // pressure + 0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY }; assert(!memcmp(buf, expected, sizeof(expected))); } @@ -236,7 +240,7 @@ int main(void) { test_serialize_inject_keycode(); test_serialize_inject_text(); test_serialize_inject_text_long(); - test_serialize_inject_mouse_event(); + test_serialize_inject_touch_event(); test_serialize_inject_scroll_event(); test_serialize_back_or_screen_on(); test_serialize_expand_notification_panel(); diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 63ee315a..798814d9 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -54,7 +54,7 @@ page at http://checkstyle.sourceforge.net/config.html --> - + @@ -99,7 +99,7 @@ page at http://checkstyle.sourceforge.net/config.html --> - + diff --git a/cross_win32.txt b/cross_win32.txt index 3ad45c79..d13af0e2 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -15,6 +15,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win32-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win32-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win32-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win32-dev' prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index 3f222ba5..09f387e1 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -15,6 +15,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win64-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win64-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win64-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win64-dev' prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32' diff --git a/meson.build b/meson.build index 57b66db6..ba19d7ee 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.10', + version: '1.11', meson_version: '>= 0.37', default_options: 'c_std=c11') diff --git a/meson_options.txt b/meson_options.txt index d93161e3..4cf4a8bf 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,5 +3,6 @@ option('compile_server', type: 'boolean', value: true, description: 'Build the s option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') -option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable') +option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') +option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 6cfb4100..892af6c7 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -10,24 +10,24 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb prepare-ffmpeg-shared-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1.4-win32-shared.zip \ - 596608277f6b937c3dea7c46e854665d75b3de56790bae07f655ca331440f003 \ - ffmpeg-4.1.4-win32-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.1-win32-shared.zip \ + 9208255f409410d95147151d7e829b5699bf8d91bfe1e81c3f470f47c2fa66d2 \ + ffmpeg-4.2.1-win32-shared prepare-ffmpeg-dev-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.4-win32-dev.zip \ - a80c86e263cfad26e202edfa5e6e939a2c88843ae26f031d3e0d981a39fd03fb \ - ffmpeg-4.1.4-win32-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.1-win32-dev.zip \ + c3469e6c5f031cbcc8cba88dee92d6548c5c6b6ff14f4097f18f72a92d0d70c4 \ + ffmpeg-4.2.1-win32-dev prepare-ffmpeg-shared-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.4-win64-shared.zip \ - a90889871de2cab8a79b392591313a188189a353f69dde1db98aebe20b280989 \ - ffmpeg-4.1.4-win64-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.1-win64-shared.zip \ + 55063d3cf750a75485c7bf196031773d81a1b25d0980c7db48ecfc7701a42331 \ + ffmpeg-4.2.1-win64-shared prepare-ffmpeg-dev-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.4-win64-dev.zip \ - 6c9d53f9e94ce1821e975ec668e5b9d6e9deb4a45d0d7e30264685d3dfbbb068 \ - ffmpeg-4.1.4-win64-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.1-win64-dev.zip \ + 5af393be5f25c0a71aa29efce768e477c35347f7f8e0d9696767d5b9d405b74e \ + ffmpeg-4.2.1-win64-dev prepare-sdl2: @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \ @@ -35,6 +35,6 @@ prepare-sdl2: SDL2-2.0.10 prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.2-windows.zip \ - d78f02e5e2c9c4c1d046dcd4e6fbdf586e5f57ef66eb0da5c2b49d745d85d5ee \ + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \ + 2df06160056ec9a84c7334af2a1e42740befbb1a2e34370e7af544a2cc78152c \ platform-tools diff --git a/release.sh b/release.sh index fbd1eb54..4c5afbf1 100755 --- a/release.sh +++ b/release.sh @@ -23,21 +23,21 @@ cd - make -f Makefile.CrossWindows # the generated server must be the same everywhere -cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win32/scrcpy-server.jar -cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win64/scrcpy-server.jar +cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win32/scrcpy-server +cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win64/scrcpy-server # get version name TAG=$(git describe --tags --always) # create release directory mkdir -p "release-$TAG" -cp "$BUILDDIR/server/scrcpy-server.jar" "release-$TAG/scrcpy-server-$TAG.jar" +cp "$BUILDDIR/server/scrcpy-server" "release-$TAG/scrcpy-server-$TAG" cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/" cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/" # generate checksums cd "release-$TAG" -sha256sum "scrcpy-server-$TAG.jar" \ +sha256sum "scrcpy-server-$TAG" \ "scrcpy-win32-$TAG.zip" \ "scrcpy-win64-$TAG.zip" > SHA256SUMS.txt diff --git a/run b/run index 7abeca05..bfb499ae 100755 --- a/run +++ b/run @@ -20,4 +20,4 @@ then exit 1 fi -SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server.jar" "$BUILDDIR/app/scrcpy" "$@" +SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" "$BUILDDIR/app/scrcpy" "$@" diff --git a/scripts/run-scrcpy.sh b/scripts/run-scrcpy.sh index fa6d7c8f..f3130ee9 100755 --- a/scripts/run-scrcpy.sh +++ b/scripts/run-scrcpy.sh @@ -1,2 +1,2 @@ #!/bin/bash -SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server.jar" "$MESON_BUILD_ROOT/app/scrcpy" +SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy" diff --git a/server/build.gradle b/server/build.gradle index f1b48a28..0804a8bd 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 11 - versionName "1.10" + versionCode 12 + versionName "1.11" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 5f2eff22..fcd6233e 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -11,13 +11,16 @@ set -e +SCRCPY_DEBUG=false +SCRCPY_VERSION_NAME=1.11 + PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" CLASSES_DIR="$BUILD_DIR/classes" SERVER_DIR=$(dirname "$0") -SERVER_BINARY=scrcpy-server.jar +SERVER_BINARY=scrcpy-server echo "Platform: android-$PLATFORM" echo "Build-tools: $BUILD_TOOLS" @@ -30,7 +33,8 @@ mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy" package com.genymobile.scrcpy; public final class BuildConfig { - public static final boolean DEBUG = false; + public static final boolean DEBUG = $SCRCPY_DEBUG; + public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME"; } EOF @@ -59,4 +63,4 @@ cd "$BUILD_DIR" jar cvf "$SERVER_BINARY" classes.dex rm -rf classes.dex classes -echo "Server generated in $BUILD_DIR/scrcpy-server.jar" +echo "Server generated in $BUILD_DIR/$SERVER_BINARY" diff --git a/server/meson.build b/server/meson.build index 43901246..4ba481d5 100644 --- a/server/meson.build +++ b/server/meson.build @@ -4,7 +4,7 @@ prebuilt_server = get_option('prebuilt_server') if prebuilt_server == '' custom_target('scrcpy-server', build_always: true, # gradle is responsible for tracking source changes - output: 'scrcpy-server.jar', + output: 'scrcpy-server', command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], console: true, install: true, @@ -16,7 +16,7 @@ else endif custom_target('scrcpy-server-prebuilt', input: prebuilt_server, - output: 'scrcpy-server.jar', + output: 'scrcpy-server', command: ['cp', '@INPUT@', '@OUTPUT@'], install: true, install_dir: 'share/scrcpy') diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index a1cd873a..30c05a3b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -7,7 +7,7 @@ public final class ControlMessage { public static final int TYPE_INJECT_KEYCODE = 0; public static final int TYPE_INJECT_TEXT = 1; - public static final int TYPE_INJECT_MOUSE_EVENT = 2; + public static final int TYPE_INJECT_TOUCH_EVENT = 2; public static final int TYPE_INJECT_SCROLL_EVENT = 3; public static final int TYPE_BACK_OR_SCREEN_ON = 4; public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; @@ -22,6 +22,8 @@ public final class ControlMessage { private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_* private int keycode; // KeyEvent.KEYCODE_* private int buttons; // MotionEvent.BUTTON_* + private long pointerId; + private float pressure; private Position position; private int hScroll; private int vScroll; @@ -45,12 +47,15 @@ public final class ControlMessage { return msg; } - public static ControlMessage createInjectMouseEvent(int action, int buttons, Position position) { + public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, + int buttons) { ControlMessage msg = new ControlMessage(); - msg.type = TYPE_INJECT_MOUSE_EVENT; + msg.type = TYPE_INJECT_TOUCH_EVENT; msg.action = action; - msg.buttons = buttons; + msg.pointerId = pointerId; + msg.pressure = pressure; msg.position = position; + msg.buttons = buttons; return msg; } @@ -110,6 +115,14 @@ public final class ControlMessage { return buttons; } + public long getPointerId() { + return pointerId; + } + + public float getPressure() { + return pressure; + } + public Position getPosition() { return position; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 8ced049d..2f8b5177 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -10,6 +10,7 @@ public class ControlMessageReader { private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17; + private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 21; private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; @@ -59,8 +60,8 @@ public class ControlMessageReader { case ControlMessage.TYPE_INJECT_TEXT: msg = parseInjectText(); break; - case ControlMessage.TYPE_INJECT_MOUSE_EVENT: - msg = parseInjectMouseEvent(); + case ControlMessage.TYPE_INJECT_TOUCH_EVENT: + msg = parseInjectTouchEvent(); break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: msg = parseInjectScrollEvent(); @@ -120,14 +121,20 @@ public class ControlMessageReader { return ControlMessage.createInjectText(text); } - private ControlMessage parseInjectMouseEvent() { - if (buffer.remaining() < INJECT_MOUSE_EVENT_PAYLOAD_LENGTH) { + @SuppressWarnings("checkstyle:MagicNumber") + private ControlMessage parseInjectTouchEvent() { + if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { return null; } int action = toUnsigned(buffer.get()); - int buttons = buffer.getInt(); + long pointerId = buffer.getLong(); Position position = readPosition(buffer); - return ControlMessage.createInjectMouseEvent(action, buttons, position); + // 16 bits fixed-point + int pressureInt = toUnsigned(buffer.getShort()); + // convert it to a float between 0 and 1 (0x1p16f is 2^16 as float) + float pressure = pressureInt == 0xffff ? 1f : (pressureInt / 0x1p16f); + int buttons = buffer.getInt(); + return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons); } private ControlMessage parseInjectScrollEvent() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 263fc2fc..ce02e333 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -19,38 +19,32 @@ public class Controller { private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); - private long lastMouseDown; - private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()}; - private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()}; + private long lastTouchDown; + private final PointersState pointersState = new PointersState(); + private final MotionEvent.PointerProperties[] pointerProperties = + new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; + private final MotionEvent.PointerCoords[] pointerCoords = + new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; public Controller(Device device, DesktopConnection connection) { this.device = device; this.connection = connection; - initPointer(); + initPointers(); sender = new DeviceMessageSender(connection); } - private void initPointer() { - MotionEvent.PointerProperties props = pointerProperties[0]; - props.id = 0; - props.toolType = MotionEvent.TOOL_TYPE_FINGER; + private void initPointers() { + for (int i = 0; i < PointersState.MAX_POINTERS; ++i) { + MotionEvent.PointerProperties props = new MotionEvent.PointerProperties(); + props.toolType = MotionEvent.TOOL_TYPE_FINGER; - MotionEvent.PointerCoords coords = pointerCoords[0]; - coords.orientation = 0; - coords.pressure = 1; - coords.size = 1; - } + MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); + coords.orientation = 0; + coords.size = 1; - private void setPointerCoords(Point point) { - MotionEvent.PointerCoords coords = pointerCoords[0]; - coords.x = point.getX(); - coords.y = point.getY(); - } - - private void setScroll(int hScroll, int vScroll) { - MotionEvent.PointerCoords coords = pointerCoords[0]; - coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); - coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); + pointerProperties[i] = props; + pointerCoords[i] = coords; + } } @SuppressWarnings("checkstyle:MagicNumber") @@ -87,8 +81,8 @@ public class Controller { case ControlMessage.TYPE_INJECT_TEXT: injectText(msg.getText()); break; - case ControlMessage.TYPE_INJECT_MOUSE_EVENT: - injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition()); + case ControlMessage.TYPE_INJECT_TOUCH_EVENT: + injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); @@ -148,19 +142,42 @@ public class Controller { return successCount; } - private boolean injectMouse(int action, int buttons, Position position) { + private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) { long now = SystemClock.uptimeMillis(); - if (action == MotionEvent.ACTION_DOWN) { - lastMouseDown = now; - } + Point point = device.getPhysicalPoint(position); if (point == null) { // ignore event return false; } - setPointerCoords(point); - MotionEvent event = MotionEvent.obtain(lastMouseDown, now, action, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, 0, 0, - InputDevice.SOURCE_TOUCHSCREEN, 0); + + int pointerIndex = pointersState.getPointerIndex(pointerId); + if (pointerIndex == -1) { + Ln.w("Too many pointers for touch event"); + return false; + } + Pointer pointer = pointersState.get(pointerIndex); + pointer.setPoint(point); + pointer.setPressure(pressure); + pointer.setUp(action == MotionEvent.ACTION_UP); + + int pointerCount = pointersState.update(pointerProperties, pointerCoords); + + if (pointerCount == 1) { + if (action == MotionEvent.ACTION_DOWN) { + lastTouchDown = now; + } + } else { + // secondary pointers must use ACTION_POINTER_* ORed with the pointerIndex + if (action == MotionEvent.ACTION_UP) { + action = MotionEvent.ACTION_POINTER_UP | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT); + } else if (action == MotionEvent.ACTION_DOWN) { + action = MotionEvent.ACTION_POINTER_DOWN | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT); + } + } + + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, + pointerCoords, 0, buttons, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); return injectEvent(event); } @@ -171,23 +188,30 @@ public class Controller { // ignore event return false; } - setPointerCoords(point); - setScroll(hScroll, vScroll); - MotionEvent event = MotionEvent.obtain(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, 0, - 0, InputDevice.SOURCE_MOUSE, 0); + + MotionEvent.PointerProperties props = pointerProperties[0]; + props.id = 0; + + MotionEvent.PointerCoords coords = pointerCoords[0]; + coords.x = point.getX(); + coords.y = point.getY(); + coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); + coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); + + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, + pointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0); return injectEvent(event); } private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { long now = SystemClock.uptimeMillis(); - KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, - InputDevice.SOURCE_KEYBOARD); + KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, + 0, 0, InputDevice.SOURCE_KEYBOARD); return injectEvent(event); } private boolean injectKeycode(int keyCode) { - return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) - && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); + return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); } private boolean injectEvent(InputEvent event) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 538135d4..708b9516 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -161,7 +161,11 @@ public final class Device { * @param mode one of the {@code SCREEN_POWER_MODE_*} constants */ public void setScreenPowerMode(int mode) { - IBinder d = SurfaceControl.getBuiltInDisplay(0); + IBinder d = SurfaceControl.getBuiltInDisplay(); + if (d == null) { + Ln.e("Could not get built-in display"); + return; + } SurfaceControl.setDisplayPowerMode(d, mode); Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index af6b2ee1..5b993f30 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -5,6 +5,7 @@ import android.graphics.Rect; public class Options { private int maxSize; private int bitRate; + private int maxFps; private boolean tunnelForward; private Rect crop; private boolean sendFrameMeta; // send PTS so that the client may record properly @@ -26,6 +27,14 @@ public class Options { this.bitRate = bitRate; } + public int getMaxFps() { + return maxFps; + } + + public void setMaxFps(int maxFps) { + this.maxFps = maxFps; + } + public boolean isTunnelForward() { return tunnelForward; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Pointer.java b/server/src/main/java/com/genymobile/scrcpy/Pointer.java new file mode 100644 index 00000000..b89cc256 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Pointer.java @@ -0,0 +1,55 @@ +package com.genymobile.scrcpy; + +public class Pointer { + + /** + * Pointer id as received from the client. + */ + private final long id; + + /** + * Local pointer id, using the lowest possible values to fill the {@link android.view.MotionEvent.PointerProperties PointerProperties}. + */ + private final int localId; + + private Point point; + private float pressure; + private boolean up; + + public Pointer(long id, int localId) { + this.id = id; + this.localId = localId; + } + + public long getId() { + return id; + } + + public int getLocalId() { + return localId; + } + + public Point getPoint() { + return point; + } + + public void setPoint(Point point) { + this.point = point; + } + + public float getPressure() { + return pressure; + } + + public void setPressure(float pressure) { + this.pressure = pressure; + } + + public boolean isUp() { + return up; + } + + public void setUp(boolean up) { + this.up = up; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/PointersState.java b/server/src/main/java/com/genymobile/scrcpy/PointersState.java new file mode 100644 index 00000000..d8daaff2 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/PointersState.java @@ -0,0 +1,103 @@ +package com.genymobile.scrcpy; + +import android.view.MotionEvent; + +import java.util.ArrayList; +import java.util.List; + +public class PointersState { + + public static final int MAX_POINTERS = 10; + + private final List pointers = new ArrayList<>(); + + private int indexOf(long id) { + for (int i = 0; i < pointers.size(); ++i) { + Pointer pointer = pointers.get(i); + if (pointer.getId() == id) { + return i; + } + } + return -1; + } + + private boolean isLocalIdAvailable(int localId) { + for (int i = 0; i < pointers.size(); ++i) { + Pointer pointer = pointers.get(i); + if (pointer.getLocalId() == localId) { + return false; + } + } + return true; + } + + private int nextUnusedLocalId() { + for (int localId = 0; localId < MAX_POINTERS; ++localId) { + if (isLocalIdAvailable(localId)) { + return localId; + } + } + return -1; + } + + public Pointer get(int index) { + return pointers.get(index); + } + + public int getPointerIndex(long id) { + int index = indexOf(id); + if (index != -1) { + // already exists, return it + return index; + } + if (pointers.size() >= MAX_POINTERS) { + // it's full + return -1; + } + // id 0 is reserved for mouse events + int localId = nextUnusedLocalId(); + if (localId == -1) { + throw new AssertionError("pointers.size() < maxFingers implies that a local id is available"); + } + Pointer pointer = new Pointer(id, localId); + pointers.add(pointer); + // return the index of the pointer + return pointers.size() - 1; + } + + /** + * Initialize the motion event parameters. + * + * @param props the pointer properties + * @param coords the pointer coordinates + * @return The number of items initialized (the number of pointers). + */ + public int update(MotionEvent.PointerProperties[] props, MotionEvent.PointerCoords[] coords) { + int count = pointers.size(); + for (int i = 0; i < count; ++i) { + Pointer pointer = pointers.get(i); + + // id 0 is reserved for mouse events + props[i].id = pointer.getLocalId(); + + Point point = pointer.getPoint(); + coords[i].x = point.getX(); + coords[i].y = point.getY(); + coords[i].pressure = pointer.getPressure(); + } + cleanUp(); + return count; + } + + /** + * Remove all pointers which are UP. + */ + private void cleanUp() { + for (int i = pointers.size() - 1; i >= 0; --i) { + Pointer pointer = pointers.get(i); + if (pointer.isUp()) { + pointers.remove(i); + } + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 8357b061..c9a37f84 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -6,6 +6,7 @@ import android.graphics.Rect; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; +import android.os.Build; import android.os.IBinder; import android.view.Surface; @@ -16,32 +17,29 @@ import java.util.concurrent.atomic.AtomicBoolean; public class ScreenEncoder implements Device.RotationListener { - private static final int DEFAULT_FRAME_RATE = 60; // fps private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds + private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms - private static final int REPEAT_FRAME_DELAY = 6; // repeat after 6 frames - - private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000; private static final int NO_PTS = -1; private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); private int bitRate; - private int frameRate; + private int maxFps; private int iFrameInterval; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int frameRate, int iFrameInterval) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; - this.frameRate = frameRate; + this.maxFps = maxFps; this.iFrameInterval = iFrameInterval; } - public ScreenEncoder(boolean sendFrameMeta, int bitRate) { - this(sendFrameMeta, bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL); + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { + this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); } @Override @@ -54,7 +52,10 @@ public class ScreenEncoder implements Device.RotationListener { } public void streamScreen(Device device, FileDescriptor fd) throws IOException { - MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval); + Workarounds.prepareMainLooper(); + Workarounds.fillAppInfo(); + + MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval); device.setRotationListener(this); boolean alive; try { @@ -137,15 +138,24 @@ public class ScreenEncoder implements Device.RotationListener { return MediaCodec.createEncoderByType("video/avc"); } - private static MediaFormat createFormat(int bitRate, int frameRate, int iFrameInterval) throws IOException { + @SuppressWarnings("checkstyle:MagicNumber") + private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, "video/avc"); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); - format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); + // must be present to configure the encoder, but does not impact the actual frame rate, which is variable + format.setInteger(MediaFormat.KEY_FRAME_RATE, 60); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval); // display the very first frame, and recover from bad quality when no new frames - format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate); // µs + format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs + if (maxFps > 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + format.setFloat(MediaFormat.KEY_MAX_FPS_TO_ENCODER, maxFps); + } else { + Ln.w("Max FPS is only supported since Android 10, the option has been ignored"); + } + } return format; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 3bd2fcdc..ad14e5d8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -7,7 +7,7 @@ import java.io.IOException; public final class Server { - private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; + private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server"; private Server() { // not instantiable @@ -17,7 +17,7 @@ public final class Server { final Device device = new Device(options); boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { - ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate()); + ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps()); if (options.getControl()) { Controller controller = new Controller(device, connection); @@ -67,29 +67,42 @@ public final class Server { @SuppressWarnings("checkstyle:MagicNumber") private static Options createOptions(String... args) { - if (args.length != 6) { - throw new IllegalArgumentException("Expecting 6 parameters"); + if (args.length < 1) { + throw new IllegalArgumentException("Missing client version"); + } + + String clientVersion = args[0]; + if (!clientVersion.equals(BuildConfig.VERSION_NAME)) { + throw new IllegalArgumentException("The server version (" + clientVersion + ") does not match the client " + + "(" + BuildConfig.VERSION_NAME + ")"); + } + + if (args.length != 8) { + throw new IllegalArgumentException("Expecting 8 parameters"); } Options options = new Options(); - int maxSize = Integer.parseInt(args[0]) & ~7; // multiple of 8 + int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8 options.setMaxSize(maxSize); - int bitRate = Integer.parseInt(args[1]); + int bitRate = Integer.parseInt(args[2]); options.setBitRate(bitRate); + int maxFps = Integer.parseInt(args[3]); + options.setMaxFps(maxFps); + // use "adb forward" instead of "adb tunnel"? (so the server must listen) - boolean tunnelForward = Boolean.parseBoolean(args[2]); + boolean tunnelForward = Boolean.parseBoolean(args[4]); options.setTunnelForward(tunnelForward); - Rect crop = parseCrop(args[3]); + Rect crop = parseCrop(args[5]); options.setCrop(crop); - boolean sendFrameMeta = Boolean.parseBoolean(args[4]); + boolean sendFrameMeta = Boolean.parseBoolean(args[6]); options.setSendFrameMeta(sendFrameMeta); - boolean control = Boolean.parseBoolean(args[5]); + boolean control = Boolean.parseBoolean(args[7]); options.setControl(control); return options; diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java new file mode 100644 index 00000000..f45d82a4 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -0,0 +1,64 @@ +package com.genymobile.scrcpy; + +import android.annotation.SuppressLint; +import android.content.pm.ApplicationInfo; +import android.os.Looper; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +public final class Workarounds { + private Workarounds() { + // not instantiable + } + + public static void prepareMainLooper() { + // Some devices internally create a Handler when creating an input Surface, causing an exception: + // "Can't create handler inside thread that has not called Looper.prepare()" + // + // + // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: + // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' + // on a null object reference" + // + Looper.prepareMainLooper(); + } + + @SuppressLint("PrivateApi") + public static void fillAppInfo() { + try { + // ActivityThread activityThread = new ActivityThread(); + Class activityThreadClass = Class.forName("android.app.ActivityThread"); + Constructor activityThreadConstructor = activityThreadClass.getDeclaredConstructor(); + activityThreadConstructor.setAccessible(true); + Object activityThread = activityThreadConstructor.newInstance(); + + // ActivityThread.sCurrentActivityThread = activityThread; + Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread"); + sCurrentActivityThreadField.setAccessible(true); + sCurrentActivityThreadField.set(null, activityThread); + + // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); + Class appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData"); + Constructor appBindDataConstructor = appBindDataClass.getDeclaredConstructor(); + appBindDataConstructor.setAccessible(true); + Object appBindData = appBindDataConstructor.newInstance(); + + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.packageName = "com.genymobile.scrcpy"; + + // appBindData.appInfo = applicationInfo; + Field appInfo = appBindDataClass.getDeclaredField("appInfo"); + appInfo.setAccessible(true); + appInfo.set(appBindData, applicationInfo); + + // activityThread.mBoundApplication = appBindData; + Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication"); + mBoundApplicationField.setAccessible(true); + mBoundApplicationField.set(activityThread, appBindData); + } catch (Throwable throwable) { + // this is a workaround, so failing is not an error + Ln.w("Could not fill app info: " + throwable.getMessage()); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index a058a8bb..27dcb443 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,44 +1,102 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.content.ClipData; +import android.os.Build; import android.os.IInterface; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class ClipboardManager { + + private static final String PACKAGE_NAME = "com.android.shell"; + private static final int USER_ID = 0; + private final IInterface manager; - private final Method getPrimaryClipMethod; - private final Method setPrimaryClipMethod; + private Method getPrimaryClipMethod; + private Method setPrimaryClipMethod; public ClipboardManager(IInterface manager) { this.manager = manager; - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); - } catch (NoSuchMethodException e) { - throw new AssertionError(e); + } + + private Method getGetPrimaryClipMethod() { + if (getPrimaryClipMethod == null) { + try { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); + } else { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); + } + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); + } + } + return getPrimaryClipMethod; + } + + private Method getSetPrimaryClipMethod() { + if (setPrimaryClipMethod == null) { + try { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); + } else { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, + String.class, int.class); + } + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); + } + } + return setPrimaryClipMethod; + } + + private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, + IllegalAccessException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return (ClipData) method.invoke(manager, PACKAGE_NAME); + } + return (ClipData) method.invoke(manager, PACKAGE_NAME, USER_ID); + } + + private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) throws InvocationTargetException, + IllegalAccessException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + method.invoke(manager, clipData, PACKAGE_NAME); + } else { + method.invoke(manager, clipData, PACKAGE_NAME, USER_ID); } } public CharSequence getText() { + Method method = getGetPrimaryClipMethod(); + if (method == null) { + return null; + } try { - ClipData clipData = (ClipData) getPrimaryClipMethod.invoke(manager, "com.android.shell"); + ClipData clipData = getPrimaryClip(method, manager); if (clipData == null || clipData.getItemCount() == 0) { return null; } return clipData.getItemAt(0).getText(); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Could not invoke " + method.getName(), e); + return null; } } public void setText(CharSequence text) { + Method method = getSetPrimaryClipMethod(); + if (method == null) { + return; + } ClipData clipData = ClipData.newPlainText(null, text); try { - setPrimaryClipMethod.invoke(manager, clipData, "com.android.shell"); + setPrimaryClip(method, manager, clipData); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Could not invoke " + method.getName(), e); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 1fc78c27..788a04c7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.os.IInterface; import android.view.InputEvent; @@ -13,22 +15,33 @@ public final class InputManager { public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; private final IInterface manager; - private final Method injectInputEventMethod; + private Method injectInputEventMethod; public InputManager(IInterface manager) { this.manager = manager; - try { - injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); - } catch (NoSuchMethodException e) { - throw new AssertionError(e); + } + + private Method getInjectInputEventMethod() { + if (injectInputEventMethod == null) { + try { + injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); + } } + return injectInputEventMethod; } public boolean injectInputEvent(InputEvent inputEvent, int mode) { + Method method = getInjectInputEventMethod(); + if (method == null) { + return false; + } try { - return (Boolean) injectInputEventMethod.invoke(manager, inputEvent, mode); + return (Boolean) method.invoke(manager, inputEvent, mode); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Could not invoke " + method.getName(), e); + return false; } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index a730d1b1..66acdba8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.annotation.SuppressLint; import android.os.Build; import android.os.IInterface; @@ -9,24 +11,35 @@ import java.lang.reflect.Method; public final class PowerManager { private final IInterface manager; - private final Method isScreenOnMethod; + private Method isScreenOnMethod; public PowerManager(IInterface manager) { this.manager = manager; - try { - @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future - String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; - isScreenOnMethod = manager.getClass().getMethod(methodName); - } catch (NoSuchMethodException e) { - throw new AssertionError(e); + } + + private Method getIsScreenOnMethod() { + if (isScreenOnMethod == null) { + try { + @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future + String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; + isScreenOnMethod = manager.getClass().getMethod(methodName); + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); + } } + return isScreenOnMethod; } public boolean isScreenOn() { + Method method = getIsScreenOnMethod(); + if (method == null) { + return false; + } try { - return (Boolean) isScreenOnMethod.invoke(manager); + return (Boolean) method.invoke(manager); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Could not invoke " + method.getName(), e); + return false; } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 7cd28da6..670de952 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -17,35 +17,49 @@ public class StatusBarManager { this.manager = manager; } - public void expandNotificationsPanel() { + private Method getExpandNotificationsPanelMethod() { if (expandNotificationsPanelMethod == null) { try { expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); } catch (NoSuchMethodException e) { - Ln.e("ServiceBarManager.expandNotificationsPanel() is not available on this device"); - return; + Ln.e("Could not find method", e); } } - try { - expandNotificationsPanelMethod.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke ServiceBarManager.expandNotificationsPanel()", e); - } + return expandNotificationsPanelMethod; } - public void collapsePanels() { + private Method getCollapsePanelsMethod() { if (collapsePanelsMethod == null) { try { collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); } catch (NoSuchMethodException e) { - Ln.e("ServiceBarManager.collapsePanels() is not available on this device"); - return; + Ln.e("Could not find method", e); } } + return collapsePanelsMethod; + } + + public void expandNotificationsPanel() { + Method method = getExpandNotificationsPanelMethod(); + if (method == null) { + return; + } try { - collapsePanelsMethod.invoke(manager); + method.invoke(manager); } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke ServiceBarManager.collapsePanels()", e); + Ln.e("Could not invoke " + method.getName(), e); + } + } + + public void collapsePanels() { + Method method = getCollapsePanelsMethod(); + if (method == null) { + return; + } + try { + method.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException e) { + Ln.e("Could not invoke " + method.getName(), e); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 5b5586ff..bef6e5d9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -1,11 +1,16 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.annotation.SuppressLint; import android.graphics.Rect; import android.os.Build; import android.os.IBinder; import android.view.Surface; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + @SuppressLint("PrivateApi") public final class SurfaceControl { @@ -23,6 +28,9 @@ public final class SurfaceControl { } } + private static Method getBuiltInDisplayMethod; + private static Method setDisplayPowerModeMethod; + private SurfaceControl() { // only static methods } @@ -76,24 +84,62 @@ public final class SurfaceControl { } } - public static IBinder getBuiltInDisplay(int builtInDisplayId) { - try { - // the method signature has changed in Android Q - // - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId); + private static Method getGetBuiltInDisplayMethod() { + if (getBuiltInDisplayMethod == null) { + try { + // the method signature has changed in Android Q + // + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class); + } else { + getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken"); + } + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); } - return (IBinder) CLASS.getMethod("getPhysicalDisplayToken", long.class).invoke(null, builtInDisplayId); - } catch (Exception e) { - throw new AssertionError(e); + } + return getBuiltInDisplayMethod; + } + + public static IBinder getBuiltInDisplay() { + Method method = getGetBuiltInDisplayMethod(); + if (method == null) { + return null; + } + try { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + // call getBuiltInDisplay(0) + return (IBinder) method.invoke(null, 0); + } + + // call getInternalDisplayToken() + return (IBinder) method.invoke(null); + } catch (InvocationTargetException | IllegalAccessException e) { + Ln.e("Could not invoke " + method.getName(), e); + return null; } } + private static Method getSetDisplayPowerModeMethod() { + if (setDisplayPowerModeMethod == null) { + try { + setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class); + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); + } + } + return setDisplayPowerModeMethod; + } + public static void setDisplayPowerMode(IBinder displayToken, int mode) { + Method method = getSetDisplayPowerModeMethod(); + if (method == null) { + return; + } try { - CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class).invoke(null, displayToken, mode); - } catch (Exception e) { - throw new AssertionError(e); + method.invoke(null, displayToken, mode); + } catch (InvocationTargetException | IllegalAccessException e) { + Ln.e("Could not invoke " + method.getName(), e); } } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index f0c643d4..ede759dc 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -78,31 +78,35 @@ public class ControlMessageReaderTest { @Test @SuppressWarnings("checkstyle:MagicNumber") - public void testParseMouseEvent() throws IOException { + public void testParseTouchEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_INJECT_MOUSE_EVENT); + dos.writeByte(ControlMessage.TYPE_INJECT_TOUCH_EVENT); dos.writeByte(MotionEvent.ACTION_DOWN); - dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeLong(-42); // pointerId dos.writeInt(100); dos.writeInt(200); dos.writeShort(1080); dos.writeShort(1920); + dos.writeShort(0xffff); // pressure + dos.writeInt(MotionEvent.BUTTON_PRIMARY); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); - Assert.assertEquals(ControlMessage.TYPE_INJECT_MOUSE_EVENT, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_INJECT_TOUCH_EVENT, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); - Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons()); + Assert.assertEquals(-42, event.getPointerId()); Assert.assertEquals(100, event.getPosition().getPoint().getX()); Assert.assertEquals(200, event.getPosition().getPoint().getY()); Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); + Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons()); } @Test