diff --git a/BUILD.md b/BUILD.md index 8801e5fc..e35d07d0 100644 --- a/BUILD.md +++ b/BUILD.md @@ -40,7 +40,7 @@ Install the required packages from your package manager. ```bash # runtime dependencies -sudo apt install ffmpeg libsdl2-2.0-0 +sudo apt install ffmpeg libsdl2-2.0-0 adb # client build dependencies sudo apt install gcc git pkg-config meson ninja-build \ @@ -233,10 +233,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.11`][direct-scrcpy-server] - _(SHA-256: ff3a454012e91d9185cfe8ca7691cea16c43a7dcc08e92fa47ab9f0ea675abd1)_ + - [`scrcpy-server-v1.12.1`][direct-scrcpy-server] + _(SHA-256: 63e569c8a1d0c1df31d48c4214871c479a601782945fed50c1e61167d78266ea)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-server-v1.11 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-server-v1.12.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: 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/LICENSE b/LICENSE index 3d6840b1..bc4bb77d 100644 --- a/LICENSE +++ b/LICENSE @@ -188,7 +188,7 @@ identification within third-party archives. Copyright (C) 2018 Genymobile - Copyright (C) 2018-2019 Romain Vimont + Copyright (C) 2018-2020 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.ko.md b/README.ko.md new file mode 100644 index 00000000..b232accd --- /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-2020 Romain Vimont + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +## 관련 글 (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 677e7a1c..da05bb7e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.11) +# scrcpy (v1.12.1) This application provides display and control of Android devices connected on USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. @@ -37,8 +37,11 @@ control it using keyboard and mouse. ### Linux -On Linux, you typically need to [build the app manually][BUILD]. Don't worry, -it's not that hard. +In Debian (_testing_ and _sid_ for now): + +``` +apt install scrcpy +``` A [Snap] package is available: [`scrcpy`][snap-link]. @@ -56,19 +59,23 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. [Ebuild]: https://wiki.gentoo.org/wiki/Ebuild [ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy +You could also [build the app manually][BUILD] (don't worry, it's not that +hard). + + ### Windows For Windows, for simplicity, prebuilt archives with all the dependencies (including `adb`) are available: - - [`scrcpy-win32-v1.11.zip`][direct-win32] - _(SHA-256: f25ed46e6f3e81e0ff9b9b4df7fe1a4bbd13f8396b7391be0a488b64c675b41e)_ - - [`scrcpy-win64-v1.11.zip`][direct-win64] - _(SHA-256: 3802c9ea0307d437947ff150ec65e53990b0beaacd0c8d0bed19c7650ce141bd)_ + - [`scrcpy-win32-v1.12.1.zip`][direct-win32] + _(SHA-256: 0f4b3b063536b50a2df05dc42c760f9cc0093a9a26dbdf02d8232c74dab43480)_ + - [`scrcpy-win64-v1.12.1.zip`][direct-win64] + _(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_ -[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 +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win32-v1.12.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip You can also [build the app manually][BUILD]. @@ -343,6 +350,13 @@ Note that it only shows _physical_ touches (with the finger on the device). ### Input control +#### Rotate device screen + +Press `Ctrl`+`r` to switch between portrait and landscape modes. + +Note that it rotates only if the application in foreground supports the +requested orientation. + #### Copy-paste It is possible to synchronize clipboards between the computer and the device, in @@ -425,6 +439,7 @@ Also see [issue #14]. | Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p` | Power on | _Right-click²_ | _Right-click²_ | Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o` + | Rotate device screen | `Ctrl`+`r` | `Cmd`+`r` | Expand notification panel | `Ctrl`+`n` | `Cmd`+`n` | Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` | Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c` @@ -481,7 +496,7 @@ Read the [developers page]. ## Licence Copyright (C) 2018 Genymobile - Copyright (C) 2018-2019 Romain Vimont + Copyright (C) 2018-2020 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/meson.build b/app/meson.build index 159ae695..3bcb9bc1 100644 --- a/app/meson.build +++ b/app/meson.build @@ -1,5 +1,6 @@ src = [ 'src/main.c', + 'src/cli.c', 'src/command.c', 'src/control_msg.c', 'src/controller.c', @@ -10,16 +11,16 @@ src = [ 'src/file_handler.c', 'src/fps_counter.c', 'src/input_manager.c', - 'src/net.c', 'src/receiver.c', 'src/recorder.c', 'src/scrcpy.c', 'src/screen.c', 'src/server.c', - 'src/str_util.c', - 'src/tiny_xpm.c', 'src/stream.c', + 'src/tiny_xpm.c', 'src/video_buffer.c', + 'src/util/net.c', + 'src/util/str_util.c' ] if not get_option('crossbuild_windows') @@ -85,7 +86,7 @@ endif conf = configuration_data() # expose the build type -conf.set('BUILD_DEBUG', get_option('buildtype') == 'debug') +conf.set('NDEBUG', get_option('buildtype') != 'debug') # the version, updated on release conf.set_quoted('SCRCPY_VERSION', meson.project_version()) @@ -140,31 +141,43 @@ install_man('scrcpy.1') ### TESTS -tests = [ - ['test_cbuf', [ - 'tests/test_cbuf.c', - ]], - ['test_control_event_serialize', [ - 'tests/test_control_msg_serialize.c', - 'src/control_msg.c', - 'src/str_util.c' - ]], - ['test_device_event_deserialize', [ - 'tests/test_device_msg_deserialize.c', - 'src/device_msg.c' - ]], - ['test_queue', [ - 'tests/test_queue.c', - ]], - ['test_strutil', [ - 'tests/test_strutil.c', - 'src/str_util.c' - ]], -] +# do not build tests in release (assertions would not be executed at all) +if get_option('buildtype') == 'debug' + tests = [ + ['test_buffer_util', [ + 'tests/test_buffer_util.c' + ]], + ['test_cbuf', [ + 'tests/test_cbuf.c', + ]], + ['test_cli', [ + 'tests/test_cli.c', + 'src/cli.c', + 'src/util/str_util.c', + ]], + ['test_control_event_serialize', [ + 'tests/test_control_msg_serialize.c', + 'src/control_msg.c', + 'src/util/str_util.c', + ]], + ['test_device_event_deserialize', [ + 'tests/test_device_msg_deserialize.c', + 'src/device_msg.c', + ]], + ['test_queue', [ + 'tests/test_queue.c', + ]], + ['test_strutil', [ + 'tests/test_strutil.c', + 'src/util/str_util.c', + ]], + ] -foreach t : tests - exe = executable(t[0], t[1], - include_directories: src_dir, - dependencies: dependencies) - test(t[0], exe) -endforeach + foreach t : tests + exe = executable(t[0], t[1], + include_directories: src_dir, + dependencies: dependencies, + c_args: ['-DSDL_MAIN_HANDLED']) + test(t[0], exe) + endforeach +endif diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 6cb062b5..9560df1c 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -26,7 +26,7 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are Default is 8000000. .TP -.BI \-\-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 @@ -42,7 +42,7 @@ Start in fullscreen. Print this help. .TP -.BI \-\-max\-fps " value +.BI "\-\-max\-fps " value Limit the framerate of screen capture (only supported on devices with Android >= 10). .TP @@ -88,7 +88,7 @@ The format is determined by the option if set, or by the file extension (.mp4 or .mkv). .TP -.BI \-\-record\-format " format +.BI "\-\-record\-format " format Force recording format (either mp4 or mkv). .TP @@ -118,29 +118,29 @@ Print the version of scrcpy. Disable window decorations (display borderless window). .TP -.BI \-\-window\-title " text +.BI "\-\-window\-title " text Set a custom window title. .TP -.BI \-\-window\-x " value +.BI "\-\-window\-x " value Set the initial window horizontal position. Default is -1 (automatic).\n .TP -.BI \-\-window\-y " value +.BI "\-\-window\-y " value Set the initial window vertical position. Default is -1 (automatic).\n .TP -.BI \-\-window\-width " value +.BI "\-\-window\-width " value Set the initial window width. Default is 0 (automatic).\n .TP -.BI \-\-window\-height " value +.BI "\-\-window\-height " value Set the initial window height. Default is 0 (automatic).\n @@ -195,6 +195,10 @@ turn screen on .B Ctrl+o turn device screen off (keep mirroring) +.TP +.B Ctrl+r +rotate device screen + .TP .B Ctrl+n expand notification panel @@ -257,7 +261,7 @@ Copyright \(co 2018 Genymobile Genymobile .UE -Copyright \(co 2018\-2019 +Copyright \(co 2018\-2020 .MT rom@rom1v.com Romain Vimont .ME diff --git a/app/src/cli.c b/app/src/cli.c new file mode 100644 index 00000000..d9e1013a --- /dev/null +++ b/app/src/cli.c @@ -0,0 +1,529 @@ +#include "cli.h" + +#include +#include +#include + +#include "config.h" +#include "recorder.h" +#include "util/log.h" +#include "util/str_util.h" + +void +scrcpy_print_usage(const char *arg0) { +#ifdef __APPLE__ +# define CTRL_OR_CMD "Cmd" +#else +# define CTRL_OR_CMD "Ctrl" +#endif + fprintf(stderr, + "Usage: %s [options]\n" + "\n" + "Options:\n" + "\n" + " --always-on-top\n" + " Make scrcpy window always on top (above other windows).\n" + "\n" + " -b, --bit-rate value\n" + " Encode the video at the given bit-rate, expressed in bits/s.\n" + " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" + " Default is %d.\n" + "\n" + " --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" + " Any --max-size value is computed on the cropped size.\n" + "\n" + " -f, --fullscreen\n" + " Start in fullscreen.\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" + " is preserved.\n" + " Default is %d%s.\n" + "\n" + " -n, --no-control\n" + " Disable device control (mirror the device in read-only).\n" + "\n" + " -N, --no-display\n" + " Do not display device (only when screen recording is\n" + " enabled).\n" + "\n" + " -p, --port port\n" + " Set the TCP port the client listens on.\n" + " Default is %d.\n" + "\n" + " --prefer-text\n" + " Inject alpha characters and space as text events instead of\n" + " key events.\n" + " This avoids issues when combining multiple keys to enter a\n" + " special character, but breaks the expected behavior of alpha\n" + " keys in games (typically WASD).\n" + "\n" + " --push-target path\n" + " Set the target directory for pushing files to the device by\n" + " drag & drop. It is passed as-is to \"adb push\".\n" + " Default is \"/sdcard/\".\n" + "\n" + " -r, --record file.mp4\n" + " Record screen to file.\n" + " The format is determined by the --record-format option if\n" + " set, or by the file extension (.mp4 or .mkv).\n" + "\n" + " --record-format format\n" + " Force recording format (either mp4 or mkv).\n" + "\n" + " --render-expired-frames\n" + " By default, to minimize latency, scrcpy always renders the\n" + " last available decoded frame, and drops any previous ones.\n" + " This flag forces to render all frames, at a cost of a\n" + " possible increased latency.\n" + "\n" + " -s, --serial serial\n" + " The device serial number. Mandatory only if several devices\n" + " are connected to adb.\n" + "\n" + " -S, --turn-screen-off\n" + " Turn the device screen off immediately.\n" + "\n" + " -t, --show-touches\n" + " Enable \"show touches\" on start, disable on quit.\n" + " It only shows physical touches (not clicks from scrcpy).\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" + " switch fullscreen mode\n" + "\n" + " " CTRL_OR_CMD "+g\n" + " resize window to 1:1 (pixel-perfect)\n" + "\n" + " " CTRL_OR_CMD "+x\n" + " Double-click on black borders\n" + " resize window to remove black borders\n" + "\n" + " Ctrl+h\n" + " Middle-click\n" + " click on HOME\n" + "\n" + " " CTRL_OR_CMD "+b\n" + " " CTRL_OR_CMD "+Backspace\n" + " Right-click (when screen is on)\n" + " click on BACK\n" + "\n" + " " CTRL_OR_CMD "+s\n" + " click on APP_SWITCH\n" + "\n" + " Ctrl+m\n" + " click on MENU\n" + "\n" + " " CTRL_OR_CMD "+Up\n" + " click on VOLUME_UP\n" + "\n" + " " CTRL_OR_CMD "+Down\n" + " click on VOLUME_DOWN\n" + "\n" + " " CTRL_OR_CMD "+p\n" + " click on POWER (turn screen on/off)\n" + "\n" + " Right-click (when screen is off)\n" + " power on\n" + "\n" + " " CTRL_OR_CMD "+o\n" + " turn device screen off (keep mirroring)\n" + "\n" + " " CTRL_OR_CMD "+r\n" + " rotate device screen\n" + "\n" + " " CTRL_OR_CMD "+n\n" + " expand notification panel\n" + "\n" + " " CTRL_OR_CMD "+Shift+n\n" + " collapse notification panel\n" + "\n" + " " CTRL_OR_CMD "+c\n" + " copy device clipboard to computer\n" + "\n" + " " CTRL_OR_CMD "+v\n" + " paste computer clipboard to device\n" + "\n" + " " CTRL_OR_CMD "+Shift+v\n" + " copy computer clipboard to device\n" + "\n" + " " CTRL_OR_CMD "+i\n" + " enable/disable FPS counter (print frames/second in logs)\n" + "\n" + " Drag & drop APK file\n" + " install APK from computer\n" + "\n", + arg0, + DEFAULT_BIT_RATE, + DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)", + DEFAULT_LOCAL_PORT); +} + +static bool +parse_integer_arg(const char *s, long *out, bool accept_suffix, long min, + long max, const char *name) { + long value; + bool ok; + if (accept_suffix) { + ok = parse_integer_with_suffix(s, &value); + } else { + ok = parse_integer(s, &value); + } + if (!ok) { + LOGE("Could not parse %s: %s", name, s); + return false; + } + + if (value < min || value > max) { + LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)", + name, value, min, max); + return false; + } + + *out = value; + return true; +} + +static bool +parse_bit_rate(const char *s, uint32_t *bit_rate) { + long value; + // long may be 32 bits (it is the case on mingw), so do not use more than + // 31 bits (long is signed) + bool ok = parse_integer_arg(s, &value, true, 0, 0x7FFFFFFF, "bit-rate"); + if (!ok) { + return false; + } + + *bit_rate = (uint32_t) value; + return true; +} + +static bool +parse_max_size(const char *s, uint16_t *max_size) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max size"); + if (!ok) { + return false; + } + + *max_size = (uint16_t) value; + return true; +} + +static bool +parse_max_fps(const char *s, uint16_t *max_fps) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 1000, "max fps"); + if (!ok) { + return false; + } + + *max_fps = (uint16_t) value; + return true; +} + +static bool +parse_window_position(const char *s, int16_t *position) { + long value; + bool ok = parse_integer_arg(s, &value, false, -1, 0x7FFF, + "window position"); + if (!ok) { + return false; + } + + *position = (int16_t) value; + return true; +} + +static bool +parse_window_dimension(const char *s, uint16_t *dimension) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, + "window dimension"); + if (!ok) { + return false; + } + + *dimension = (uint16_t) value; + return true; +} + +static bool +parse_port(const char *s, uint16_t *port) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "port"); + if (!ok) { + return false; + } + + *port = (uint16_t) value; + return true; +} + +static bool +parse_record_format(const char *optarg, enum recorder_format *format) { + if (!strcmp(optarg, "mp4")) { + *format = RECORDER_FORMAT_MP4; + return true; + } + if (!strcmp(optarg, "mkv")) { + *format = RECORDER_FORMAT_MKV; + return true; + } + LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); + return false; +} + +static enum recorder_format +guess_record_format(const char *filename) { + size_t len = strlen(filename); + if (len < 4) { + return 0; + } + const char *ext = &filename[len - 4]; + if (!strcmp(ext, ".mp4")) { + return RECORDER_FORMAT_MP4; + } + if (!strcmp(ext, ".mkv")) { + return RECORDER_FORMAT_MKV; + } + return 0; +} + +#define OPT_RENDER_EXPIRED_FRAMES 1000 +#define OPT_WINDOW_TITLE 1001 +#define OPT_PUSH_TARGET 1002 +#define OPT_ALWAYS_ON_TOP 1003 +#define OPT_CROP 1004 +#define OPT_RECORD_FORMAT 1005 +#define OPT_PREFER_TEXT 1006 +#define OPT_WINDOW_X 1007 +#define OPT_WINDOW_Y 1008 +#define OPT_WINDOW_WIDTH 1009 +#define OPT_WINDOW_HEIGHT 1010 +#define OPT_WINDOW_BORDERLESS 1011 +#define OPT_MAX_FPS 1012 + +bool +scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { + static const struct option long_options[] = { + {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, + {"bit-rate", required_argument, NULL, 'b'}, + {"crop", required_argument, NULL, OPT_CROP}, + {"fullscreen", no_argument, NULL, 'f'}, + {"help", no_argument, NULL, 'h'}, + {"max-fps", required_argument, NULL, OPT_MAX_FPS}, + {"max-size", required_argument, NULL, 'm'}, + {"no-control", no_argument, NULL, 'n'}, + {"no-display", no_argument, NULL, 'N'}, + {"port", required_argument, NULL, 'p'}, + {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, + {"record", required_argument, NULL, 'r'}, + {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, + {"render-expired-frames", no_argument, NULL, + OPT_RENDER_EXPIRED_FRAMES}, + {"serial", required_argument, NULL, 's'}, + {"show-touches", no_argument, NULL, 't'}, + {"turn-screen-off", no_argument, NULL, 'S'}, + {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, + {"version", no_argument, NULL, 'v'}, + {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, + {"window-x", required_argument, NULL, OPT_WINDOW_X}, + {"window-y", required_argument, NULL, OPT_WINDOW_Y}, + {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, + {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, + {"window-borderless", no_argument, NULL, + OPT_WINDOW_BORDERLESS}, + {NULL, 0, NULL, 0 }, + }; + + struct scrcpy_options *opts = &args->opts; + + optind = 0; // reset to start from the first argument in tests + + 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, &opts->bit_rate)) { + return false; + } + break; + case 'c': + LOGW("Deprecated option -c. Use --crop instead."); + // fall through + case OPT_CROP: + opts->crop = optarg; + break; + case 'f': + opts->fullscreen = true; + break; + case 'F': + 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, &opts->max_size)) { + return false; + } + break; + case 'n': + opts->control = false; + break; + case 'N': + opts->display = false; + break; + case 'p': + if (!parse_port(optarg, &opts->port)) { + return false; + } + break; + case 'r': + opts->record_filename = optarg; + break; + case 's': + opts->serial = optarg; + break; + case 'S': + opts->turn_screen_off = true; + break; + case 't': + opts->show_touches = true; + break; + case 'T': + 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: + opts->render_expired_frames = true; + break; + case OPT_WINDOW_TITLE: + opts->window_title = optarg; + break; + case OPT_WINDOW_X: + if (!parse_window_position(optarg, &opts->window_x)) { + return false; + } + break; + case OPT_WINDOW_Y: + if (!parse_window_position(optarg, &opts->window_y)) { + return false; + } + break; + case OPT_WINDOW_WIDTH: + if (!parse_window_dimension(optarg, &opts->window_width)) { + return false; + } + break; + case OPT_WINDOW_HEIGHT: + if (!parse_window_dimension(optarg, &opts->window_height)) { + return false; + } + break; + case OPT_WINDOW_BORDERLESS: + opts->window_borderless = true; + break; + case OPT_PUSH_TARGET: + opts->push_target = optarg; + break; + case OPT_PREFER_TEXT: + opts->prefer_text = true; + break; + default: + // getopt prints the error message on stderr + return false; + } + } + + if (!opts->display && !opts->record_filename) { + LOGE("-N/--no-display requires screen recording (-r/--record)"); + return false; + } + + if (!opts->display && opts->fullscreen) { + LOGE("-f/--fullscreen-window is incompatible with -N/--no-display"); + return false; + } + + int index = optind; + if (index < argc) { + LOGE("Unexpected additional argument: %s", argv[index]); + return false; + } + + if (opts->record_format && !opts->record_filename) { + LOGE("Record format specified without recording"); + return false; + } + + 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)", + opts->record_filename); + return false; + } + } + + if (!opts->control && opts->turn_screen_off) { + LOGE("Could not request to turn screen off if control is disabled"); + return false; + } + + return true; +} diff --git a/app/src/cli.h b/app/src/cli.h new file mode 100644 index 00000000..2e2bfe93 --- /dev/null +++ b/app/src/cli.h @@ -0,0 +1,21 @@ +#ifndef SCRCPY_CLI_H +#define SCRCPY_CLI_H + +#include + +#include "config.h" +#include "scrcpy.h" + +struct scrcpy_cli_args { + struct scrcpy_options opts; + bool help; + bool version; +}; + +void +scrcpy_print_usage(const char *arg0); + +bool +scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]); + +#endif diff --git a/app/src/command.c b/app/src/command.c index d914e6ab..63afccb4 100644 --- a/app/src/command.c +++ b/app/src/command.c @@ -4,11 +4,14 @@ #include #include #include +#include +#include +#include #include "config.h" #include "common.h" -#include "log.h" -#include "str_util.h" +#include "util/log.h" +#include "util/str_util.h" static const char *adb_command; @@ -91,7 +94,7 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { memcpy(&cmd[i], adb_cmd, len * sizeof(const char *)); cmd[len + i] = NULL; - enum process_result r = cmd_execute(cmd[0], cmd, &process); + enum process_result r = cmd_execute(cmd, &process); if (r != PROCESS_SUCCESS) { show_adb_err_msg(r, cmd); return PROCESS_NONE; @@ -202,3 +205,14 @@ process_check_success(process_t proc, const char *name) { } return true; } + +bool +is_regular_file(const char *path) { + struct stat path_stat; + int r = stat(path, &path_stat); + if (r) { + perror("stat"); + return false; + } + return S_ISREG(path_stat.st_mode); +} diff --git a/app/src/command.h b/app/src/command.h index d119c9bb..9fc81c1c 100644 --- a/app/src/command.h +++ b/app/src/command.h @@ -18,6 +18,7 @@ # define PRIsizet PRIu32 # endif # define PROCESS_NONE NULL +# define NO_EXIT_CODE -1u // max value as unsigned typedef HANDLE process_t; typedef DWORD exit_code_t; @@ -28,6 +29,7 @@ # define PRIsizet "zu" # define PRIexitcode "d" # define PROCESS_NONE -1 +# define NO_EXIT_CODE -1 typedef pid_t process_t; typedef int exit_code_t; @@ -35,8 +37,6 @@ #include "config.h" -# define NO_EXIT_CODE -1 - enum process_result { PROCESS_SUCCESS, PROCESS_ERROR_GENERIC, @@ -44,7 +44,7 @@ enum process_result { }; enum process_result -cmd_execute(const char *path, const char *const argv[], process_t *process); +cmd_execute(const char *const argv[], process_t *process); bool cmd_terminate(process_t pid); @@ -85,4 +85,8 @@ process_check_success(process_t proc, const char *name); char * get_executable_path(void); +// returns true if the file exists and is not a directory +bool +is_regular_file(const char *path); + #endif diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e042dc5a..45113139 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -1,12 +1,12 @@ #include "control_msg.h" +#include #include -#include #include "config.h" -#include "buffer_util.h" -#include "log.h" -#include "str_util.h" +#include "util/buffer_util.h" +#include "util/log.h" +#include "util/str_util.h" static void write_position(uint8_t *buf, const struct position *position) { @@ -27,7 +27,7 @@ write_string(const char *utf8, size_t max_len, unsigned char *buf) { static uint16_t to_fixed_point_16(float f) { - SDL_assert(f >= 0.0f && f <= 1.0f); + assert(f >= 0.0f && f <= 1.0f); uint32_t u = f * 0x1p16f; // 2^16 if (u >= 0xffff) { u = 0xffff; @@ -78,6 +78,7 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_GET_CLIPBOARD: + case CONTROL_MSG_TYPE_ROTATE_DEVICE: // no additional data return 1; default: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 2f319d9d..49a159a6 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -28,6 +28,7 @@ enum control_msg_type { CONTROL_MSG_TYPE_GET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD, CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + CONTROL_MSG_TYPE_ROTATE_DEVICE, }; enum screen_power_mode { diff --git a/app/src/controller.c b/app/src/controller.c index 7f90d787..d59a7411 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -1,10 +1,10 @@ #include "controller.h" -#include +#include #include "config.h" -#include "lock_util.h" -#include "log.h" +#include "util/lock.h" +#include "util/log.h" bool controller_init(struct controller *controller, socket_t control_socket) { @@ -85,7 +85,8 @@ run_controller(void *data) { } struct control_msg msg; bool non_empty = cbuf_take(&controller->queue, &msg); - SDL_assert(non_empty); + assert(non_empty); + (void) non_empty; mutex_unlock(controller->mutex); bool ok = process_msg(controller, &msg); diff --git a/app/src/controller.h b/app/src/controller.h index 1b0d005b..8011ef6a 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -6,10 +6,10 @@ #include #include "config.h" -#include "cbuf.h" #include "control_msg.h" -#include "net.h" #include "receiver.h" +#include "util/cbuf.h" +#include "util/net.h" struct control_msg_queue CBUF(struct control_msg, 64); diff --git a/app/src/decoder.c b/app/src/decoder.c index cad19913..49d4ce86 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -10,12 +9,11 @@ #include "config.h" #include "compat.h" -#include "buffer_util.h" #include "events.h" -#include "lock_util.h" -#include "log.h" #include "recorder.h" #include "video_buffer.h" +#include "util/buffer_util.h" +#include "util/log.h" // set the decoded frame as ready for rendering, and notify static void diff --git a/app/src/device.c b/app/src/device.c index 4f50ab48..f4c2628b 100644 --- a/app/src/device.c +++ b/app/src/device.c @@ -1,7 +1,7 @@ #include "device.h" #include "config.h" -#include "log.h" +#include "util/log.h" bool device_read_info(socket_t device_socket, char *device_name, struct size *size) { diff --git a/app/src/device.h b/app/src/device.h index 34a5f17f..8a94cd86 100644 --- a/app/src/device.h +++ b/app/src/device.h @@ -5,7 +5,7 @@ #include "config.h" #include "common.h" -#include "net.h" +#include "util/net.h" #define DEVICE_NAME_FIELD_LENGTH 64 diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 2fc90ae4..db176129 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -1,11 +1,10 @@ #include "device_msg.h" #include -#include #include "config.h" -#include "buffer_util.h" -#include "log.h" +#include "util/buffer_util.h" +#include "util/log.h" ssize_t device_msg_deserialize(const unsigned char *buf, size_t len, diff --git a/app/src/file_handler.c b/app/src/file_handler.c index e02ca2a9..ba689404 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -1,12 +1,12 @@ #include "file_handler.h" +#include #include -#include #include "config.h" #include "command.h" -#include "lock_util.h" -#include "log.h" +#include "util/lock.h" +#include "util/log.h" #define DEFAULT_PUSH_TARGET "/sdcard/" @@ -120,7 +120,8 @@ run_file_handler(void *data) { } struct file_handler_request req; bool non_empty = cbuf_take(&file_handler->queue, &req); - SDL_assert(non_empty); + assert(non_empty); + (void) non_empty; process_t process; if (req.action == ACTION_INSTALL_APK) { diff --git a/app/src/file_handler.h b/app/src/file_handler.h index 4c158296..078d0ca5 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -6,8 +6,8 @@ #include #include "config.h" -#include "cbuf.h" #include "command.h" +#include "util/cbuf.h" typedef enum { ACTION_INSTALL_APK, diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 2a9478f6..58c62d55 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -1,11 +1,11 @@ #include "fps_counter.h" -#include +#include #include #include "config.h" -#include "lock_util.h" -#include "log.h" +#include "util/lock.h" +#include "util/log.h" #define FPS_COUNTER_INTERVAL_MS 1000 @@ -77,7 +77,7 @@ run_fps_counter(void *data) { uint32_t now = SDL_GetTicks(); check_interval_expired(counter, now); - SDL_assert(counter->next_timestamp > now); + assert(counter->next_timestamp > now); uint32_t remaining = counter->next_timestamp - now; // ignore the reason (timeout or signaled), we just loop anyway diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 7d333c1b..8c4c230a 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -1,11 +1,11 @@ #include "input_manager.h" -#include +#include #include "config.h" #include "event_converter.h" -#include "lock_util.h" -#include "log.h" +#include "util/lock.h" +#include "util/log.h" // Convert window coordinates (as provided by SDL_GetMouseState() to renderer // coordinates (as provided in SDL mouse events) @@ -211,13 +211,23 @@ clipboard_paste(struct controller *controller) { } } +static void +rotate_device(struct controller *controller) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_ROTATE_DEVICE; + + if (!controller_push_msg(controller, &msg)) { + LOGW("Could not request device rotation"); + } +} + void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { if (!im->prefer_text) { char c = event->text[0]; if (isalpha(c) || c == ' ') { - SDL_assert(event->text[1] == '\0'); + assert(event->text[1] == '\0'); // letters and space are handled as raw key event return; } @@ -388,6 +398,11 @@ input_manager_process_key(struct input_manager *im, } } return; + case SDLK_r: + if (control && cmd && !shift && !repeat && down) { + rotate_device(controller); + } + return; } return; @@ -550,13 +565,8 @@ convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *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; + to->inject_scroll_event.hscroll = from->x; + to->inject_scroll_event.vscroll = from->y; return true; } diff --git a/app/src/lock_util.h b/app/src/lock_util.h deleted file mode 100644 index 260d2c12..00000000 --- a/app/src/lock_util.h +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef LOCKUTIL_H -#define LOCKUTIL_H - -#include -#include - -#include "config.h" -#include "log.h" - -static inline void -mutex_lock(SDL_mutex *mutex) { - if (SDL_LockMutex(mutex)) { - LOGC("Could not lock mutex"); - abort(); - } -} - -static inline void -mutex_unlock(SDL_mutex *mutex) { - if (SDL_UnlockMutex(mutex)) { - LOGC("Could not unlock mutex"); - abort(); - } -} - -static inline void -cond_wait(SDL_cond *cond, SDL_mutex *mutex) { - if (SDL_CondWait(cond, mutex)) { - LOGC("Could not wait on condition"); - abort(); - } -} - -static inline int -cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) { - int r = SDL_CondWaitTimeout(cond, mutex, ms); - if (r < 0) { - LOGC("Could not wait on condition with timeout"); - abort(); - } - return r; -} - -static inline void -cond_signal(SDL_cond *cond) { - if (SDL_CondSignal(cond)) { - LOGC("Could not signal a condition"); - abort(); - } -} - -#endif diff --git a/app/src/main.c b/app/src/main.c index 8a835bf1..d683c508 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1,206 +1,15 @@ #include "scrcpy.h" -#include #include -#include #include #include #define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem #include #include "config.h" +#include "cli.h" #include "compat.h" -#include "log.h" -#include "recorder.h" - -struct args { - struct scrcpy_options opts; - bool help; - bool version; -}; - -static void usage(const char *arg0) { -#ifdef __APPLE__ -# define CTRL_OR_CMD "Cmd" -#else -# define CTRL_OR_CMD "Ctrl" -#endif - fprintf(stderr, - "Usage: %s [options]\n" - "\n" - "Options:\n" - "\n" - " --always-on-top\n" - " Make scrcpy window always on top (above other windows).\n" - "\n" - " -b, --bit-rate value\n" - " Encode the video at the given bit-rate, expressed in bits/s.\n" - " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" - " Default is %d.\n" - "\n" - " --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" - " Any --max-size value is computed on the cropped size.\n" - "\n" - " -f, --fullscreen\n" - " Start in fullscreen.\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" - " is preserved.\n" - " Default is %d%s.\n" - "\n" - " -n, --no-control\n" - " Disable device control (mirror the device in read-only).\n" - "\n" - " -N, --no-display\n" - " Do not display device (only when screen recording is\n" - " enabled).\n" - "\n" - " -p, --port port\n" - " Set the TCP port the client listens on.\n" - " Default is %d.\n" - "\n" - " --prefer-text\n" - " Inject alpha characters and space as text events instead of\n" - " key events.\n" - " This avoids issues when combining multiple keys to enter a\n" - " special character, but breaks the expected behavior of alpha\n" - " keys in games (typically WASD).\n" - "\n" - " --push-target path\n" - " Set the target directory for pushing files to the device by\n" - " drag & drop. It is passed as-is to \"adb push\".\n" - " Default is \"/sdcard/\".\n" - "\n" - " -r, --record file.mp4\n" - " Record screen to file.\n" - " The format is determined by the --record-format option if\n" - " set, or by the file extension (.mp4 or .mkv).\n" - "\n" - " --record-format format\n" - " Force recording format (either mp4 or mkv).\n" - "\n" - " --render-expired-frames\n" - " By default, to minimize latency, scrcpy always renders the\n" - " last available decoded frame, and drops any previous ones.\n" - " This flag forces to render all frames, at a cost of a\n" - " possible increased latency.\n" - "\n" - " -s, --serial serial\n" - " The device serial number. Mandatory only if several devices\n" - " are connected to adb.\n" - "\n" - " -S, --turn-screen-off\n" - " Turn the device screen off immediately.\n" - "\n" - " -t, --show-touches\n" - " Enable \"show touches\" on start, disable on quit.\n" - " It only shows physical touches (not clicks from scrcpy).\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" - " switch fullscreen mode\n" - "\n" - " " CTRL_OR_CMD "+g\n" - " resize window to 1:1 (pixel-perfect)\n" - "\n" - " " CTRL_OR_CMD "+x\n" - " Double-click on black borders\n" - " resize window to remove black borders\n" - "\n" - " Ctrl+h\n" - " Middle-click\n" - " click on HOME\n" - "\n" - " " CTRL_OR_CMD "+b\n" - " " CTRL_OR_CMD "+Backspace\n" - " Right-click (when screen is on)\n" - " click on BACK\n" - "\n" - " " CTRL_OR_CMD "+s\n" - " click on APP_SWITCH\n" - "\n" - " Ctrl+m\n" - " click on MENU\n" - "\n" - " " CTRL_OR_CMD "+Up\n" - " click on VOLUME_UP\n" - "\n" - " " CTRL_OR_CMD "+Down\n" - " click on VOLUME_DOWN\n" - "\n" - " " CTRL_OR_CMD "+p\n" - " click on POWER (turn screen on/off)\n" - "\n" - " Right-click (when screen is off)\n" - " power on\n" - "\n" - " " CTRL_OR_CMD "+o\n" - " turn device screen off (keep mirroring)\n" - "\n" - " " CTRL_OR_CMD "+n\n" - " expand notification panel\n" - "\n" - " " CTRL_OR_CMD "+Shift+n\n" - " collapse notification panel\n" - "\n" - " " CTRL_OR_CMD "+c\n" - " copy device clipboard to computer\n" - "\n" - " " CTRL_OR_CMD "+v\n" - " paste computer clipboard to device\n" - "\n" - " " CTRL_OR_CMD "+Shift+v\n" - " copy computer clipboard to device\n" - "\n" - " " CTRL_OR_CMD "+i\n" - " enable/disable FPS counter (print frames/second in logs)\n" - "\n" - " Drag & drop APK file\n" - " install APK from computer\n" - "\n", - arg0, - DEFAULT_BIT_RATE, - DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)", - DEFAULT_LOCAL_PORT); -} +#include "util/log.h" static void print_version(void) { @@ -220,373 +29,6 @@ print_version(void) { LIBAVUTIL_VERSION_MICRO); } -static bool -parse_bit_rate(char *optarg, uint32_t *bit_rate) { - char *endptr; - if (*optarg == '\0') { - LOGE("Bit-rate parameter is empty"); - return false; - } - long value = strtol(optarg, &endptr, 0); - int mul = 1; - if (*endptr != '\0') { - if (optarg == endptr) { - LOGE("Invalid bit-rate: %s", optarg); - return false; - } - if ((*endptr == 'M' || *endptr == 'm') && endptr[1] == '\0') { - mul = 1000000; - } else if ((*endptr == 'K' || *endptr == 'k') && endptr[1] == '\0') { - mul = 1000; - } else { - LOGE("Invalid bit-rate unit: %s", optarg); - return false; - } - } - if (value < 0 || ((uint32_t) -1) / mul < value) { - LOGE("Bitrate must be positive and less than 2^32: %s", optarg); - return false; - } - - *bit_rate = (uint32_t) value * mul; - return true; -} - -static bool -parse_max_size(char *optarg, uint16_t *max_size) { - char *endptr; - if (*optarg == '\0') { - LOGE("Max size parameter is empty"); - return false; - } - long value = strtol(optarg, &endptr, 0); - if (*endptr != '\0') { - LOGE("Invalid max size: %s", optarg); - return false; - } - if (value & ~0xffff) { - LOGE("Max size must be between 0 and 65535: %ld", value); - return false; - } - - *max_size = (uint16_t) value; - 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("Port parameter is empty"); - return false; - } - long value = strtol(optarg, &endptr, 0); - if (*endptr != '\0') { - LOGE("Invalid port: %s", optarg); - return false; - } - if (value & ~0xffff) { - LOGE("Port out of range: %ld", value); - return false; - } - - *port = (uint16_t) value; - return true; -} - -static bool -parse_record_format(const char *optarg, enum recorder_format *format) { - if (!strcmp(optarg, "mp4")) { - *format = RECORDER_FORMAT_MP4; - return true; - } - if (!strcmp(optarg, "mkv")) { - *format = RECORDER_FORMAT_MKV; - return true; - } - LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); - return false; -} - -static enum recorder_format -guess_record_format(const char *filename) { - size_t len = strlen(filename); - if (len < 4) { - return 0; - } - const char *ext = &filename[len - 4]; - if (!strcmp(ext, ".mp4")) { - return RECORDER_FORMAT_MP4; - } - if (!strcmp(ext, ".mkv")) { - return RECORDER_FORMAT_MKV; - } - return 0; -} - -#define OPT_RENDER_EXPIRED_FRAMES 1000 -#define OPT_WINDOW_TITLE 1001 -#define OPT_PUSH_TARGET 1002 -#define OPT_ALWAYS_ON_TOP 1003 -#define OPT_CROP 1004 -#define OPT_RECORD_FORMAT 1005 -#define OPT_PREFER_TEXT 1006 -#define OPT_WINDOW_X 1007 -#define OPT_WINDOW_Y 1008 -#define OPT_WINDOW_WIDTH 1009 -#define OPT_WINDOW_HEIGHT 1010 -#define OPT_WINDOW_BORDERLESS 1011 -#define OPT_MAX_FPS 1012 - -static bool -parse_args(struct args *args, int argc, char *argv[]) { - static const struct option long_options[] = { - {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, - {"bit-rate", required_argument, NULL, 'b'}, - {"crop", required_argument, NULL, OPT_CROP}, - {"fullscreen", no_argument, NULL, 'f'}, - {"help", no_argument, NULL, 'h'}, - {"max-fps", required_argument, NULL, OPT_MAX_FPS}, - {"max-size", required_argument, NULL, 'm'}, - {"no-control", no_argument, NULL, 'n'}, - {"no-display", no_argument, NULL, 'N'}, - {"port", required_argument, NULL, 'p'}, - {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, - {"record", required_argument, NULL, 'r'}, - {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, - {"render-expired-frames", no_argument, NULL, - OPT_RENDER_EXPIRED_FRAMES}, - {"serial", required_argument, NULL, 's'}, - {"show-touches", no_argument, NULL, 't'}, - {"turn-screen-off", no_argument, NULL, 'S'}, - {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, - {"version", no_argument, NULL, 'v'}, - {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, - {"window-x", required_argument, NULL, OPT_WINDOW_X}, - {"window-y", required_argument, NULL, OPT_WINDOW_Y}, - {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, - {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, - {"window-borderless", no_argument, NULL, - OPT_WINDOW_BORDERLESS}, - {NULL, 0, NULL, 0 }, - }; - - struct scrcpy_options *opts = &args->opts; - - int c; - while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, - NULL)) != -1) { - switch (c) { - case 'b': - if (!parse_bit_rate(optarg, &opts->bit_rate)) { - return false; - } - break; - case 'c': - LOGW("Deprecated option -c. Use --crop instead."); - // fall through - case OPT_CROP: - opts->crop = optarg; - break; - case 'f': - opts->fullscreen = true; - break; - case 'F': - 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, &opts->max_size)) { - return false; - } - break; - case 'n': - opts->control = false; - break; - case 'N': - opts->display = false; - break; - case 'p': - if (!parse_port(optarg, &opts->port)) { - return false; - } - break; - case 'r': - opts->record_filename = optarg; - break; - case 's': - opts->serial = optarg; - break; - case 'S': - opts->turn_screen_off = true; - break; - case 't': - opts->show_touches = true; - break; - case 'T': - 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: - opts->render_expired_frames = true; - break; - case OPT_WINDOW_TITLE: - opts->window_title = optarg; - break; - case OPT_WINDOW_X: - if (!parse_window_position(optarg, &opts->window_x)) { - return false; - } - break; - case OPT_WINDOW_Y: - if (!parse_window_position(optarg, &opts->window_y)) { - return false; - } - break; - case OPT_WINDOW_WIDTH: - if (!parse_window_dimension(optarg, &opts->window_width)) { - return false; - } - break; - case OPT_WINDOW_HEIGHT: - if (!parse_window_dimension(optarg, &opts->window_height)) { - return false; - } - break; - case OPT_WINDOW_BORDERLESS: - opts->window_borderless = true; - break; - case OPT_PUSH_TARGET: - opts->push_target = optarg; - break; - case OPT_PREFER_TEXT: - opts->prefer_text = true; - break; - default: - // getopt prints the error message on stderr - return false; - } - } - - if (!opts->display && !opts->record_filename) { - LOGE("-N/--no-display requires screen recording (-r/--record)"); - return false; - } - - if (!opts->display && opts->fullscreen) { - LOGE("-f/--fullscreen-window is incompatible with -N/--no-display"); - return false; - } - - int index = optind; - if (index < argc) { - LOGE("Unexpected additional argument: %s", argv[index]); - return false; - } - - if (opts->record_format && !opts->record_filename) { - LOGE("Record format specified without recording"); - return false; - } - - 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)", - opts->record_filename); - return false; - } - } - - if (!opts->control && opts->turn_screen_off) { - LOGE("Could not request to turn screen off if control is disabled"); - return false; - } - - return true; -} - int main(int argc, char *argv[]) { #ifdef __WINDOWS__ @@ -595,18 +37,23 @@ main(int argc, char *argv[]) { setbuf(stdout, NULL); setbuf(stderr, NULL); #endif - struct args args = { + +#ifndef NDEBUG + SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); +#endif + + struct scrcpy_cli_args args = { .opts = SCRCPY_OPTIONS_DEFAULT, .help = false, .version = false, }; - if (!parse_args(&args, argc, argv)) { + if (!scrcpy_parse_args(&args, argc, argv)) { return 1; } if (args.help) { - usage(argv[0]); + scrcpy_print_usage(argv[0]); return 0; } @@ -625,10 +72,6 @@ main(int argc, char *argv[]) { return 1; } -#ifdef BUILD_DEBUG - SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); -#endif - int res = scrcpy(&args.opts) ? 0 : 1; avformat_network_deinit(); // ignore failure diff --git a/app/src/receiver.c b/app/src/receiver.c index 1c80bb00..0474ff55 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -1,12 +1,12 @@ #include "receiver.h" -#include +#include #include #include "config.h" #include "device_msg.h" -#include "lock_util.h" -#include "log.h" +#include "util/lock.h" +#include "util/log.h" bool receiver_init(struct receiver *receiver, socket_t control_socket) { @@ -23,7 +23,7 @@ receiver_destroy(struct receiver *receiver) { } static void -process_msg(struct receiver *receiver, struct device_msg *msg) { +process_msg(struct device_msg *msg) { switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: LOGI("Device clipboard copied"); @@ -33,7 +33,7 @@ process_msg(struct receiver *receiver, struct device_msg *msg) { } static ssize_t -process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) { +process_msgs(const unsigned char *buf, size_t len) { size_t head = 0; for (;;) { struct device_msg msg; @@ -45,11 +45,11 @@ process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) { return head; } - process_msg(receiver, &msg); + process_msg(&msg); device_msg_destroy(&msg); head += r; - SDL_assert(head <= len); + assert(head <= len); if (head == len) { return head; } @@ -64,7 +64,7 @@ run_receiver(void *data) { size_t head = 0; for (;;) { - SDL_assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE); + assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE); ssize_t r = net_recv(receiver->control_socket, buf, DEVICE_MSG_SERIALIZED_MAX_SIZE - head); if (r <= 0) { @@ -72,7 +72,7 @@ run_receiver(void *data) { break; } - ssize_t consumed = process_msgs(receiver, buf, r); + ssize_t consumed = process_msgs(buf, r); if (consumed == -1) { // an error occurred break; diff --git a/app/src/receiver.h b/app/src/receiver.h index 6108e545..8387903b 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -6,7 +6,7 @@ #include #include "config.h" -#include "net.h" +#include "util/net.h" // receive events from the device // managed by the controller diff --git a/app/src/recorder.c b/app/src/recorder.c index f6f6fd96..465b24e8 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -1,12 +1,12 @@ #include "recorder.h" +#include #include -#include #include "config.h" #include "compat.h" -#include "lock_util.h" -#include "log.h" +#include "util/lock.h" +#include "util/log.h" static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us @@ -116,7 +116,7 @@ recorder_get_format_name(enum recorder_format format) { bool recorder_open(struct recorder *recorder, const AVCodec *input_codec) { const char *format_name = recorder_get_format_name(recorder->format); - SDL_assert(format_name); + assert(format_name); const AVOutputFormat *format = find_muxer(format_name); if (!format) { LOGE("Could not find muxer"); @@ -357,7 +357,7 @@ recorder_join(struct recorder *recorder) { bool recorder_push(struct recorder *recorder, const AVPacket *packet) { mutex_lock(recorder->mutex); - SDL_assert(!recorder->stopped); + assert(!recorder->stopped); if (recorder->failed) { // reject any new packet (this will stop the stream) diff --git a/app/src/recorder.h b/app/src/recorder.h index 4ad77197..4f5d526c 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -8,7 +8,7 @@ #include "config.h" #include "common.h" -#include "queue.h" +#include "util/queue.h" enum recorder_format { RECORDER_FORMAT_AUTO, diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 67f1de16..17be1ed4 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -18,15 +18,15 @@ #include "file_handler.h" #include "fps_counter.h" #include "input_manager.h" -#include "log.h" -#include "lock_util.h" -#include "net.h" #include "recorder.h" #include "screen.h" #include "server.h" #include "stream.h" #include "tiny_xpm.h" #include "video_buffer.h" +#include "util/lock.h" +#include "util/log.h" +#include "util/net.h" static struct server server = SERVER_INITIALIZER; static struct screen screen = SCREEN_INITIALIZER; @@ -103,6 +103,7 @@ sdl_init_and_configure(bool display) { // static int event_watcher(void *data, SDL_Event *event) { + (void) data; if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_RESIZED) { // called from another thread, not very safe, but it's a workaround! @@ -201,6 +202,7 @@ handle_event(SDL_Event *event, bool control) { static bool event_loop(bool display, bool control) { + (void) display; #ifdef CONTINUOUS_RESIZING_WORKAROUND if (display) { SDL_AddEventWatch(event_watcher, NULL); @@ -256,6 +258,7 @@ sdl_priority_from_av_level(int level) { static void av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { + (void) avcl; SDL_LogPriority priority = sdl_priority_from_av_level(level); if (priority == 0) { return; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 8723f29f..75de8717 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -42,7 +42,7 @@ struct scrcpy_options { .push_target = NULL, \ .record_format = RECORDER_FORMAT_AUTO, \ .port = DEFAULT_LOCAL_PORT, \ - .max_size = DEFAULT_LOCAL_PORT, \ + .max_size = DEFAULT_MAX_SIZE, \ .bit_rate = DEFAULT_BIT_RATE, \ .max_fps = 0, \ .window_x = -1, \ diff --git a/app/src/screen.c b/app/src/screen.c index ab4d434e..beb10754 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -1,5 +1,6 @@ #include "screen.h" +#include #include #include @@ -7,10 +8,10 @@ #include "common.h" #include "compat.h" #include "icon.xpm" -#include "lock_util.h" -#include "log.h" #include "tiny_xpm.h" #include "video_buffer.h" +#include "util/lock.h" +#include "util/log.h" #define DISPLAY_MARGINS 96 @@ -110,7 +111,7 @@ get_optimal_size(struct size current_size, struct size frame_size) { } // w and h must fit into 16 bits - SDL_assert_release(w < 0x10000 && h < 0x10000); + assert(w < 0x10000 && h < 0x10000); return (struct size) {w, h}; } @@ -185,8 +186,8 @@ screen_init_rendering(struct screen *screen, const char *window_title, window_flags |= SDL_WINDOW_BORDERLESS; } - int x = window_x != -1 ? window_x : SDL_WINDOWPOS_UNDEFINED; - int y = window_y != -1 ? window_y : SDL_WINDOWPOS_UNDEFINED; + int x = window_x != -1 ? window_x : (int) SDL_WINDOWPOS_UNDEFINED; + int y = window_y != -1 ? window_y : (int) SDL_WINDOWPOS_UNDEFINED; screen->window = SDL_CreateWindow(window_title, x, y, window_size.width, window_size.height, window_flags); @@ -392,8 +393,8 @@ screen_handle_window_event(struct screen *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); + assert(screen->windowed_window_size_backup.width); + 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 diff --git a/app/src/server.c b/app/src/server.c index b37b39d0..ff167aeb 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -1,22 +1,22 @@ #include "server.h" +#include #include #include #include #include -#include #include #include "config.h" #include "command.h" -#include "log.h" -#include "net.h" +#include "util/log.h" +#include "util/net.h" #define SOCKET_NAME "scrcpy" #define SERVER_FILENAME "scrcpy-server" #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME -#define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME +#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" static const char * get_server_path(void) { @@ -67,7 +67,12 @@ get_server_path(void) { static bool push_server(const char *serial) { - process_t process = adb_push(serial, get_server_path(), DEVICE_SERVER_PATH); + const char *server_path = get_server_path(); + if (!is_regular_file(server_path)) { + LOGE("'%s' does not exist or is not a regular file\n", server_path); + return false; + } + process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH); return process_check_success(process, "adb push"); } @@ -124,7 +129,7 @@ execute_server(struct server *server, const struct server_params *params) { sprintf(max_fps_string, "%"PRIu16, params->max_fps); const char *const cmd[] = { "shell", - "CLASSPATH=/data/local/tmp/" SERVER_FILENAME, + "CLASSPATH=" DEVICE_SERVER_PATH, "app_process", #ifdef SERVER_DEBUGGER # define SERVER_DEBUGGER_PORT "5005" @@ -199,7 +204,7 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { static void close_socket(socket_t *socket) { - SDL_assert(*socket != INVALID_SOCKET); + assert(*socket != INVALID_SOCKET); net_shutdown(*socket, SHUT_RDWR); if (!net_close(*socket)) { LOGW("Could not close socket"); @@ -323,7 +328,7 @@ server_stop(struct server *server) { close_socket(&server->control_socket); } - SDL_assert(server->process != PROCESS_NONE); + assert(server->process != PROCESS_NONE); if (!cmd_terminate(server->process)) { LOGW("Could not terminate server"); diff --git a/app/src/server.h b/app/src/server.h index f46ced19..0cb1ab3a 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -6,7 +6,7 @@ #include "config.h" #include "command.h" -#include "net.h" +#include "util/net.h" struct server { char *serial; diff --git a/app/src/stream.c b/app/src/stream.c index 6c3192f2..dd2dbd76 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -1,8 +1,8 @@ #include "stream.h" +#include #include #include -#include #include #include #include @@ -10,12 +10,11 @@ #include "config.h" #include "compat.h" -#include "buffer_util.h" #include "decoder.h" #include "events.h" -#include "lock_util.h" -#include "log.h" #include "recorder.h" +#include "util/buffer_util.h" +#include "util/log.h" #define BUFSIZE 0x10000 @@ -44,7 +43,8 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) { uint64_t pts = buffer_read64be(header); uint32_t len = buffer_read32be(&header[8]); - SDL_assert(len); + assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0); + assert(len); if (av_new_packet(packet, len)) { LOGE("Could not allocate packet"); @@ -52,12 +52,12 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) { } r = net_recv_all(stream->socket, packet->data, len); - if (r < len) { + if (r < 0 || ((uint32_t) r) < len) { av_packet_unref(packet); return false; } - packet->pts = pts != NO_PTS ? pts : AV_NOPTS_VALUE; + packet->pts = pts != NO_PTS ? (int64_t) pts : AV_NOPTS_VALUE; return true; } @@ -107,8 +107,9 @@ stream_parse(struct stream *stream, AVPacket *packet) { AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1); // PARSER_FLAG_COMPLETE_FRAMES is set - SDL_assert(r == in_len); - SDL_assert(out_len == in_len); + assert(r == in_len); + (void) r; + assert(out_len == in_len); if (stream->parser->key_frame == 1) { packet->flags |= AV_PKT_FLAG_KEY; diff --git a/app/src/stream.h b/app/src/stream.h index cb50468e..f7c5e475 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -8,7 +8,7 @@ #include #include "config.h" -#include "net.h" +#include "util/net.h" struct video_buffer; diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index 6a3a5a47..fbcf2355 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -17,10 +17,11 @@ #include #include #include -#include "log.h" + +#include "util/log.h" enum process_result -cmd_execute(const char *path, const char *const argv[], pid_t *pid) { +cmd_execute(const char *const argv[], pid_t *pid) { int fd[2]; if (pipe(fd) == -1) { @@ -51,7 +52,7 @@ cmd_execute(const char *path, const char *const argv[], pid_t *pid) { // child close read side close(fd[0]); if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) { - execvp(path, (char *const *)argv); + execvp(argv[0], (char *const *)argv); if (errno == ENOENT) { ret = PROCESS_ERROR_MISSING_BINARY; } else { diff --git a/app/src/sys/unix/net.c b/app/src/sys/unix/net.c index d940f3bb..d67a660f 100644 --- a/app/src/sys/unix/net.c +++ b/app/src/sys/unix/net.c @@ -1,4 +1,4 @@ -#include "net.h" +#include "util/net.h" #include diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c index f23730a0..55edaf8f 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/command.c @@ -1,8 +1,8 @@ #include "command.h" #include "config.h" -#include "log.h" -#include "str_util.h" +#include "util/log.h" +#include "util/str_util.h" static int build_cmd(char *cmd, size_t len, const char *const argv[]) { @@ -19,7 +19,7 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) { } enum process_result -cmd_execute(const char *path, const char *const argv[], HANDLE *handle) { +cmd_execute(const char *const argv[], HANDLE *handle) { STARTUPINFOW si; PROCESS_INFORMATION pi; memset(&si, 0, sizeof(si)); diff --git a/app/src/sys/win/net.c b/app/src/sys/win/net.c index 55519782..aebce7fc 100644 --- a/app/src/sys/win/net.c +++ b/app/src/sys/win/net.c @@ -1,7 +1,7 @@ -#include "net.h" +#include "util/net.h" #include "config.h" -#include "log.h" +#include "util/log.h" bool net_init(void) { diff --git a/app/src/tiny_xpm.c b/app/src/tiny_xpm.c index 5ea89078..feb3d1cb 100644 --- a/app/src/tiny_xpm.c +++ b/app/src/tiny_xpm.c @@ -1,12 +1,13 @@ #include "tiny_xpm.h" +#include #include #include #include #include #include "config.h" -#include "log.h" +#include "util/log.h" struct index { char c; @@ -36,7 +37,7 @@ find_color(struct index *index, int len, char c, uint32_t *color) { // (non-const) "char *" SDL_Surface * read_xpm(char *xpm[]) { -#if SDL_ASSERT_LEVEL >= 2 +#ifndef NDEBUG // patch the XPM to change the icon color in debug mode xpm[2] = ". c #CC00CC"; #endif @@ -51,24 +52,26 @@ read_xpm(char *xpm[]) { int chars = strtol(endptr + 1, &endptr, 10); // sanity checks - SDL_assert(0 <= width && width < 256); - SDL_assert(0 <= height && height < 256); - SDL_assert(0 <= colors && colors < 256); - SDL_assert(chars == 1); // this implementation does not support more + assert(0 <= width && width < 256); + assert(0 <= height && height < 256); + assert(0 <= colors && colors < 256); + assert(chars == 1); // this implementation does not support more + + (void) chars; // init index struct index index[colors]; for (int i = 0; i < colors; ++i) { const char *line = xpm[1+i]; index[i].c = line[0]; - SDL_assert(line[1] == '\t'); - SDL_assert(line[2] == 'c'); - SDL_assert(line[3] == ' '); + assert(line[1] == '\t'); + assert(line[2] == 'c'); + assert(line[3] == ' '); if (line[4] == '#') { index[i].color = 0xff000000 | strtol(&line[5], &endptr, 0x10); - SDL_assert(*endptr == '\0'); + assert(*endptr == '\0'); } else { - SDL_assert(!strcmp("None", &line[4])); + assert(!strcmp("None", &line[4])); index[i].color = 0; } } @@ -85,7 +88,8 @@ read_xpm(char *xpm[]) { char c = line[x]; uint32_t color; bool color_found = find_color(index, colors, c, &color); - SDL_assert(color_found); + assert(color_found); + (void) color_found; pixels[y * width + x] = color; } } diff --git a/app/src/buffer_util.h b/app/src/util/buffer_util.h similarity index 93% rename from app/src/buffer_util.h rename to app/src/util/buffer_util.h index 262df1dc..17234e42 100644 --- a/app/src/buffer_util.h +++ b/app/src/util/buffer_util.h @@ -36,8 +36,8 @@ buffer_read32be(const uint8_t *buf) { return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; } -static inline -uint64_t buffer_read64be(const uint8_t *buf) { +static inline uint64_t +buffer_read64be(const uint8_t *buf) { uint32_t msb = buffer_read32be(buf); uint32_t lsb = buffer_read32be(&buf[4]); return ((uint64_t) msb << 32) | lsb; diff --git a/app/src/cbuf.h b/app/src/util/cbuf.h similarity index 100% rename from app/src/cbuf.h rename to app/src/util/cbuf.h diff --git a/app/src/util/lock.h b/app/src/util/lock.h new file mode 100644 index 00000000..cb7c318c --- /dev/null +++ b/app/src/util/lock.h @@ -0,0 +1,74 @@ +#ifndef LOCK_H +#define LOCK_H + +#include +#include + +#include "config.h" +#include "log.h" + +static inline void +mutex_lock(SDL_mutex *mutex) { + int r = SDL_LockMutex(mutex); +#ifndef NDEBUG + if (r) { + LOGC("Could not lock mutex: %s", SDL_GetError()); + abort(); + } +#else + (void) r; +#endif +} + +static inline void +mutex_unlock(SDL_mutex *mutex) { + int r = SDL_UnlockMutex(mutex); +#ifndef NDEBUG + if (r) { + LOGC("Could not unlock mutex: %s", SDL_GetError()); + abort(); + } +#else + (void) r; +#endif +} + +static inline void +cond_wait(SDL_cond *cond, SDL_mutex *mutex) { + int r = SDL_CondWait(cond, mutex); +#ifndef NDEBUG + if (r) { + LOGC("Could not wait on condition: %s", SDL_GetError()); + abort(); + } +#else + (void) r; +#endif +} + +static inline int +cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) { + int r = SDL_CondWaitTimeout(cond, mutex, ms); +#ifndef NDEBUG + if (r < 0) { + LOGC("Could not wait on condition with timeout: %s", SDL_GetError()); + abort(); + } +#endif + return r; +} + +static inline void +cond_signal(SDL_cond *cond) { + int r = SDL_CondSignal(cond); +#ifndef NDEBUG + if (r) { + LOGC("Could not signal a condition: %s", SDL_GetError()); + abort(); + } +#else + (void) r; +#endif +} + +#endif diff --git a/app/src/log.h b/app/src/util/log.h similarity index 100% rename from app/src/log.h rename to app/src/util/log.h diff --git a/app/src/net.c b/app/src/util/net.c similarity index 100% rename from app/src/net.c rename to app/src/util/net.c diff --git a/app/src/net.h b/app/src/util/net.h similarity index 100% rename from app/src/net.h rename to app/src/util/net.h diff --git a/app/src/queue.h b/app/src/util/queue.h similarity index 96% rename from app/src/queue.h rename to app/src/util/queue.h index 6cf7aba6..12bc9e89 100644 --- a/app/src/queue.h +++ b/app/src/util/queue.h @@ -2,9 +2,9 @@ #ifndef QUEUE_H #define QUEUE_H +#include #include #include -#include #include "config.h" @@ -67,7 +67,7 @@ // type so that we can "return" it) #define queue_take(PQ, NEXTFIELD, PITEM) \ (void) ({ \ - SDL_assert(!queue_is_empty(PQ)); \ + assert(!queue_is_empty(PQ)); \ *(PITEM) = (PQ)->first; \ (PQ)->first = (PQ)->first->NEXTFIELD; \ }) diff --git a/app/src/str_util.c b/app/src/util/str_util.c similarity index 67% rename from app/src/str_util.c rename to app/src/util/str_util.c index 15378d8a..4d175407 100644 --- a/app/src/str_util.c +++ b/app/src/util/str_util.c @@ -1,5 +1,7 @@ #include "str_util.h" +#include +#include #include #include @@ -60,6 +62,59 @@ strquote(const char *src) { return quoted; } +bool +parse_integer(const char *s, long *out) { + char *endptr; + if (*s == '\0') { + return false; + } + errno = 0; + long value = strtol(s, &endptr, 0); + if (errno == ERANGE) { + return false; + } + if (*endptr != '\0') { + return false; + } + + *out = value; + return true; +} + +bool +parse_integer_with_suffix(const char *s, long *out) { + char *endptr; + if (*s == '\0') { + return false; + } + errno = 0; + long value = strtol(s, &endptr, 0); + if (errno == ERANGE) { + return false; + } + int mul = 1; + if (*endptr != '\0') { + if (s == endptr) { + return false; + } + if ((*endptr == 'M' || *endptr == 'm') && endptr[1] == '\0') { + mul = 1000000; + } else if ((*endptr == 'K' || *endptr == 'k') && endptr[1] == '\0') { + mul = 1000; + } else { + return false; + } + } + + if ((value < 0 && LONG_MIN / mul > value) || + (value > 0 && LONG_MAX / mul < value)) { + return false; + } + + *out = value * mul; + return true; +} + size_t utf8_truncation_index(const char *utf8, size_t max_len) { size_t len = strlen(utf8); diff --git a/app/src/str_util.h b/app/src/util/str_util.h similarity index 73% rename from app/src/str_util.h rename to app/src/util/str_util.h index 56490190..8d9b990c 100644 --- a/app/src/str_util.h +++ b/app/src/util/str_util.h @@ -1,6 +1,7 @@ #ifndef STRUTIL_H #define STRUTIL_H +#include #include #include "config.h" @@ -25,6 +26,18 @@ xstrjoin(char *dst, const char *const tokens[], char sep, size_t n); char * strquote(const char *src); +// parse s as an integer into value +// returns true if the conversion succeeded, false otherwise +bool +parse_integer(const char *s, long *out); + +// parse s as an integer into value +// like parse_integer(), but accept 'k'/'K' (x1000) and 'm'/'M' (x1000000) as +// suffix +// returns true if the conversion succeeded, false otherwise +bool +parse_integer_with_suffix(const char *s, long *out); + // return the index to truncate a UTF-8 string at a valid position size_t utf8_truncation_index(const char *utf8, size_t max_len); diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 2b5f1c2f..629680d9 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -1,13 +1,13 @@ #include "video_buffer.h" -#include +#include #include #include #include #include "config.h" -#include "lock_util.h" -#include "log.h" +#include "util/lock.h" +#include "util/log.h" bool video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, @@ -91,7 +91,7 @@ video_buffer_offer_decoded_frame(struct video_buffer *vb, const AVFrame * video_buffer_consume_rendered_frame(struct video_buffer *vb) { - SDL_assert(!vb->rendering_frame_consumed); + assert(!vb->rendering_frame_consumed); vb->rendering_frame_consumed = true; fps_counter_add_rendered_frame(vb->fps_counter); if (vb->render_expired_frames) { diff --git a/app/tests/test_buffer_util.c b/app/tests/test_buffer_util.c new file mode 100644 index 00000000..ba3f9f06 --- /dev/null +++ b/app/tests/test_buffer_util.c @@ -0,0 +1,76 @@ +#include + +#include "util/buffer_util.h" + +static void test_buffer_write16be(void) { + uint16_t val = 0xABCD; + uint8_t buf[2]; + + buffer_write16be(buf, val); + + assert(buf[0] == 0xAB); + assert(buf[1] == 0xCD); +} + +static void test_buffer_write32be(void) { + uint32_t val = 0xABCD1234; + uint8_t buf[4]; + + buffer_write32be(buf, val); + + assert(buf[0] == 0xAB); + assert(buf[1] == 0xCD); + assert(buf[2] == 0x12); + assert(buf[3] == 0x34); +} + +static void test_buffer_write64be(void) { + uint64_t val = 0xABCD1234567890EF; + uint8_t buf[8]; + + buffer_write64be(buf, val); + + assert(buf[0] == 0xAB); + assert(buf[1] == 0xCD); + assert(buf[2] == 0x12); + assert(buf[3] == 0x34); + assert(buf[4] == 0x56); + assert(buf[5] == 0x78); + assert(buf[6] == 0x90); + assert(buf[7] == 0xEF); +} + +static void test_buffer_read16be(void) { + uint8_t buf[2] = {0xAB, 0xCD}; + + uint16_t val = buffer_read16be(buf); + + assert(val == 0xABCD); +} + +static void test_buffer_read32be(void) { + uint8_t buf[4] = {0xAB, 0xCD, 0x12, 0x34}; + + uint32_t val = buffer_read32be(buf); + + assert(val == 0xABCD1234); +} + +static void test_buffer_read64be(void) { + uint8_t buf[8] = {0xAB, 0xCD, 0x12, 0x34, + 0x56, 0x78, 0x90, 0xEF}; + + uint64_t val = buffer_read64be(buf); + + assert(val == 0xABCD1234567890EF); +} + +int main(void) { + test_buffer_write16be(); + test_buffer_write32be(); + test_buffer_write64be(); + test_buffer_read16be(); + test_buffer_read32be(); + test_buffer_read64be(); + return 0; +} diff --git a/app/tests/test_cbuf.c b/app/tests/test_cbuf.c index 9d5fdc27..dbe50aab 100644 --- a/app/tests/test_cbuf.c +++ b/app/tests/test_cbuf.c @@ -1,7 +1,7 @@ #include #include -#include "cbuf.h" +#include "util/cbuf.h" struct int_queue CBUF(int, 32); diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c new file mode 100644 index 00000000..539c3c94 --- /dev/null +++ b/app/tests/test_cli.c @@ -0,0 +1,128 @@ +#include + +#include "cli.h" +#include "common.h" + +static void test_flag_version(void) { + struct scrcpy_cli_args args = { + .opts = SCRCPY_OPTIONS_DEFAULT, + .help = false, + .version = false, + }; + + char *argv[] = {"scrcpy", "-v"}; + + bool ok = scrcpy_parse_args(&args, 2, argv); + assert(ok); + assert(!args.help); + assert(args.version); +} + +static void test_flag_help(void) { + struct scrcpy_cli_args args = { + .opts = SCRCPY_OPTIONS_DEFAULT, + .help = false, + .version = false, + }; + + char *argv[] = {"scrcpy", "-v"}; + + bool ok = scrcpy_parse_args(&args, 2, argv); + assert(ok); + assert(!args.help); + assert(args.version); +} + +static void test_options(void) { + struct scrcpy_cli_args args = { + .opts = SCRCPY_OPTIONS_DEFAULT, + .help = false, + .version = false, + }; + + char *argv[] = { + "scrcpy", + "--always-on-top", + "--bit-rate", "5M", + "--crop", "100:200:300:400", + "--fullscreen", + "--max-fps", "30", + "--max-size", "1024", + // "--no-control" is not compatible with "--turn-screen-off" + // "--no-display" is not compatible with "--fulscreen" + "--port", "1234", + "--push-target", "/sdcard/Movies", + "--record", "file", + "--record-format", "mkv", + "--render-expired-frames", + "--serial", "0123456789abcdef", + "--show-touches", + "--turn-screen-off", + "--prefer-text", + "--window-title", "my device", + "--window-x", "100", + "--window-y", "-1", + "--window-width", "600", + "--window-height", "0", + "--window-borderless", + }; + + bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv); + assert(ok); + + const struct scrcpy_options *opts = &args.opts; + assert(opts->always_on_top); + fprintf(stderr, "%d\n", (int) opts->bit_rate); + assert(opts->bit_rate == 5000000); + assert(!strcmp(opts->crop, "100:200:300:400")); + assert(opts->fullscreen); + assert(opts->max_fps == 30); + assert(opts->max_size == 1024); + assert(opts->port == 1234); + assert(!strcmp(opts->push_target, "/sdcard/Movies")); + assert(!strcmp(opts->record_filename, "file")); + assert(opts->record_format == RECORDER_FORMAT_MKV); + assert(opts->render_expired_frames); + assert(!strcmp(opts->serial, "0123456789abcdef")); + assert(opts->show_touches); + assert(opts->turn_screen_off); + assert(opts->prefer_text); + assert(!strcmp(opts->window_title, "my device")); + assert(opts->window_x == 100); + assert(opts->window_y == -1); + assert(opts->window_width == 600); + assert(opts->window_height == 0); + assert(opts->window_borderless); +} + +static void test_options2(void) { + struct scrcpy_cli_args args = { + .opts = SCRCPY_OPTIONS_DEFAULT, + .help = false, + .version = false, + }; + + char *argv[] = { + "scrcpy", + "--no-control", + "--no-display", + "--record", "file.mp4", // cannot enable --no-display without recording + }; + + bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv); + assert(ok); + + const struct scrcpy_options *opts = &args.opts; + assert(!opts->control); + assert(!opts->display); + assert(!strcmp(opts->record_filename, "file.mp4")); + assert(opts->record_format == RECORDER_FORMAT_MP4); +} + +int main(void) { + test_flag_version(); + test_flag_help(); + test_options(); + test_options2(); + return 0; +}; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 83ab011f..d6f556f3 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -236,6 +236,21 @@ static void test_serialize_set_screen_power_mode(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_rotate_device(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_ROTATE_DEVICE, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 1); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_ROTATE_DEVICE, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(void) { test_serialize_inject_keycode(); test_serialize_inject_text(); @@ -248,5 +263,6 @@ int main(void) { test_serialize_get_clipboard(); test_serialize_set_clipboard(); test_serialize_set_screen_power_mode(); + test_serialize_rotate_device(); return 0; } diff --git a/app/tests/test_queue.c b/app/tests/test_queue.c index bcbced2b..b0950bb0 100644 --- a/app/tests/test_queue.c +++ b/app/tests/test_queue.c @@ -1,6 +1,6 @@ #include -#include +#include "util/queue.h" struct foo { int value; diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index 18ac4a7d..200e0f63 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -1,7 +1,10 @@ #include +#include +#include #include +#include -#include "str_util.h" +#include "util/str_util.h" static void test_xstrncpy_simple(void) { char s[] = "xxxxxxxxxx"; @@ -126,6 +129,16 @@ static void test_xstrjoin_truncated_after_sep(void) { assert(!strcmp("abc de ", s)); } +static void test_strquote(void) { + const char *s = "abcde"; + char *out = strquote(s); + + // add '"' at the beginning and the end + assert(!strcmp("\"abcde\"", out)); + + SDL_free(out); +} + static void test_utf8_truncate(void) { const char *s = "aÉbÔc"; assert(strlen(s) == 7); // É and Ô are 2 bytes-wide @@ -157,6 +170,73 @@ static void test_utf8_truncate(void) { assert(count == 7); // no more chars } +static void test_parse_integer(void) { + long value; + bool ok = parse_integer("1234", &value); + assert(ok); + assert(value == 1234); + + ok = parse_integer("-1234", &value); + assert(ok); + assert(value == -1234); + + ok = parse_integer("1234k", &value); + assert(!ok); + + ok = parse_integer("123456789876543212345678987654321", &value); + assert(!ok); // out-of-range +} + +static void test_parse_integer_with_suffix(void) { + long value; + bool ok = parse_integer_with_suffix("1234", &value); + assert(ok); + assert(value == 1234); + + ok = parse_integer_with_suffix("-1234", &value); + assert(ok); + assert(value == -1234); + + ok = parse_integer_with_suffix("1234k", &value); + assert(ok); + assert(value == 1234000); + + ok = parse_integer_with_suffix("1234m", &value); + assert(ok); + assert(value == 1234000000); + + ok = parse_integer_with_suffix("-1234k", &value); + assert(ok); + assert(value == -1234000); + + ok = parse_integer_with_suffix("-1234m", &value); + assert(ok); + assert(value == -1234000000); + + ok = parse_integer_with_suffix("123456789876543212345678987654321", &value); + assert(!ok); // out-of-range + + char buf[32]; + + sprintf(buf, "%ldk", LONG_MAX / 2000); + ok = parse_integer_with_suffix(buf, &value); + assert(ok); + assert(value == LONG_MAX / 2000 * 1000); + + sprintf(buf, "%ldm", LONG_MAX / 2000); + ok = parse_integer_with_suffix(buf, &value); + assert(!ok); + + sprintf(buf, "%ldk", LONG_MIN / 2000); + ok = parse_integer_with_suffix(buf, &value); + assert(ok); + assert(value == LONG_MIN / 2000 * 1000); + + sprintf(buf, "%ldm", LONG_MIN / 2000); + ok = parse_integer_with_suffix(buf, &value); + assert(!ok); +} + int main(void) { test_xstrncpy_simple(); test_xstrncpy_just_fit(); @@ -166,6 +246,9 @@ int main(void) { test_xstrjoin_truncated_in_token(); test_xstrjoin_truncated_before_sep(); test_xstrjoin_truncated_after_sep(); + test_strquote(); test_utf8_truncate(); + test_parse_integer(); + test_parse_integer_with_suffix(); return 0; } diff --git a/meson.build b/meson.build index ba19d7ee..412c9c51 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,10 @@ project('scrcpy', 'c', - version: '1.11', + version: '1.12.1', meson_version: '>= 0.37', - default_options: 'c_std=c11') + default_options: [ + 'c_std=c11', + 'warning_level=2', + ]) if get_option('compile_app') subdir('app') diff --git a/server/build.gradle b/server/build.gradle index 0804a8bd..539a97b8 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 12 - versionName "1.11" + versionCode 14 + versionName "1.12.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index fcd6233e..c117d89c 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=1.11 +SCRCPY_VERSION_NAME=1.12.1 PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} @@ -40,7 +40,7 @@ EOF echo "Generating java from aidl..." cd "$SERVER_DIR/src/main/aidl" -"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o "$CLASSES_DIR" \ +"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \ android/view/IRotationWatcher.aidl echo "Compiling java sources..." diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 30c05a3b..195b04bf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -15,6 +15,7 @@ public final class ControlMessage { public static final int TYPE_GET_CLIPBOARD = 7; public static final int TYPE_SET_CLIPBOARD = 8; public static final int TYPE_SET_SCREEN_POWER_MODE = 9; + public static final int TYPE_ROTATE_DEVICE = 10; private int type; private String text; @@ -47,8 +48,7 @@ public final class ControlMessage { return msg; } - public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, - int buttons) { + public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_TOUCH_EVENT; msg.action = action; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 2f8b5177..726b5659 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -76,6 +76,7 @@ public class ControlMessageReader { case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: case ControlMessage.TYPE_GET_CLIPBOARD: + case ControlMessage.TYPE_ROTATE_DEVICE: msg = ControlMessage.createEmpty(type); break; default: diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index ce02e333..dc0fa67b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -13,6 +13,8 @@ import java.io.IOException; public class Controller { + private static final int DEVICE_ID_VIRTUAL = -1; + private final Device device; private final DesktopConnection connection; private final DeviceMessageSender sender; @@ -21,10 +23,8 @@ public class Controller { 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]; + 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; @@ -106,6 +106,9 @@ public class Controller { case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: device.setScreenPowerMode(msg.getAction()); break; + case ControlMessage.TYPE_ROTATE_DEVICE: + device.rotateDevice(); + break; default: // do nothing } @@ -176,8 +179,9 @@ public class Controller { } } - MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, - pointerCoords, 0, buttons, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); + MotionEvent event = MotionEvent + .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0, + InputDevice.SOURCE_TOUCHSCREEN, 0); return injectEvent(event); } @@ -198,15 +202,16 @@ public class Controller { 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); + MotionEvent event = MotionEvent + .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 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); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 708b9516..9448098a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; +import com.genymobile.scrcpy.wrappers.WindowManager; import android.graphics.Rect; import android.os.Build; @@ -170,6 +171,27 @@ public final class Device { Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); } + /** + * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). + */ + public void rotateDevice() { + WindowManager wm = serviceManager.getWindowManager(); + + boolean accelerometerRotation = !wm.isRotationFrozen(); + + int currentRotation = wm.getRotation(); + int newRotation = (currentRotation & 1) ^ 1; // 0->1, 1->0, 2->1, 3->0 + String newRotationString = newRotation == 0 ? "portrait" : "landscape"; + + Ln.i("Device rotation requested: " + newRotationString); + wm.freezeRotation(newRotation); + + // restore auto-rotate if necessary + if (accelerometerRotation) { + wm.thawRotation(); + } + } + static Rect flipRect(Rect crop) { return new Rect(crop.top, crop.left, crop.bottom, crop.right); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index bb741225..26f13a56 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -12,10 +12,7 @@ public final class Ln { private static final String PREFIX = "[server] "; enum Level { - DEBUG, - INFO, - WARN, - ERROR; + DEBUG, INFO, WARN, ERROR } private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO; diff --git a/server/src/main/java/com/genymobile/scrcpy/Point.java b/server/src/main/java/com/genymobile/scrcpy/Point.java index 9ef2db03..c2a30fa8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Point.java +++ b/server/src/main/java/com/genymobile/scrcpy/Point.java @@ -28,8 +28,7 @@ public class Point { return false; } Point point = (Point) o; - return x == point.x - && y == point.y; + return x == point.x && y == point.y; } @Override @@ -39,9 +38,6 @@ public class Point { @Override public String toString() { - return "Point{" - + "x=" + x - + ", y=" + y - + '}'; + return "Point{" + "x=" + x + ", y=" + y + '}'; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/Position.java index 757fa36e..b46d2f73 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Position.java +++ b/server/src/main/java/com/genymobile/scrcpy/Position.java @@ -32,8 +32,7 @@ public class Position { return false; } Position position = (Position) o; - return Objects.equals(point, position.point) - && Objects.equals(screenSize, position.screenSize); + return Objects.equals(point, position.point) && Objects.equals(screenSize, position.screenSize); } @Override @@ -43,10 +42,7 @@ public class Position { @Override public String toString() { - return "Position{" - + "point=" + point - + ", screenSize=" + screenSize - + '}'; + return "Position{" + "point=" + point + ", screenSize=" + screenSize + '}'; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index ad14e5d8..56b738fb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,13 +1,15 @@ package com.genymobile.scrcpy; import android.graphics.Rect; +import android.media.MediaCodec; +import android.os.Build; import java.io.File; import java.io.IOException; public final class Server { - private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server"; + private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; private Server() { // not instantiable @@ -73,8 +75,8 @@ public final class Server { String clientVersion = args[0]; if (!clientVersion.equals(BuildConfig.VERSION_NAME)) { - throw new IllegalArgumentException("The server version (" + clientVersion + ") does not match the client " - + "(" + BuildConfig.VERSION_NAME + ")"); + throw new IllegalArgumentException( + "The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")"); } if (args.length != 8) { @@ -133,11 +135,26 @@ public final class Server { } } + @SuppressWarnings("checkstyle:MagicNumber") + private static void suggestFix(Throwable e) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (e instanceof MediaCodec.CodecException) { + MediaCodec.CodecException mce = (MediaCodec.CodecException) e; + if (mce.getErrorCode() == 0xfffffc0e) { + Ln.e("The hardware encoder is not able to encode at the given definition."); + Ln.e("Try with a lower definition:"); + Ln.e(" scrcpy -m 1024"); + } + } + } + } + public static void main(String... args) throws Exception { Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { Ln.e("Exception on thread " + t, e); + suggestFix(e); } }); diff --git a/server/src/main/java/com/genymobile/scrcpy/Size.java b/server/src/main/java/com/genymobile/scrcpy/Size.java index 0d546bbd..fd4b6971 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/Size.java @@ -38,8 +38,7 @@ public final class Size { return false; } Size size = (Size) o; - return width == size.width - && height == size.height; + return width == size.width && height == size.height; } @Override @@ -49,9 +48,6 @@ public final class Size { @Override public String toString() { - return "Size{" - + "width=" + width - + ", height=" + height - + '}'; + return "Size{" + "width=" + width + ", height=" + height + '}'; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index f45d82a4..b1b81903 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -1,11 +1,15 @@ package com.genymobile.scrcpy; import android.annotation.SuppressLint; +import android.app.Application; +import android.app.Instrumentation; +import android.content.Context; import android.content.pm.ApplicationInfo; import android.os.Looper; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Method; public final class Workarounds { private Workarounds() { @@ -48,14 +52,25 @@ public final class Workarounds { applicationInfo.packageName = "com.genymobile.scrcpy"; // appBindData.appInfo = applicationInfo; - Field appInfo = appBindDataClass.getDeclaredField("appInfo"); - appInfo.setAccessible(true); - appInfo.set(appBindData, applicationInfo); + Field appInfoField = appBindDataClass.getDeclaredField("appInfo"); + appInfoField.setAccessible(true); + appInfoField.set(appBindData, applicationInfo); // activityThread.mBoundApplication = appBindData; Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication"); mBoundApplicationField.setAccessible(true); mBoundApplicationField.set(activityThread, appBindData); + + // Context ctx = activityThread.getSystemContext(); + Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext"); + Context ctx = (Context) getSystemContextMethod.invoke(activityThread); + + Application app = Instrumentation.newApplication(Application.class, ctx); + + // activityThread.mInitialApplication = app; + Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication"); + mInitialApplicationField.setAccessible(true); + mInitialApplicationField.set(activityThread, app); } catch (Throwable throwable) { // this is a workaround, so failing is not an error Ln.w("Could not fill app info: " + throwable.getMessage()); 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 27dcb443..592bdf6b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -22,47 +22,37 @@ public class ClipboardManager { this.manager = manager; } - private Method getGetPrimaryClipMethod() { + private Method getGetPrimaryClipMethod() throws NoSuchMethodException { 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); + 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); } } return getPrimaryClipMethod; } - private Method getSetPrimaryClipMethod() { + private Method getSetPrimaryClipMethod() throws NoSuchMethodException { 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); + 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); } } return setPrimaryClipMethod; } - private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, - IllegalAccessException { + 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 { + 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 { @@ -71,32 +61,26 @@ public class ClipboardManager { } public CharSequence getText() { - Method method = getGetPrimaryClipMethod(); - if (method == null) { - return null; - } try { + Method method = getGetPrimaryClipMethod(); ClipData clipData = getPrimaryClip(method, manager); if (clipData == null || clipData.getItemCount() == 0) { return null; } return clipData.getItemAt(0).getText(); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); return null; } } public void setText(CharSequence text) { - Method method = getSetPrimaryClipMethod(); - if (method == null) { - return; - } - ClipData clipData = ClipData.newPlainText(null, text); try { + Method method = getSetPrimaryClipMethod(); + ClipData clipData = ClipData.newPlainText(null, text); setPrimaryClip(method, manager, clipData); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", 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 788a04c7..44fa613b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -21,26 +21,19 @@ public final class InputManager { this.manager = manager; } - private Method getInjectInputEventMethod() { + private Method getInjectInputEventMethod() throws NoSuchMethodException { if (injectInputEventMethod == null) { - try { - injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); - } catch (NoSuchMethodException e) { - Ln.e("Could not find method", e); - } + injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); } return injectInputEventMethod; } public boolean injectInputEvent(InputEvent inputEvent, int mode) { - Method method = getInjectInputEventMethod(); - if (method == null) { - return false; - } try { - return (Boolean) method.invoke(manager, inputEvent, mode); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + Method method = getInjectInputEventMethod(); + return (boolean) method.invoke(manager, inputEvent, mode); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); return false; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index 66acdba8..8ff074b3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -17,28 +17,21 @@ public final class PowerManager { this.manager = manager; } - private Method getIsScreenOnMethod() { + private Method getIsScreenOnMethod() throws NoSuchMethodException { 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); - } + @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); } return isScreenOnMethod; } public boolean isScreenOn() { - Method method = getIsScreenOnMethod(); - if (method == null) { - return false; - } try { - return (Boolean) method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + Method method = getIsScreenOnMethod(); + return (boolean) method.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); return false; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 670de952..6f8941bd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -17,49 +17,35 @@ public class StatusBarManager { this.manager = manager; } - private Method getExpandNotificationsPanelMethod() { + private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException { if (expandNotificationsPanelMethod == null) { - try { - expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); - } catch (NoSuchMethodException e) { - Ln.e("Could not find method", e); - } + expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); } return expandNotificationsPanelMethod; } - private Method getCollapsePanelsMethod() { + private Method getCollapsePanelsMethod() throws NoSuchMethodException { if (collapsePanelsMethod == null) { - try { - collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); - } catch (NoSuchMethodException e) { - Ln.e("Could not find method", e); - } + collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); } return collapsePanelsMethod; } public void expandNotificationsPanel() { - Method method = getExpandNotificationsPanelMethod(); - if (method == null) { - return; - } try { + Method method = getExpandNotificationsPanelMethod(); method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); } } public void collapsePanels() { - Method method = getCollapsePanelsMethod(); - if (method == null) { - return; - } try { + Method method = getCollapsePanelsMethod(); method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", 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 bef6e5d9..227bbc85 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -84,29 +84,23 @@ public final class SurfaceControl { } } - private static Method getGetBuiltInDisplayMethod() { + private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException { 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); + // 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"); } } return getBuiltInDisplayMethod; } public static IBinder getBuiltInDisplay() { - Method method = getGetBuiltInDisplayMethod(); - if (method == null) { - return null; - } + try { + Method method = getGetBuiltInDisplayMethod(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { // call getBuiltInDisplay(0) return (IBinder) method.invoke(null, 0); @@ -114,32 +108,25 @@ public final class SurfaceControl { // call getInternalDisplayToken() return (IBinder) method.invoke(null); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); return null; } } - private static Method getSetDisplayPowerModeMethod() { + private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException { if (setDisplayPowerModeMethod == null) { - try { - setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class); - } catch (NoSuchMethodException e) { - Ln.e("Could not find method", e); - } + setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class); } return setDisplayPowerModeMethod; } public static void setDisplayPowerMode(IBinder displayToken, int mode) { - Method method = getSetDisplayPowerModeMethod(); - if (method == null) { - return; - } try { + Method method = getSetDisplayPowerModeMethod(); method.invoke(null, displayToken, mode); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index 52096461..cc687cd5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -1,27 +1,95 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.os.IInterface; import android.view.IRotationWatcher; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + public final class WindowManager { private final IInterface manager; + private Method getRotationMethod; + private Method freezeRotationMethod; + private Method isRotationFrozenMethod; + private Method thawRotationMethod; public WindowManager(IInterface manager) { this.manager = manager; } - public int getRotation() { - try { + private Method getGetRotationMethod() throws NoSuchMethodException { + if (getRotationMethod == null) { Class cls = manager.getClass(); try { - return (Integer) cls.getMethod("getRotation").invoke(manager); - } catch (NoSuchMethodException e) { // method changed since this commit: // https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2 - return (Integer) cls.getMethod("getDefaultDisplayRotation").invoke(manager); + getRotationMethod = cls.getMethod("getDefaultDisplayRotation"); + } catch (NoSuchMethodException e) { + // old version + getRotationMethod = cls.getMethod("getRotation"); } - } catch (Exception e) { - throw new AssertionError(e); + } + return getRotationMethod; + } + + private Method getFreezeRotationMethod() throws NoSuchMethodException { + if (freezeRotationMethod == null) { + freezeRotationMethod = manager.getClass().getMethod("freezeRotation", int.class); + } + return freezeRotationMethod; + } + + private Method getIsRotationFrozenMethod() throws NoSuchMethodException { + if (isRotationFrozenMethod == null) { + isRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen"); + } + return isRotationFrozenMethod; + } + + private Method getThawRotationMethod() throws NoSuchMethodException { + if (thawRotationMethod == null) { + thawRotationMethod = manager.getClass().getMethod("thawRotation"); + } + return thawRotationMethod; + } + + public int getRotation() { + try { + Method method = getGetRotationMethod(); + return (int) method.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return 0; + } + } + + public void freezeRotation(int rotation) { + try { + Method method = getFreezeRotationMethod(); + method.invoke(manager, rotation); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + } + } + + public boolean isRotationFrozen() { + try { + Method method = getIsRotationFrozenMethod(); + return (boolean) method.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return false; + } + } + + public void thawRotation() { + try { + Method method = getThawRotationMethod(); + method.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); } } @@ -29,11 +97,12 @@ public final class WindowManager { try { Class cls = manager.getClass(); try { - cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); - } catch (NoSuchMethodException e) { // display parameter added since this commit: // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1 cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0); + } catch (NoSuchMethodException e) { + // old version + cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); } } catch (Exception e) { throw new AssertionError(e); diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index ede759dc..5e663bb9 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -240,6 +240,22 @@ public class ControlMessageReaderTest { Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction()); } + @Test + public void testParseRotateDevice() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_ROTATE_DEVICE); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType()); + } + @Test public void testMultiEvents() throws IOException { ControlMessageReader reader = new ControlMessageReader();