Merge pull request #14 from Genymobile/master

update
This commit is contained in:
feiser2016 2020-01-21 15:20:48 +08:00 committed by GitHub
commit 1c896f47af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 2090 additions and 970 deletions

View file

@ -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:

84
FAQ.ko.md Normal file
View file

@ -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
```

View file

@ -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.

498
README.ko.md Normal file
View file

@ -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/

View file

@ -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.

View file

@ -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

View file

@ -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

529
app/src/cli.c Normal file
View file

@ -0,0 +1,529 @@
#include "cli.h"
#include <getopt.h>
#include <stdint.h>
#include <unistd.h>
#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;
}

21
app/src/cli.h Normal file
View file

@ -0,0 +1,21 @@
#ifndef SCRCPY_CLI_H
#define SCRCPY_CLI_H
#include <stdbool.h>
#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

View file

@ -4,11 +4,14 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#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);
}

View file

@ -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

View file

@ -1,12 +1,12 @@
#include "control_msg.h"
#include <assert.h>
#include <string.h>
#include <SDL2/SDL_assert.h>
#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:

View file

@ -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 {

View file

@ -1,10 +1,10 @@
#include "controller.h"
#include <SDL2/SDL_assert.h>
#include <assert.h>
#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);

View file

@ -6,10 +6,10 @@
#include <SDL2/SDL_thread.h>
#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);

View file

@ -2,7 +2,6 @@
#include <libavformat/avformat.h>
#include <libavutil/time.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_events.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
@ -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

View file

@ -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) {

View file

@ -5,7 +5,7 @@
#include "config.h"
#include "common.h"
#include "net.h"
#include "util/net.h"
#define DEVICE_NAME_FIELD_LENGTH 64

View file

@ -1,11 +1,10 @@
#include "device_msg.h"
#include <string.h>
#include <SDL2/SDL_assert.h>
#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,

View file

@ -1,12 +1,12 @@
#include "file_handler.h"
#include <assert.h>
#include <string.h>
#include <SDL2/SDL_assert.h>
#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) {

View file

@ -6,8 +6,8 @@
#include <SDL2/SDL_thread.h>
#include "config.h"
#include "cbuf.h"
#include "command.h"
#include "util/cbuf.h"
typedef enum {
ACTION_INSTALL_APK,

View file

@ -1,11 +1,11 @@
#include "fps_counter.h"
#include <SDL2/SDL_assert.h>
#include <assert.h>
#include <SDL2/SDL_timer.h>
#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

View file

@ -1,11 +1,11 @@
#include "input_manager.h"
#include <SDL2/SDL_assert.h>
#include <assert.h>
#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
// <https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>
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;
}

View file

@ -1,52 +0,0 @@
#ifndef LOCKUTIL_H
#define LOCKUTIL_H
#include <stdint.h>
#include <SDL2/SDL_mutex.h>
#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

View file

@ -1,206 +1,15 @@
#include "scrcpy.h"
#include <getopt.h>
#include <stdbool.h>
#include <stdint.h>
#include <unistd.h>
#include <libavformat/avformat.h>
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
#include <SDL2/SDL.h>
#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

View file

@ -1,12 +1,12 @@
#include "receiver.h"
#include <SDL2/SDL_assert.h>
#include <assert.h>
#include <SDL2/SDL_clipboard.h>
#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;

View file

@ -6,7 +6,7 @@
#include <SDL2/SDL_thread.h>
#include "config.h"
#include "net.h"
#include "util/net.h"
// receive events from the device
// managed by the controller

View file

@ -1,12 +1,12 @@
#include "recorder.h"
#include <assert.h>
#include <libavutil/time.h>
#include <SDL2/SDL_assert.h>
#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)

View file

@ -8,7 +8,7 @@
#include "config.h"
#include "common.h"
#include "queue.h"
#include "util/queue.h"
enum recorder_format {
RECORDER_FORMAT_AUTO,

View file

@ -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) {
// <https://stackoverflow.com/a/40693139/1987178>
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;

View file

@ -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, \

View file

@ -1,5 +1,6 @@
#include "screen.h"
#include <assert.h>
#include <string.h>
#include <SDL2/SDL.h>
@ -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

View file

@ -1,22 +1,22 @@
#include "server.h"
#include <assert.h>
#include <errno.h>
#include <inttypes.h>
#include <libgen.h>
#include <stdio.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_timer.h>
#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");

View file

@ -6,7 +6,7 @@
#include "config.h"
#include "command.h"
#include "net.h"
#include "util/net.h"
struct server {
char *serial;

View file

@ -1,8 +1,8 @@
#include "stream.h"
#include <assert.h>
#include <libavformat/avformat.h>
#include <libavutil/time.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_events.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
@ -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;

View file

@ -8,7 +8,7 @@
#include <SDL2/SDL_thread.h>
#include "config.h"
#include "net.h"
#include "util/net.h"
struct video_buffer;

View file

@ -17,10 +17,11 @@
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#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 {

View file

@ -1,4 +1,4 @@
#include "net.h"
#include "util/net.h"
#include <unistd.h>

View file

@ -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));

View file

@ -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) {

View file

@ -1,12 +1,13 @@
#include "tiny_xpm.h"
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#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;
}
}

View file

@ -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;

74
app/src/util/lock.h Normal file
View file

@ -0,0 +1,74 @@
#ifndef LOCK_H
#define LOCK_H
#include <stdint.h>
#include <SDL2/SDL_mutex.h>
#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

View file

@ -2,9 +2,9 @@
#ifndef QUEUE_H
#define QUEUE_H
#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <SDL2/SDL_assert.h>
#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; \
})

View file

@ -1,5 +1,7 @@
#include "str_util.h"
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
@ -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);

View file

@ -1,6 +1,7 @@
#ifndef STRUTIL_H
#define STRUTIL_H
#include <stdbool.h>
#include <stddef.h>
#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);

View file

@ -1,13 +1,13 @@
#include "video_buffer.h"
#include <SDL2/SDL_assert.h>
#include <assert.h>
#include <SDL2/SDL_mutex.h>
#include <libavutil/avutil.h>
#include <libavformat/avformat.h>
#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) {

View file

@ -0,0 +1,76 @@
#include <assert.h>
#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;
}

View file

@ -1,7 +1,7 @@
#include <assert.h>
#include <string.h>
#include "cbuf.h"
#include "util/cbuf.h"
struct int_queue CBUF(int, 32);

128
app/tests/test_cli.c Normal file
View file

@ -0,0 +1,128 @@
#include <assert.h>
#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;
};

View file

@ -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;
}

View file

@ -1,6 +1,6 @@
#include <assert.h>
#include <queue.h>
#include "util/queue.h"
struct foo {
int value;

View file

@ -1,7 +1,10 @@
#include <assert.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <SDL2/SDL.h>
#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;
}

View file

@ -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')

View file

@ -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 {

View file

@ -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..."

View file

@ -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;

View file

@ -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:

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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;

View file

@ -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 + '}';
}
}

View file

@ -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 + '}';
}
}

View file

@ -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);
}
});

View file

@ -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 + '}';
}
}

View file

@ -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());

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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
// <https://github.com/Genymobile/scrcpy/issues/586>
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
// <https://github.com/Genymobile/scrcpy/issues/586>
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);
}
}

View file

@ -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);

View file

@ -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();