Compare commits
95 Commits
fixhidpi.4
...
faq
Author | SHA1 | Date | |
---|---|---|---|
7bb91638ad | |||
bc7508427b | |||
24ade6ad77 | |||
c396758b4e | |||
f903cd376d | |||
e8127375ae | |||
1144f64214 | |||
39356602ed | |||
0fb22c3e98 | |||
31bd95022b | |||
4687a0ebac | |||
6965d051ae | |||
71df3175bd | |||
a0f8e7fd9f | |||
e4cebc8d4c | |||
ba1b36758e | |||
ad92a192b5 | |||
81a8e503e5 | |||
242e57d69b | |||
024c2f7e6b | |||
1eae139b6e | |||
419c869c9c | |||
929bf48c7e | |||
d950383b72 | |||
61274a7cdb | |||
64bcac9157 | |||
3259c60b22 | |||
e0b117de13 | |||
eb0f339271 | |||
bdd05b4a16 | |||
525d6d4a75 | |||
4041043d1c | |||
fbc86a616c | |||
b2bf25c52c | |||
5eeaed09ae | |||
3100533e56 | |||
684e0abb74 | |||
86fcd89d80 | |||
8a694a9785 | |||
26529d377f | |||
15a206b7fc | |||
d0f5a7fd9f | |||
510caff0cd | |||
b5ebb234dd | |||
73e8ec1b35 | |||
8dc11a0286 | |||
06104a701b | |||
8bc056b9c6 | |||
7637a113e3 | |||
31d9d56117 | |||
6abb8fd0cd | |||
2b845680b0 | |||
ebdc2ee8b5 | |||
dfd0707a29 | |||
8ec077ce1b | |||
83ace84280 | |||
c9d886f38b | |||
7d7f3daff2 | |||
40c3c57613 | |||
2aa65015bc | |||
4906aff454 | |||
cb6b300483 | |||
c2116082ab | |||
3599fcaae5 | |||
c610a6b3c7 | |||
704c0ff4dd | |||
b145b8d5f4 | |||
90293240cc | |||
213c468c20 | |||
18f2e33a8b | |||
7aed5d5b60 | |||
601b0fecdd | |||
7fd800d583 | |||
1d97d7213d | |||
fb976816f9 | |||
1b78a77962 | |||
59073223aa | |||
59bc5bc1f5 | |||
9fd7a80a89 | |||
b6e2f8ae00 | |||
ce5635f28c | |||
771bd8404d | |||
af3027a85b | |||
b963a3b9d5 | |||
aa0f77c898 | |||
35c05bb3ce | |||
f6f2868868 | |||
6996cbf5d3 | |||
e282100d0b | |||
b08a98324d | |||
c916af0984 | |||
ff061b4f30 | |||
157c60feb4 | |||
2d90e1befd | |||
0e301ddf19 |
8
BUILD.md
8
BUILD.md
@ -40,7 +40,7 @@ Install the required packages from your package manager.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# runtime dependencies
|
# runtime dependencies
|
||||||
sudo apt install ffmpeg libsdl2-2.0-0
|
sudo apt install ffmpeg libsdl2-2.0-0 adb
|
||||||
|
|
||||||
# client build dependencies
|
# client build dependencies
|
||||||
sudo apt install gcc git pkg-config meson ninja-build \
|
sudo apt install gcc git pkg-config meson ninja-build \
|
||||||
@ -233,10 +233,10 @@ You can then [run](README.md#run) _scrcpy_.
|
|||||||
|
|
||||||
## Prebuilt server
|
## Prebuilt server
|
||||||
|
|
||||||
- [`scrcpy-server-v1.10.jar`][direct-scrcpy-server]
|
- [`scrcpy-server-v1.12.1`][direct-scrcpy-server]
|
||||||
_(SHA-256: cbeb1a4e046f1392c1dc73c3ccffd7f86dec4636b505556ea20929687a119390)_
|
_(SHA-256: 63e569c8a1d0c1df31d48c4214871c479a601782945fed50c1e61167d78266ea)_
|
||||||
|
|
||||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-server-v1.10.jar
|
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-server-v1.12.1
|
||||||
|
|
||||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||||
configuration:
|
configuration:
|
||||||
|
@ -189,7 +189,7 @@ The client uses 4 threads:
|
|||||||
recording,
|
recording,
|
||||||
- the **controller** thread, sending _control messages_ to the server,
|
- the **controller** thread, sending _control messages_ to the server,
|
||||||
- the **receiver** thread (managed by the controller), receiving _device
|
- the **receiver** thread (managed by the controller), receiving _device
|
||||||
messages_ from the client.
|
messages_ from the server.
|
||||||
|
|
||||||
In addition, another thread can be started if necessary to handle APK
|
In addition, another thread can be started if necessary to handle APK
|
||||||
installation or file push requests (via drag&drop on the main window) or to
|
installation or file push requests (via drag&drop on the main window) or to
|
||||||
@ -214,7 +214,7 @@ When a new decoded frame is available, the decoder _swaps_ the decoding and
|
|||||||
rendering frame (with proper synchronization). Thus, it immediatly starts
|
rendering frame (with proper synchronization). Thus, it immediatly starts
|
||||||
to decode a new frame while the main thread renders the last one.
|
to decode a new frame while the main thread renders the last one.
|
||||||
|
|
||||||
If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw
|
If a [recorder] is present (i.e. `--record` is enabled), then it muxes the raw
|
||||||
H.264 packet to the output video file.
|
H.264 packet to the output video file.
|
||||||
|
|
||||||
[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h
|
[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h
|
||||||
|
84
FAQ.ko.md
Normal file
84
FAQ.ko.md
Normal 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
|
||||||
|
```
|
149
FAQ.md
149
FAQ.md
@ -3,19 +3,102 @@
|
|||||||
Here are the common reported problems and their status.
|
Here are the common reported problems and their status.
|
||||||
|
|
||||||
|
|
||||||
### On Windows, my device is not detected
|
## `adb` issues
|
||||||
|
|
||||||
The most common is your device not being detected by `adb`, or is unauthorized.
|
`scrcpy` execute `adb` commands to initialize the connection with the device. If
|
||||||
Check everything is ok by calling:
|
`adb` fails, then scrcpy will not work.
|
||||||
|
|
||||||
adb devices
|
In that case, it will print this error:
|
||||||
|
|
||||||
Windows may need some [drivers] to detect your device.
|
> ERROR: "adb push" returned with value 1
|
||||||
|
|
||||||
|
This is typically not a bug in _scrcpy_, but a problem in your environment.
|
||||||
|
|
||||||
|
To find out the cause, execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb devices
|
||||||
|
```
|
||||||
|
|
||||||
|
### `adb` not found
|
||||||
|
|
||||||
|
You need `adb` accessible from your `PATH`.
|
||||||
|
|
||||||
|
On Windows, the current directory is in your `PATH`, and `adb.exe` is included
|
||||||
|
in the release, so it should work out-of-the-box.
|
||||||
|
|
||||||
|
|
||||||
|
### Device unauthorized
|
||||||
|
|
||||||
|
Check [stackoverflow][device-unauthorized].
|
||||||
|
|
||||||
|
[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized
|
||||||
|
|
||||||
|
|
||||||
|
### Device not detected
|
||||||
|
|
||||||
|
If your device is not detected, you may need some [drivers] (on Windows).
|
||||||
|
|
||||||
[drivers]: https://developer.android.com/studio/run/oem-usb.html
|
[drivers]: https://developer.android.com/studio/run/oem-usb.html
|
||||||
|
|
||||||
|
|
||||||
### I can only mirror, I cannot interact with the device
|
### Several devices connected
|
||||||
|
|
||||||
|
If several devices are connected, you will encounter this error:
|
||||||
|
|
||||||
|
> adb: error: failed to get feature set: more than one device/emulator
|
||||||
|
|
||||||
|
the identifier of the device you want to mirror must be provided:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy -s 01234567890abcdef
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that if your device is connected over TCP/IP, you'll get this message:
|
||||||
|
|
||||||
|
> adb: error: more than one device/emulator
|
||||||
|
> ERROR: "adb reverse" returned with value 1
|
||||||
|
> WARN: 'adb reverse' failed, fallback to 'adb forward'
|
||||||
|
|
||||||
|
This is expected (due to a bug on old Android versions, see [#5]), but in that
|
||||||
|
case, scrcpy fallbacks to a different method, which should work.
|
||||||
|
|
||||||
|
[#5]: https://github.com/Genymobile/scrcpy/issues/5
|
||||||
|
|
||||||
|
|
||||||
|
### Conflicts between adb versions
|
||||||
|
|
||||||
|
> adb server version (41) doesn't match this client (39); killing...
|
||||||
|
|
||||||
|
This error occurs when you use several `adb` versions simultaneously. You must
|
||||||
|
find the program using a different `adb` version, and use the same `adb` version
|
||||||
|
everywhere.
|
||||||
|
|
||||||
|
You could overwrite the `adb` binary in the other program, or ask _scrcpy_ to
|
||||||
|
use a specific `adb` binary, by setting the `ADB` environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
set ADB=/path/to/your/adb
|
||||||
|
scrcpy
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Device disconnected
|
||||||
|
|
||||||
|
If _scrcpy_ stops itself with the warning "Device disconnected", then the
|
||||||
|
`adb` connection has been closed.
|
||||||
|
|
||||||
|
Try with another USB cable or plug it into another USB port. See [#281] and
|
||||||
|
[#283].
|
||||||
|
|
||||||
|
[#281]: https://github.com/Genymobile/scrcpy/issues/281
|
||||||
|
[#283]: https://github.com/Genymobile/scrcpy/issues/283
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Control issues
|
||||||
|
|
||||||
|
### Mouse and keyboard do not work
|
||||||
|
|
||||||
On some devices, you may need to enable an option to allow [simulating input].
|
On some devices, you may need to enable an option to allow [simulating input].
|
||||||
In developer options, enable:
|
In developer options, enable:
|
||||||
@ -29,18 +112,25 @@ In developer options, enable:
|
|||||||
### Mouse clicks at wrong location
|
### Mouse clicks at wrong location
|
||||||
|
|
||||||
On MacOS, with HiDPI support and multiple screens, input location are wrongly
|
On MacOS, with HiDPI support and multiple screens, input location are wrongly
|
||||||
scaled. See [issue 15].
|
scaled. See [#15].
|
||||||
|
|
||||||
[issue 15]: https://github.com/Genymobile/scrcpy/issues/15
|
[#15]: https://github.com/Genymobile/scrcpy/issues/15
|
||||||
|
|
||||||
A workaround is to build with HiDPI support disabled:
|
Open _scrcpy_ directly on the monitor you use it.
|
||||||
|
|
||||||
```bash
|
|
||||||
meson x --buildtype release -Dhidpi_support=false
|
|
||||||
```
|
|
||||||
|
|
||||||
However, the video will be displayed at lower resolution.
|
### Special characters do not work
|
||||||
|
|
||||||
|
Injecting text input is [limited to ASCII characters][text-input]. A trick
|
||||||
|
allows to also inject some [accented characters][accented-characters], but
|
||||||
|
that's all. See [#37].
|
||||||
|
|
||||||
|
[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode
|
||||||
|
[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters
|
||||||
|
[#37]: https://github.com/Genymobile/scrcpy/issues/37
|
||||||
|
|
||||||
|
|
||||||
|
## Client issues
|
||||||
|
|
||||||
### The quality is low on HiDPI display
|
### The quality is low on HiDPI display
|
||||||
|
|
||||||
@ -51,6 +141,11 @@ On Windows, you may need to configure the [scaling behavior].
|
|||||||
|
|
||||||
[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723
|
[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723
|
||||||
|
|
||||||
|
If your computer definition is far smaller than your screen, then you'll get
|
||||||
|
poor quality. See [#40].
|
||||||
|
|
||||||
|
[#40]: https://github.com/Genymobile/scrcpy/issues/40
|
||||||
|
|
||||||
|
|
||||||
### KWin compositor crashes
|
### KWin compositor crashes
|
||||||
|
|
||||||
@ -61,19 +156,29 @@ As a workaround, [disable "Block compositing"][kwin].
|
|||||||
[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613
|
[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613
|
||||||
|
|
||||||
|
|
||||||
### I get an error "Could not open video stream"
|
## Crashes
|
||||||
|
|
||||||
|
### Exception
|
||||||
|
|
||||||
There may be many reasons. One common cause is that the hardware encoder of your
|
There may be many reasons. One common cause is that the hardware encoder of your
|
||||||
device is not able to encode at the given definition:
|
device is not able to encode at the given definition:
|
||||||
|
|
||||||
```
|
> ```
|
||||||
ERROR: Exception on thread Thread[main,5,main]
|
> ERROR: Exception on thread Thread[main,5,main]
|
||||||
android.media.MediaCodec$CodecException: Error 0xfffffc0e
|
> android.media.MediaCodec$CodecException: Error 0xfffffc0e
|
||||||
...
|
> ...
|
||||||
Exit due to uncaughtException in main thread:
|
> Exit due to uncaughtException in main thread:
|
||||||
ERROR: Could not open video stream
|
> ERROR: Could not open video stream
|
||||||
INFO: Initial texture: 1080x2336
|
> INFO: Initial texture: 1080x2336
|
||||||
```
|
> ```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
> ```
|
||||||
|
> ERROR: Exception on thread Thread[main,5,main]
|
||||||
|
> java.lang.IllegalStateException
|
||||||
|
> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method)
|
||||||
|
> ```
|
||||||
|
|
||||||
Just try with a lower definition:
|
Just try with a lower definition:
|
||||||
|
|
||||||
|
2
LICENSE
2
LICENSE
@ -188,7 +188,7 @@
|
|||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright (C) 2018 Genymobile
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -100,11 +100,11 @@ dist-win32: build-server build-win32 build-win32-noconsole
|
|||||||
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
|
cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
|
||||||
cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
@ -115,11 +115,11 @@ dist-win64: build-server build-win64 build-win64-noconsole
|
|||||||
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
|
cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
|
||||||
cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
|
498
README.ko.md
Normal file
498
README.ko.md
Normal 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/
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
주요 기능은 다음과 같습니다.
|
||||||
|
|
||||||
|
- **가벼움** (기본적이며 디바이스의 화면만을 보여줌)
|
||||||
|
- **뛰어난 성능** (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/
|
310
README.md
310
README.md
@ -1,4 +1,4 @@
|
|||||||
# scrcpy (v1.10)
|
# scrcpy (v1.12.1)
|
||||||
|
|
||||||
This application provides display and control of Android devices connected on
|
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.
|
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
|
### Linux
|
||||||
|
|
||||||
On Linux, you typically need to [build the app manually][BUILD]. Don't worry,
|
In Debian (_testing_ and _sid_ for now):
|
||||||
it's not that hard.
|
|
||||||
|
```
|
||||||
|
apt install scrcpy
|
||||||
|
```
|
||||||
|
|
||||||
A [Snap] package is available: [`scrcpy`][snap-link].
|
A [Snap] package is available: [`scrcpy`][snap-link].
|
||||||
|
|
||||||
@ -56,19 +59,38 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
|
|||||||
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
|
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
|
||||||
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
[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
|
### Windows
|
||||||
|
|
||||||
For Windows, for simplicity, prebuilt archives with all the dependencies
|
For Windows, for simplicity, a prebuilt archive with all the dependencies
|
||||||
(including `adb`) are available:
|
(including `adb`) is available:
|
||||||
|
|
||||||
- [`scrcpy-win32-v1.10.zip`][direct-win32]
|
- [`scrcpy-win64-v1.12.1.zip`][direct-win64]
|
||||||
_(SHA-256: f98b400b3764404b33b212e9762dd6f1593ddb766c1480fc2609c94768e4a8e1)_
|
_(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_
|
||||||
- [`scrcpy-win64-v1.10.zip`][direct-win64]
|
|
||||||
_(SHA-256: 95de34575d873c7e95dfcfb5e74d0f6af4f70b2a5bc6fde0f48d1a05480e3a44)_
|
|
||||||
|
|
||||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-win32-v1.10.zip
|
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip
|
||||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-win64-v1.10.zip
|
|
||||||
|
It is also available in [Chocolatey]:
|
||||||
|
|
||||||
|
[Chocolatey]: https://chocolatey.org/
|
||||||
|
|
||||||
|
```bash
|
||||||
|
choco install scrcpy
|
||||||
|
choco install adb # if you don't have it yet
|
||||||
|
```
|
||||||
|
|
||||||
|
And in [Scoop]:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scoop install scrcpy
|
||||||
|
scoop install adb # if you don't have it yet
|
||||||
|
```
|
||||||
|
|
||||||
|
[Scoop]: https://scoop.sh
|
||||||
|
|
||||||
You can also [build the app manually][BUILD].
|
You can also [build the app manually][BUILD].
|
||||||
|
|
||||||
@ -108,8 +130,9 @@ scrcpy --help
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
### Capture configuration
|
||||||
|
|
||||||
### Reduce size
|
#### Reduce size
|
||||||
|
|
||||||
Sometimes, it is useful to mirror an Android device at a lower definition to
|
Sometimes, it is useful to mirror an Android device at a lower definition to
|
||||||
increase performance.
|
increase performance.
|
||||||
@ -125,7 +148,7 @@ The other dimension is computed to that the device aspect ratio is preserved.
|
|||||||
That way, a device in 1920×1080 will be mirrored at 1024×576.
|
That way, a device in 1920×1080 will be mirrored at 1024×576.
|
||||||
|
|
||||||
|
|
||||||
### Change bit-rate
|
#### Change bit-rate
|
||||||
|
|
||||||
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
|
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
|
||||||
|
|
||||||
@ -134,8 +157,15 @@ scrcpy --bit-rate 2M
|
|||||||
scrcpy -b 2M # short version
|
scrcpy -b 2M # short version
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Limit frame rate
|
||||||
|
|
||||||
### Crop
|
On devices with Android >= 10, the capture frame rate can be limited:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --max-fps 15
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Crop
|
||||||
|
|
||||||
The device screen may be cropped to mirror only part of the screen.
|
The device screen may be cropped to mirror only part of the screen.
|
||||||
|
|
||||||
@ -143,35 +173,12 @@ This is useful for example to mirror only one eye of the Oculus Go:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
|
scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
|
||||||
scrcpy -c 1224:1440:0:0 # short version
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If `--max-size` is also specified, resizing is applied after cropping.
|
If `--max-size` is also specified, resizing is applied after cropping.
|
||||||
|
|
||||||
|
|
||||||
### Wireless
|
### Recording
|
||||||
|
|
||||||
_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
|
|
||||||
device over TCP/IP:
|
|
||||||
|
|
||||||
1. Connect the device to the same Wi-Fi as your computer.
|
|
||||||
2. Get your device IP address (in Settings → About phone → Status).
|
|
||||||
3. Enable adb over TCP/IP on your device: `adb tcpip 5555`.
|
|
||||||
4. Unplug your device.
|
|
||||||
5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_.
|
|
||||||
6. Run `scrcpy` as usual.
|
|
||||||
|
|
||||||
It may be useful to decrease the bit-rate and the definition:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --bit-rate 2M --max-size 800
|
|
||||||
scrcpy -b2M -m800 # short version
|
|
||||||
```
|
|
||||||
|
|
||||||
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
|
|
||||||
|
|
||||||
|
|
||||||
### Record screen
|
|
||||||
|
|
||||||
It is possible to record the screen while mirroring:
|
It is possible to record the screen while mirroring:
|
||||||
|
|
||||||
@ -196,7 +203,31 @@ variation] does not impact the recorded file.
|
|||||||
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||||
|
|
||||||
|
|
||||||
### Multi-devices
|
### Connection
|
||||||
|
|
||||||
|
#### Wireless
|
||||||
|
|
||||||
|
_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
|
||||||
|
device over TCP/IP:
|
||||||
|
|
||||||
|
1. Connect the device to the same Wi-Fi as your computer.
|
||||||
|
2. Get your device IP address (in Settings → About phone → Status).
|
||||||
|
3. Enable adb over TCP/IP on your device: `adb tcpip 5555`.
|
||||||
|
4. Unplug your device.
|
||||||
|
5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_.
|
||||||
|
6. Run `scrcpy` as usual.
|
||||||
|
|
||||||
|
It may be useful to decrease the bit-rate and the definition:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --bit-rate 2M --max-size 800
|
||||||
|
scrcpy -b2M -m800 # short version
|
||||||
|
```
|
||||||
|
|
||||||
|
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
|
||||||
|
|
||||||
|
|
||||||
|
#### Multi-devices
|
||||||
|
|
||||||
If several devices are listed in `adb devices`, you must specify the _serial_:
|
If several devices are listed in `adb devices`, you must specify the _serial_:
|
||||||
|
|
||||||
@ -205,10 +236,74 @@ scrcpy --serial 0123456789abcdef
|
|||||||
scrcpy -s 0123456789abcdef # short version
|
scrcpy -s 0123456789abcdef # short version
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If the device is connected over TCP/IP:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --serial 192.168.0.1:5555
|
||||||
|
scrcpy -s 192.168.0.1:5555 # short version
|
||||||
|
```
|
||||||
|
|
||||||
You can start several instances of _scrcpy_ for several devices.
|
You can start several instances of _scrcpy_ for several devices.
|
||||||
|
|
||||||
|
#### SSH tunnel
|
||||||
|
|
||||||
### Fullscreen
|
To connect to a remote device, it is possible to connect a local `adb` client to
|
||||||
|
a remote `adb` server (provided they use the same version of the _adb_
|
||||||
|
protocol):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb kill-server # kill the local adb server on 5037
|
||||||
|
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
|
||||||
|
# keep this open
|
||||||
|
```
|
||||||
|
|
||||||
|
From another terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy
|
||||||
|
```
|
||||||
|
|
||||||
|
Like for wireless connections, it may be useful to reduce quality:
|
||||||
|
|
||||||
|
```
|
||||||
|
scrcpy -b2M -m800 --max-fps 15
|
||||||
|
```
|
||||||
|
|
||||||
|
### Window configuration
|
||||||
|
|
||||||
|
#### Title
|
||||||
|
|
||||||
|
By default, the window title is the device model. It can be changed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --window-title 'My device'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Position and size
|
||||||
|
|
||||||
|
The initial window position and size may be specified:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Borderless
|
||||||
|
|
||||||
|
To disable window decorations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --window-borderless
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Always on top
|
||||||
|
|
||||||
|
To keep the scrcpy window always on top:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --always-on-top
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Fullscreen
|
||||||
|
|
||||||
The app may be started directly in fullscreen:
|
The app may be started directly in fullscreen:
|
||||||
|
|
||||||
@ -220,17 +315,45 @@ scrcpy -f # short version
|
|||||||
Fullscreen can then be toggled dynamically with `Ctrl`+`f`.
|
Fullscreen can then be toggled dynamically with `Ctrl`+`f`.
|
||||||
|
|
||||||
|
|
||||||
### Always on top
|
### Other mirroring options
|
||||||
|
|
||||||
The window of app can always be above others by:
|
#### Read-only
|
||||||
|
|
||||||
|
To disable controls (everything which can interact with the device: input keys,
|
||||||
|
mouse events, drag&drop files):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --always-on-top
|
scrcpy --no-control
|
||||||
scrcpy -T # short version
|
scrcpy -n
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Turn screen off
|
||||||
|
|
||||||
### Show touches
|
It is possible to turn the device screen off while mirroring on start with a
|
||||||
|
command-line option:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --turn-screen-off
|
||||||
|
scrcpy -S
|
||||||
|
```
|
||||||
|
|
||||||
|
Or by pressing `Ctrl`+`o` at any time.
|
||||||
|
|
||||||
|
To turn it back on, press `POWER` (or `Ctrl`+`p`).
|
||||||
|
|
||||||
|
#### Render expired frames
|
||||||
|
|
||||||
|
By default, to minimize latency, _scrcpy_ always renders the last decoded frame
|
||||||
|
available, and drops any previous one.
|
||||||
|
|
||||||
|
To force the rendering of all frames (at a cost of a possible increased
|
||||||
|
latency), use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --render-expired-frames
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Show touches
|
||||||
|
|
||||||
For presentations, it may be useful to show physical touches (on the physical
|
For presentations, it may be useful to show physical touches (on the physical
|
||||||
device).
|
device).
|
||||||
@ -247,7 +370,50 @@ scrcpy -t
|
|||||||
Note that it only shows _physical_ touches (with the finger on the device).
|
Note that it only shows _physical_ touches (with the finger on the device).
|
||||||
|
|
||||||
|
|
||||||
### Install APK
|
### 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
|
||||||
|
both directions:
|
||||||
|
|
||||||
|
- `Ctrl`+`c` copies the device clipboard to the computer clipboard;
|
||||||
|
- `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard;
|
||||||
|
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but
|
||||||
|
breaks non-ASCII characters).
|
||||||
|
|
||||||
|
#### Text injection preference
|
||||||
|
|
||||||
|
There are two kinds of [events][textevents] generated when typing text:
|
||||||
|
- _key events_, signaling that a key is pressed or released;
|
||||||
|
- _text events_, signaling that a text has been entered.
|
||||||
|
|
||||||
|
By default, letters are injected using key events, so that the keyboard behaves
|
||||||
|
as expected in games (typically for WASD keys).
|
||||||
|
|
||||||
|
But this may [cause issues][prefertext]. If you encounter such a problem, you
|
||||||
|
can avoid it by:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --prefer-text
|
||||||
|
```
|
||||||
|
|
||||||
|
(but this will break keyboard behavior in games)
|
||||||
|
|
||||||
|
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
|
||||||
|
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
|
||||||
|
|
||||||
|
|
||||||
|
### File drop
|
||||||
|
|
||||||
|
#### Install APK
|
||||||
|
|
||||||
To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_
|
To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_
|
||||||
window.
|
window.
|
||||||
@ -255,7 +421,7 @@ window.
|
|||||||
There is no visual feedback, a log is printed to the console.
|
There is no visual feedback, a log is printed to the console.
|
||||||
|
|
||||||
|
|
||||||
### Push file to device
|
#### Push file to device
|
||||||
|
|
||||||
To push a file to `/sdcard/` on the device, drag & drop a (non-APK) file to the
|
To push a file to `/sdcard/` on the device, drag & drop a (non-APK) file to the
|
||||||
_scrcpy_ window.
|
_scrcpy_ window.
|
||||||
@ -268,53 +434,8 @@ The target directory can be changed on start:
|
|||||||
scrcpy --push-target /sdcard/foo/bar/
|
scrcpy --push-target /sdcard/foo/bar/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Read-only
|
|
||||||
|
|
||||||
To disable controls (everything which can interact with the device: input keys,
|
### Audio forwarding
|
||||||
mouse events, drag&drop files):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --no-control
|
|
||||||
scrcpy -n
|
|
||||||
```
|
|
||||||
|
|
||||||
### Turn screen off
|
|
||||||
|
|
||||||
It is possible to turn the device screen off while mirroring on start with a
|
|
||||||
command-line option:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --turn-screen-off
|
|
||||||
scrcpy -S
|
|
||||||
```
|
|
||||||
|
|
||||||
Or by pressing `Ctrl`+`o` at any time.
|
|
||||||
|
|
||||||
To turn it back on, press `POWER` (or `Ctrl`+`p`).
|
|
||||||
|
|
||||||
|
|
||||||
### Render expired frames
|
|
||||||
|
|
||||||
By default, to minimize latency, _scrcpy_ always renders the last decoded frame
|
|
||||||
available, and drops any previous one.
|
|
||||||
|
|
||||||
To force the rendering of all frames (at a cost of a possible increased
|
|
||||||
latency), use:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --render-expired-frames
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom window title
|
|
||||||
|
|
||||||
By default, the window title is the device model. It can be changed:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --window-title 'My device'
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Forward audio
|
|
||||||
|
|
||||||
Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only).
|
Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only).
|
||||||
|
|
||||||
@ -340,6 +461,7 @@ Also see [issue #14].
|
|||||||
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
|
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
|
||||||
| Power on | _Right-click²_ | _Right-click²_
|
| Power on | _Right-click²_ | _Right-click²_
|
||||||
| Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o`
|
| Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o`
|
||||||
|
| Rotate device screen | `Ctrl`+`r` | `Cmd`+`r`
|
||||||
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
|
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
|
||||||
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
|
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
|
||||||
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`
|
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`
|
||||||
@ -396,7 +518,7 @@ Read the [developers page].
|
|||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
Copyright (C) 2018 Genymobile
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
src = [
|
src = [
|
||||||
'src/main.c',
|
'src/main.c',
|
||||||
|
'src/cli.c',
|
||||||
'src/command.c',
|
'src/command.c',
|
||||||
'src/control_msg.c',
|
'src/control_msg.c',
|
||||||
'src/controller.c',
|
'src/controller.c',
|
||||||
@ -10,16 +11,16 @@ src = [
|
|||||||
'src/file_handler.c',
|
'src/file_handler.c',
|
||||||
'src/fps_counter.c',
|
'src/fps_counter.c',
|
||||||
'src/input_manager.c',
|
'src/input_manager.c',
|
||||||
'src/net.c',
|
|
||||||
'src/receiver.c',
|
'src/receiver.c',
|
||||||
'src/recorder.c',
|
'src/recorder.c',
|
||||||
'src/scrcpy.c',
|
'src/scrcpy.c',
|
||||||
'src/screen.c',
|
'src/screen.c',
|
||||||
'src/server.c',
|
'src/server.c',
|
||||||
'src/str_util.c',
|
|
||||||
'src/tiny_xpm.c',
|
|
||||||
'src/stream.c',
|
'src/stream.c',
|
||||||
|
'src/tiny_xpm.c',
|
||||||
'src/video_buffer.c',
|
'src/video_buffer.c',
|
||||||
|
'src/util/net.c',
|
||||||
|
'src/util/str_util.c'
|
||||||
]
|
]
|
||||||
|
|
||||||
if not get_option('crossbuild_windows')
|
if not get_option('crossbuild_windows')
|
||||||
@ -85,7 +86,7 @@ endif
|
|||||||
conf = configuration_data()
|
conf = configuration_data()
|
||||||
|
|
||||||
# expose the build type
|
# expose the build type
|
||||||
conf.set('BUILD_DEBUG', get_option('buildtype') == 'debug')
|
conf.set('NDEBUG', get_option('buildtype') != 'debug')
|
||||||
|
|
||||||
# the version, updated on release
|
# the version, updated on release
|
||||||
conf.set_quoted('SCRCPY_VERSION', meson.project_version())
|
conf.set_quoted('SCRCPY_VERSION', meson.project_version())
|
||||||
@ -123,10 +124,8 @@ configure_file(configuration: conf, output: 'config.h')
|
|||||||
src_dir = include_directories('src')
|
src_dir = include_directories('src')
|
||||||
|
|
||||||
if get_option('windows_noconsole')
|
if get_option('windows_noconsole')
|
||||||
c_args = [ '-mwindows' ]
|
link_args = [ '-Wl,--subsystem,windows' ]
|
||||||
link_args = [ '-mwindows' ]
|
|
||||||
else
|
else
|
||||||
c_args = []
|
|
||||||
link_args = []
|
link_args = []
|
||||||
endif
|
endif
|
||||||
|
|
||||||
@ -134,7 +133,7 @@ executable('scrcpy', src,
|
|||||||
dependencies: dependencies,
|
dependencies: dependencies,
|
||||||
include_directories: src_dir,
|
include_directories: src_dir,
|
||||||
install: true,
|
install: true,
|
||||||
c_args: c_args,
|
c_args: [],
|
||||||
link_args: link_args)
|
link_args: link_args)
|
||||||
|
|
||||||
install_man('scrcpy.1')
|
install_man('scrcpy.1')
|
||||||
@ -142,31 +141,43 @@ install_man('scrcpy.1')
|
|||||||
|
|
||||||
### TESTS
|
### TESTS
|
||||||
|
|
||||||
tests = [
|
# do not build tests in release (assertions would not be executed at all)
|
||||||
['test_cbuf', [
|
if get_option('buildtype') == 'debug'
|
||||||
'tests/test_cbuf.c',
|
tests = [
|
||||||
]],
|
['test_buffer_util', [
|
||||||
['test_control_event_serialize', [
|
'tests/test_buffer_util.c'
|
||||||
'tests/test_control_msg_serialize.c',
|
]],
|
||||||
'src/control_msg.c',
|
['test_cbuf', [
|
||||||
'src/str_util.c'
|
'tests/test_cbuf.c',
|
||||||
]],
|
]],
|
||||||
['test_device_event_deserialize', [
|
['test_cli', [
|
||||||
'tests/test_device_msg_deserialize.c',
|
'tests/test_cli.c',
|
||||||
'src/device_msg.c'
|
'src/cli.c',
|
||||||
]],
|
'src/util/str_util.c',
|
||||||
['test_queue', [
|
]],
|
||||||
'tests/test_queue.c',
|
['test_control_event_serialize', [
|
||||||
]],
|
'tests/test_control_msg_serialize.c',
|
||||||
['test_strutil', [
|
'src/control_msg.c',
|
||||||
'tests/test_strutil.c',
|
'src/util/str_util.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/util/str_util.c',
|
||||||
|
]],
|
||||||
|
]
|
||||||
|
|
||||||
foreach t : tests
|
foreach t : tests
|
||||||
exe = executable(t[0], t[1],
|
exe = executable(t[0], t[1],
|
||||||
include_directories: src_dir,
|
include_directories: src_dir,
|
||||||
dependencies: dependencies)
|
dependencies: dependencies,
|
||||||
test(t[0], exe)
|
c_args: ['-DSDL_MAIN_HANDLED'])
|
||||||
endforeach
|
test(t[0], exe)
|
||||||
|
endforeach
|
||||||
|
endif
|
||||||
|
66
app/scrcpy.1
66
app/scrcpy.1
@ -15,6 +15,10 @@ provides display and control of Android devices connected on USB (or over TCP/IP
|
|||||||
|
|
||||||
.SH OPTIONS
|
.SH OPTIONS
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-\-always\-on\-top
|
||||||
|
Make scrcpy window always on top (above other windows).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-b, \-\-bit\-rate " value
|
.BI "\-b, \-\-bit\-rate " value
|
||||||
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
||||||
@ -22,7 +26,7 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are
|
|||||||
Default is 8000000.
|
Default is 8000000.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-c, \-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
||||||
Crop the device screen on the server.
|
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
|
The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any
|
||||||
@ -33,14 +37,14 @@ value is computed on the cropped size.
|
|||||||
.B \-f, \-\-fullscreen
|
.B \-f, \-\-fullscreen
|
||||||
Start in fullscreen.
|
Start in fullscreen.
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-F, \-\-record\-format " format
|
|
||||||
Force recording format (either mp4 or mkv).
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-h, \-\-help
|
.B \-h, \-\-help
|
||||||
Print this help.
|
Print this help.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-max\-fps " value
|
||||||
|
Limit the framerate of screen capture (only supported on devices with Android >= 10).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-m, \-\-max\-size " value
|
.BI "\-m, \-\-max\-size " value
|
||||||
Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved.
|
Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved.
|
||||||
@ -61,6 +65,13 @@ Set the TCP port the client listens on.
|
|||||||
|
|
||||||
Default is 27183.
|
Default is 27183.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-\-prefer\-text
|
||||||
|
Inject alpha characters and space as text events instead of key events.
|
||||||
|
|
||||||
|
This avoids issues when combining multiple keys to enter special characters,
|
||||||
|
but breaks the expected behavior of alpha keys in games (typically WASD).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-push\-target " path
|
.BI "\-\-push\-target " path
|
||||||
Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push".
|
Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push".
|
||||||
@ -73,9 +84,13 @@ Record screen to
|
|||||||
.IR file .
|
.IR file .
|
||||||
|
|
||||||
The format is determined by the
|
The format is determined by the
|
||||||
.B \-F/\-\-record\-format
|
.B \-\-record\-format
|
||||||
option if set, or by the file extension (.mp4 or .mkv).
|
option if set, or by the file extension (.mp4 or .mkv).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-record\-format " format
|
||||||
|
Force recording format (either mp4 or mkv).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-\-render\-expired\-frames
|
.B \-\-render\-expired\-frames
|
||||||
By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency.
|
By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency.
|
||||||
@ -94,18 +109,41 @@ Enable "show touches" on start, disable on quit.
|
|||||||
|
|
||||||
It only shows physical touches (not clicks from scrcpy).
|
It only shows physical touches (not clicks from scrcpy).
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-T, \-\-always\-on\-top
|
|
||||||
Make scrcpy window always on top (above other windows).
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-v, \-\-version
|
.B \-v, \-\-version
|
||||||
Print the version of scrcpy.
|
Print the version of scrcpy.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI \-\-window\-title " text
|
.B \-\-window\-borderless
|
||||||
|
Disable window decorations (display borderless window).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-window\-title " text
|
||||||
Set a custom window title.
|
Set a custom window title.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-window\-x " value
|
||||||
|
Set the initial window horizontal position.
|
||||||
|
|
||||||
|
Default is -1 (automatic).\n
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-window\-y " value
|
||||||
|
Set the initial window vertical position.
|
||||||
|
|
||||||
|
Default is -1 (automatic).\n
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-window\-width " value
|
||||||
|
Set the initial window width.
|
||||||
|
|
||||||
|
Default is 0 (automatic).\n
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-window\-height " value
|
||||||
|
Set the initial window height.
|
||||||
|
|
||||||
|
Default is 0 (automatic).\n
|
||||||
|
|
||||||
.SH SHORTCUTS
|
.SH SHORTCUTS
|
||||||
|
|
||||||
@ -157,6 +195,10 @@ turn screen on
|
|||||||
.B Ctrl+o
|
.B Ctrl+o
|
||||||
turn device screen off (keep mirroring)
|
turn device screen off (keep mirroring)
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B Ctrl+r
|
||||||
|
rotate device screen
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+n
|
.B Ctrl+n
|
||||||
expand notification panel
|
expand notification panel
|
||||||
@ -219,7 +261,7 @@ Copyright \(co 2018 Genymobile
|
|||||||
Genymobile
|
Genymobile
|
||||||
.UE
|
.UE
|
||||||
|
|
||||||
Copyright \(co 2018\-2019
|
Copyright \(co 2018\-2020
|
||||||
.MT rom@rom1v.com
|
.MT rom@rom1v.com
|
||||||
Romain Vimont
|
Romain Vimont
|
||||||
.ME
|
.ME
|
||||||
|
529
app/src/cli.c
Normal file
529
app/src/cli.c
Normal 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
21
app/src/cli.h
Normal 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
|
@ -4,11 +4,14 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
#include "str_util.h"
|
#include "util/str_util.h"
|
||||||
|
|
||||||
static const char *adb_command;
|
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 *));
|
memcpy(&cmd[i], adb_cmd, len * sizeof(const char *));
|
||||||
cmd[len + i] = NULL;
|
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) {
|
if (r != PROCESS_SUCCESS) {
|
||||||
show_adb_err_msg(r, cmd);
|
show_adb_err_msg(r, cmd);
|
||||||
return PROCESS_NONE;
|
return PROCESS_NONE;
|
||||||
@ -202,3 +205,14 @@ process_check_success(process_t proc, const char *name) {
|
|||||||
}
|
}
|
||||||
return true;
|
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);
|
||||||
|
}
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
# define PRIsizet PRIu32
|
# define PRIsizet PRIu32
|
||||||
# endif
|
# endif
|
||||||
# define PROCESS_NONE NULL
|
# define PROCESS_NONE NULL
|
||||||
|
# define NO_EXIT_CODE -1u // max value as unsigned
|
||||||
typedef HANDLE process_t;
|
typedef HANDLE process_t;
|
||||||
typedef DWORD exit_code_t;
|
typedef DWORD exit_code_t;
|
||||||
|
|
||||||
@ -28,6 +29,7 @@
|
|||||||
# define PRIsizet "zu"
|
# define PRIsizet "zu"
|
||||||
# define PRIexitcode "d"
|
# define PRIexitcode "d"
|
||||||
# define PROCESS_NONE -1
|
# define PROCESS_NONE -1
|
||||||
|
# define NO_EXIT_CODE -1
|
||||||
typedef pid_t process_t;
|
typedef pid_t process_t;
|
||||||
typedef int exit_code_t;
|
typedef int exit_code_t;
|
||||||
|
|
||||||
@ -35,8 +37,6 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
# define NO_EXIT_CODE -1
|
|
||||||
|
|
||||||
enum process_result {
|
enum process_result {
|
||||||
PROCESS_SUCCESS,
|
PROCESS_SUCCESS,
|
||||||
PROCESS_ERROR_GENERIC,
|
PROCESS_ERROR_GENERIC,
|
||||||
@ -44,7 +44,7 @@ enum process_result {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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
|
bool
|
||||||
cmd_terminate(process_t pid);
|
cmd_terminate(process_t pid);
|
||||||
@ -85,4 +85,8 @@ process_check_success(process_t proc, const char *name);
|
|||||||
char *
|
char *
|
||||||
get_executable_path(void);
|
get_executable_path(void);
|
||||||
|
|
||||||
|
// returns true if the file exists and is not a directory
|
||||||
|
bool
|
||||||
|
is_regular_file(const char *path);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
#include "control_msg.h"
|
#include "control_msg.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <SDL2/SDL_assert.h>
|
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "buffer_util.h"
|
#include "util/buffer_util.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
#include "str_util.h"
|
#include "util/str_util.h"
|
||||||
|
|
||||||
static void
|
static void
|
||||||
write_position(uint8_t *buf, const struct position *position) {
|
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
|
static uint16_t
|
||||||
to_fixed_point_16(float f) {
|
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
|
uint32_t u = f * 0x1p16f; // 2^16
|
||||||
if (u >= 0xffff) {
|
if (u >= 0xffff) {
|
||||||
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_EXPAND_NOTIFICATION_PANEL:
|
||||||
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
||||||
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
|
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
|
||||||
|
case CONTROL_MSG_TYPE_ROTATE_DEVICE:
|
||||||
// no additional data
|
// no additional data
|
||||||
return 1;
|
return 1;
|
||||||
default:
|
default:
|
||||||
|
@ -28,6 +28,7 @@ enum control_msg_type {
|
|||||||
CONTROL_MSG_TYPE_GET_CLIPBOARD,
|
CONTROL_MSG_TYPE_GET_CLIPBOARD,
|
||||||
CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
||||||
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
|
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
|
||||||
|
CONTROL_MSG_TYPE_ROTATE_DEVICE,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum screen_power_mode {
|
enum screen_power_mode {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
|
|
||||||
#include <SDL2/SDL_assert.h>
|
#include <assert.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "lock_util.h"
|
#include "util/lock.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
bool
|
bool
|
||||||
controller_init(struct controller *controller, socket_t control_socket) {
|
controller_init(struct controller *controller, socket_t control_socket) {
|
||||||
@ -85,7 +85,8 @@ run_controller(void *data) {
|
|||||||
}
|
}
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
bool non_empty = cbuf_take(&controller->queue, &msg);
|
bool non_empty = cbuf_take(&controller->queue, &msg);
|
||||||
SDL_assert(non_empty);
|
assert(non_empty);
|
||||||
|
(void) non_empty;
|
||||||
mutex_unlock(controller->mutex);
|
mutex_unlock(controller->mutex);
|
||||||
|
|
||||||
bool ok = process_msg(controller, &msg);
|
bool ok = process_msg(controller, &msg);
|
||||||
|
@ -6,10 +6,10 @@
|
|||||||
#include <SDL2/SDL_thread.h>
|
#include <SDL2/SDL_thread.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "cbuf.h"
|
|
||||||
#include "control_msg.h"
|
#include "control_msg.h"
|
||||||
#include "net.h"
|
|
||||||
#include "receiver.h"
|
#include "receiver.h"
|
||||||
|
#include "util/cbuf.h"
|
||||||
|
#include "util/net.h"
|
||||||
|
|
||||||
struct control_msg_queue CBUF(struct control_msg, 64);
|
struct control_msg_queue CBUF(struct control_msg, 64);
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
#include <libavutil/time.h>
|
#include <libavutil/time.h>
|
||||||
#include <SDL2/SDL_assert.h>
|
|
||||||
#include <SDL2/SDL_events.h>
|
#include <SDL2/SDL_events.h>
|
||||||
#include <SDL2/SDL_mutex.h>
|
#include <SDL2/SDL_mutex.h>
|
||||||
#include <SDL2/SDL_thread.h>
|
#include <SDL2/SDL_thread.h>
|
||||||
@ -10,12 +9,11 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "compat.h"
|
#include "compat.h"
|
||||||
#include "buffer_util.h"
|
|
||||||
#include "events.h"
|
#include "events.h"
|
||||||
#include "lock_util.h"
|
|
||||||
#include "log.h"
|
|
||||||
#include "recorder.h"
|
#include "recorder.h"
|
||||||
#include "video_buffer.h"
|
#include "video_buffer.h"
|
||||||
|
#include "util/buffer_util.h"
|
||||||
|
#include "util/log.h"
|
||||||
|
|
||||||
// set the decoded frame as ready for rendering, and notify
|
// set the decoded frame as ready for rendering, and notify
|
||||||
static void
|
static void
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#include "device.h"
|
#include "device.h"
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
bool
|
bool
|
||||||
device_read_info(socket_t device_socket, char *device_name, struct size *size) {
|
device_read_info(socket_t device_socket, char *device_name, struct size *size) {
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "net.h"
|
#include "util/net.h"
|
||||||
|
|
||||||
#define DEVICE_NAME_FIELD_LENGTH 64
|
#define DEVICE_NAME_FIELD_LENGTH 64
|
||||||
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
#include "device_msg.h"
|
#include "device_msg.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <SDL2/SDL_assert.h>
|
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "buffer_util.h"
|
#include "util/buffer_util.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
ssize_t
|
ssize_t
|
||||||
device_msg_deserialize(const unsigned char *buf, size_t len,
|
device_msg_deserialize(const unsigned char *buf, size_t len,
|
||||||
|
@ -75,7 +75,8 @@ convert_meta_state(SDL_Keymod mod) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
|
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
|
||||||
|
bool prefer_text) {
|
||||||
switch (from) {
|
switch (from) {
|
||||||
MAP(SDLK_RETURN, AKEYCODE_ENTER);
|
MAP(SDLK_RETURN, AKEYCODE_ENTER);
|
||||||
MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER);
|
MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER);
|
||||||
@ -92,6 +93,12 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
|
|||||||
MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN);
|
MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN);
|
||||||
MAP(SDLK_UP, AKEYCODE_DPAD_UP);
|
MAP(SDLK_UP, AKEYCODE_DPAD_UP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prefer_text) {
|
||||||
|
// do not forward alpha and space key events
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) {
|
if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,8 @@ enum android_metastate
|
|||||||
convert_meta_state(SDL_Keymod mod);
|
convert_meta_state(SDL_Keymod mod);
|
||||||
|
|
||||||
bool
|
bool
|
||||||
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod);
|
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
|
||||||
|
bool prefer_text);
|
||||||
|
|
||||||
enum android_motionevent_buttons
|
enum android_motionevent_buttons
|
||||||
convert_mouse_buttons(uint32_t state);
|
convert_mouse_buttons(uint32_t state);
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
#include "file_handler.h"
|
#include "file_handler.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <SDL2/SDL_assert.h>
|
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "command.h"
|
#include "command.h"
|
||||||
#include "lock_util.h"
|
#include "util/lock.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
#define DEFAULT_PUSH_TARGET "/sdcard/"
|
#define DEFAULT_PUSH_TARGET "/sdcard/"
|
||||||
|
|
||||||
@ -120,7 +120,8 @@ run_file_handler(void *data) {
|
|||||||
}
|
}
|
||||||
struct file_handler_request req;
|
struct file_handler_request req;
|
||||||
bool non_empty = cbuf_take(&file_handler->queue, &req);
|
bool non_empty = cbuf_take(&file_handler->queue, &req);
|
||||||
SDL_assert(non_empty);
|
assert(non_empty);
|
||||||
|
(void) non_empty;
|
||||||
|
|
||||||
process_t process;
|
process_t process;
|
||||||
if (req.action == ACTION_INSTALL_APK) {
|
if (req.action == ACTION_INSTALL_APK) {
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
#include <SDL2/SDL_thread.h>
|
#include <SDL2/SDL_thread.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "cbuf.h"
|
|
||||||
#include "command.h"
|
#include "command.h"
|
||||||
|
#include "util/cbuf.h"
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
ACTION_INSTALL_APK,
|
ACTION_INSTALL_APK,
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
#include "fps_counter.h"
|
#include "fps_counter.h"
|
||||||
|
|
||||||
#include <SDL2/SDL_assert.h>
|
#include <assert.h>
|
||||||
#include <SDL2/SDL_timer.h>
|
#include <SDL2/SDL_timer.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "lock_util.h"
|
#include "util/lock.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
#define FPS_COUNTER_INTERVAL_MS 1000
|
#define FPS_COUNTER_INTERVAL_MS 1000
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ run_fps_counter(void *data) {
|
|||||||
uint32_t now = SDL_GetTicks();
|
uint32_t now = SDL_GetTicks();
|
||||||
check_interval_expired(counter, now);
|
check_interval_expired(counter, now);
|
||||||
|
|
||||||
SDL_assert(counter->next_timestamp > now);
|
assert(counter->next_timestamp > now);
|
||||||
uint32_t remaining = counter->next_timestamp - now;
|
uint32_t remaining = counter->next_timestamp - now;
|
||||||
|
|
||||||
// ignore the reason (timeout or signaled), we just loop anyway
|
// ignore the reason (timeout or signaled), we just loop anyway
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
#include "input_manager.h"
|
#include "input_manager.h"
|
||||||
|
|
||||||
#include <SDL2/SDL_assert.h>
|
#include <assert.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "event_converter.h"
|
#include "event_converter.h"
|
||||||
#include "lock_util.h"
|
#include "util/lock.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
// Convert window coordinates (as provided by SDL_GetMouseState() to renderer
|
// Convert window coordinates (as provided by SDL_GetMouseState() to renderer
|
||||||
// coordinates (as provided in SDL mouse events)
|
// coordinates (as provided in SDL mouse events)
|
||||||
@ -211,15 +211,28 @@ 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
|
void
|
||||||
input_manager_process_text_input(struct input_manager *im,
|
input_manager_process_text_input(struct input_manager *im,
|
||||||
const SDL_TextInputEvent *event) {
|
const SDL_TextInputEvent *event) {
|
||||||
char c = event->text[0];
|
if (!im->prefer_text) {
|
||||||
if (isalpha(c) || c == ' ') {
|
char c = event->text[0];
|
||||||
SDL_assert(event->text[1] == '\0');
|
if (isalpha(c) || c == ' ') {
|
||||||
// letters and space are handled as raw key event
|
assert(event->text[1] == '\0');
|
||||||
return;
|
// letters and space are handled as raw key event
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
|
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
|
||||||
msg.inject_text.text = SDL_strdup(event->text);
|
msg.inject_text.text = SDL_strdup(event->text);
|
||||||
@ -234,7 +247,8 @@ input_manager_process_text_input(struct input_manager *im,
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) {
|
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
|
||||||
|
bool prefer_text) {
|
||||||
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
|
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
|
||||||
|
|
||||||
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
|
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
|
||||||
@ -242,7 +256,8 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint16_t mod = from->keysym.mod;
|
uint16_t mod = from->keysym.mod;
|
||||||
if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) {
|
if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod,
|
||||||
|
prefer_text)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,6 +398,11 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case SDLK_r:
|
||||||
|
if (control && cmd && !shift && !repeat && down) {
|
||||||
|
rotate_device(controller);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -393,7 +413,7 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
if (convert_input_key(event, &msg)) {
|
if (convert_input_key(event, &msg, im->prefer_text)) {
|
||||||
if (!controller_push_msg(controller, &msg)) {
|
if (!controller_push_msg(controller, &msg)) {
|
||||||
LOGW("Could not request 'inject keycode'");
|
LOGW("Could not request 'inject keycode'");
|
||||||
}
|
}
|
||||||
@ -545,13 +565,8 @@ convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
|
|||||||
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
|
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
|
||||||
|
|
||||||
to->inject_scroll_event.position = position;
|
to->inject_scroll_event.position = position;
|
||||||
|
to->inject_scroll_event.hscroll = from->x;
|
||||||
int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
|
to->inject_scroll_event.vscroll = from->y;
|
||||||
// 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;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ struct input_manager {
|
|||||||
struct controller *controller;
|
struct controller *controller;
|
||||||
struct video_buffer *video_buffer;
|
struct video_buffer *video_buffer;
|
||||||
struct screen *screen;
|
struct screen *screen;
|
||||||
|
bool prefer_text;
|
||||||
};
|
};
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -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
|
|
476
app/src/main.c
476
app/src/main.c
@ -1,191 +1,15 @@
|
|||||||
#include "scrcpy.h"
|
#include "scrcpy.h"
|
||||||
|
|
||||||
#include <getopt.h>
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
|
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "cli.h"
|
||||||
#include "compat.h"
|
#include "compat.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
#include "recorder.h"
|
|
||||||
|
|
||||||
struct args {
|
|
||||||
const char *serial;
|
|
||||||
const char *crop;
|
|
||||||
const char *record_filename;
|
|
||||||
const char *window_title;
|
|
||||||
const char *push_target;
|
|
||||||
enum recorder_format record_format;
|
|
||||||
bool fullscreen;
|
|
||||||
bool no_control;
|
|
||||||
bool no_display;
|
|
||||||
bool help;
|
|
||||||
bool version;
|
|
||||||
bool show_touches;
|
|
||||||
uint16_t port;
|
|
||||||
uint16_t max_size;
|
|
||||||
uint32_t bit_rate;
|
|
||||||
bool always_on_top;
|
|
||||||
bool turn_screen_off;
|
|
||||||
bool render_expired_frames;
|
|
||||||
};
|
|
||||||
|
|
||||||
static void usage(const char *arg0) {
|
|
||||||
#ifdef __APPLE__
|
|
||||||
# define CTRL_OR_CMD "Cmd"
|
|
||||||
#else
|
|
||||||
# define CTRL_OR_CMD "Ctrl"
|
|
||||||
#endif
|
|
||||||
fprintf(stderr,
|
|
||||||
"Usage: %s [options]\n"
|
|
||||||
"\n"
|
|
||||||
"Options:\n"
|
|
||||||
"\n"
|
|
||||||
" -b, --bit-rate value\n"
|
|
||||||
" Encode the video at the given bit-rate, expressed in bits/s.\n"
|
|
||||||
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
|
||||||
" Default is %d.\n"
|
|
||||||
"\n"
|
|
||||||
" -c, --crop width:height:x:y\n"
|
|
||||||
" Crop 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"
|
|
||||||
" -F, --record-format format\n"
|
|
||||||
" Force recording format (either mp4 or mkv).\n"
|
|
||||||
"\n"
|
|
||||||
" -h, --help\n"
|
|
||||||
" Print this help.\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"
|
|
||||||
" --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 -F/--record-format option if\n"
|
|
||||||
" set, or by the file extension (.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"
|
|
||||||
" -T, --always-on-top\n"
|
|
||||||
" Make scrcpy window always on top (above other windows).\n"
|
|
||||||
"\n"
|
|
||||||
" -v, --version\n"
|
|
||||||
" Print the version of scrcpy.\n"
|
|
||||||
"\n"
|
|
||||||
" --window-title text\n"
|
|
||||||
" Set a custom window title.\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);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
print_version(void) {
|
print_version(void) {
|
||||||
@ -205,250 +29,6 @@ print_version(void) {
|
|||||||
LIBAVUTIL_VERSION_MICRO);
|
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_port(char *optarg, uint16_t *port) {
|
|
||||||
char *endptr;
|
|
||||||
if (*optarg == '\0') {
|
|
||||||
LOGE("Invalid 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
|
|
||||||
|
|
||||||
static bool
|
|
||||||
parse_args(struct args *args, int argc, char *argv[]) {
|
|
||||||
static const struct option long_options[] = {
|
|
||||||
{"always-on-top", no_argument, NULL, 'T'},
|
|
||||||
{"bit-rate", required_argument, NULL, 'b'},
|
|
||||||
{"crop", required_argument, NULL, 'c'},
|
|
||||||
{"fullscreen", no_argument, NULL, 'f'},
|
|
||||||
{"help", no_argument, NULL, 'h'},
|
|
||||||
{"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, 'F'},
|
|
||||||
{"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'},
|
|
||||||
{"version", no_argument, NULL, 'v'},
|
|
||||||
{"window-title", required_argument, NULL,
|
|
||||||
OPT_WINDOW_TITLE},
|
|
||||||
{NULL, 0, NULL, 0 },
|
|
||||||
};
|
|
||||||
int c;
|
|
||||||
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options,
|
|
||||||
NULL)) != -1) {
|
|
||||||
switch (c) {
|
|
||||||
case 'b':
|
|
||||||
if (!parse_bit_rate(optarg, &args->bit_rate)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'c':
|
|
||||||
args->crop = optarg;
|
|
||||||
break;
|
|
||||||
case 'f':
|
|
||||||
args->fullscreen = true;
|
|
||||||
break;
|
|
||||||
case 'F':
|
|
||||||
if (!parse_record_format(optarg, &args->record_format)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'h':
|
|
||||||
args->help = true;
|
|
||||||
break;
|
|
||||||
case 'm':
|
|
||||||
if (!parse_max_size(optarg, &args->max_size)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'n':
|
|
||||||
args->no_control = true;
|
|
||||||
break;
|
|
||||||
case 'N':
|
|
||||||
args->no_display = true;
|
|
||||||
break;
|
|
||||||
case 'p':
|
|
||||||
if (!parse_port(optarg, &args->port)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'r':
|
|
||||||
args->record_filename = optarg;
|
|
||||||
break;
|
|
||||||
case 's':
|
|
||||||
args->serial = optarg;
|
|
||||||
break;
|
|
||||||
case 'S':
|
|
||||||
args->turn_screen_off = true;
|
|
||||||
break;
|
|
||||||
case 't':
|
|
||||||
args->show_touches = true;
|
|
||||||
break;
|
|
||||||
case 'T':
|
|
||||||
args->always_on_top = true;
|
|
||||||
break;
|
|
||||||
case 'v':
|
|
||||||
args->version = true;
|
|
||||||
break;
|
|
||||||
case OPT_RENDER_EXPIRED_FRAMES:
|
|
||||||
args->render_expired_frames = true;
|
|
||||||
break;
|
|
||||||
case OPT_WINDOW_TITLE:
|
|
||||||
args->window_title = optarg;
|
|
||||||
break;
|
|
||||||
case OPT_PUSH_TARGET:
|
|
||||||
args->push_target = optarg;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// getopt prints the error message on stderr
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args->no_display && !args->record_filename) {
|
|
||||||
LOGE("-N/--no-display requires screen recording (-r/--record)");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args->no_display && args->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 (args->record_format && !args->record_filename) {
|
|
||||||
LOGE("Record format specified without recording");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args->record_filename && !args->record_format) {
|
|
||||||
args->record_format = guess_record_format(args->record_filename);
|
|
||||||
if (!args->record_format) {
|
|
||||||
LOGE("No format specified for \"%s\" (try with -F mkv)",
|
|
||||||
args->record_filename);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args->no_control && args->turn_screen_off) {
|
|
||||||
LOGE("Could not request to turn screen off if control is disabled");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
int
|
||||||
main(int argc, char *argv[]) {
|
main(int argc, char *argv[]) {
|
||||||
#ifdef __WINDOWS__
|
#ifdef __WINDOWS__
|
||||||
@ -457,31 +37,23 @@ main(int argc, char *argv[]) {
|
|||||||
setbuf(stdout, NULL);
|
setbuf(stdout, NULL);
|
||||||
setbuf(stderr, NULL);
|
setbuf(stderr, NULL);
|
||||||
#endif
|
#endif
|
||||||
struct args args = {
|
|
||||||
.serial = NULL,
|
#ifndef NDEBUG
|
||||||
.crop = NULL,
|
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
|
||||||
.record_filename = NULL,
|
#endif
|
||||||
.window_title = NULL,
|
|
||||||
.push_target = NULL,
|
struct scrcpy_cli_args args = {
|
||||||
.record_format = 0,
|
.opts = SCRCPY_OPTIONS_DEFAULT,
|
||||||
.help = false,
|
.help = false,
|
||||||
.version = false,
|
.version = false,
|
||||||
.show_touches = false,
|
|
||||||
.port = DEFAULT_LOCAL_PORT,
|
|
||||||
.max_size = DEFAULT_MAX_SIZE,
|
|
||||||
.bit_rate = DEFAULT_BIT_RATE,
|
|
||||||
.always_on_top = false,
|
|
||||||
.no_control = false,
|
|
||||||
.no_display = false,
|
|
||||||
.turn_screen_off = false,
|
|
||||||
.render_expired_frames = false,
|
|
||||||
};
|
};
|
||||||
if (!parse_args(&args, argc, argv)) {
|
|
||||||
|
if (!scrcpy_parse_args(&args, argc, argv)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.help) {
|
if (args.help) {
|
||||||
usage(argv[0]);
|
scrcpy_print_usage(argv[0]);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -500,29 +72,7 @@ main(int argc, char *argv[]) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef BUILD_DEBUG
|
int res = scrcpy(&args.opts) ? 0 : 1;
|
||||||
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct scrcpy_options options = {
|
|
||||||
.serial = args.serial,
|
|
||||||
.crop = args.crop,
|
|
||||||
.port = args.port,
|
|
||||||
.record_filename = args.record_filename,
|
|
||||||
.window_title = args.window_title,
|
|
||||||
.push_target = args.push_target,
|
|
||||||
.record_format = args.record_format,
|
|
||||||
.max_size = args.max_size,
|
|
||||||
.bit_rate = args.bit_rate,
|
|
||||||
.show_touches = args.show_touches,
|
|
||||||
.fullscreen = args.fullscreen,
|
|
||||||
.always_on_top = args.always_on_top,
|
|
||||||
.control = !args.no_control,
|
|
||||||
.display = !args.no_display,
|
|
||||||
.turn_screen_off = args.turn_screen_off,
|
|
||||||
.render_expired_frames = args.render_expired_frames,
|
|
||||||
};
|
|
||||||
int res = scrcpy(&options) ? 0 : 1;
|
|
||||||
|
|
||||||
avformat_network_deinit(); // ignore failure
|
avformat_network_deinit(); // ignore failure
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
#include "receiver.h"
|
#include "receiver.h"
|
||||||
|
|
||||||
#include <SDL2/SDL_assert.h>
|
#include <assert.h>
|
||||||
#include <SDL2/SDL_clipboard.h>
|
#include <SDL2/SDL_clipboard.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "device_msg.h"
|
#include "device_msg.h"
|
||||||
#include "lock_util.h"
|
#include "util/lock.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
bool
|
bool
|
||||||
receiver_init(struct receiver *receiver, socket_t control_socket) {
|
receiver_init(struct receiver *receiver, socket_t control_socket) {
|
||||||
@ -23,7 +23,7 @@ receiver_destroy(struct receiver *receiver) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
process_msg(struct receiver *receiver, struct device_msg *msg) {
|
process_msg(struct device_msg *msg) {
|
||||||
switch (msg->type) {
|
switch (msg->type) {
|
||||||
case DEVICE_MSG_TYPE_CLIPBOARD:
|
case DEVICE_MSG_TYPE_CLIPBOARD:
|
||||||
LOGI("Device clipboard copied");
|
LOGI("Device clipboard copied");
|
||||||
@ -33,7 +33,7 @@ process_msg(struct receiver *receiver, struct device_msg *msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static ssize_t
|
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;
|
size_t head = 0;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
struct device_msg msg;
|
struct device_msg msg;
|
||||||
@ -45,11 +45,11 @@ process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) {
|
|||||||
return head;
|
return head;
|
||||||
}
|
}
|
||||||
|
|
||||||
process_msg(receiver, &msg);
|
process_msg(&msg);
|
||||||
device_msg_destroy(&msg);
|
device_msg_destroy(&msg);
|
||||||
|
|
||||||
head += r;
|
head += r;
|
||||||
SDL_assert(head <= len);
|
assert(head <= len);
|
||||||
if (head == len) {
|
if (head == len) {
|
||||||
return head;
|
return head;
|
||||||
}
|
}
|
||||||
@ -64,7 +64,7 @@ run_receiver(void *data) {
|
|||||||
size_t head = 0;
|
size_t head = 0;
|
||||||
|
|
||||||
for (;;) {
|
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,
|
ssize_t r = net_recv(receiver->control_socket, buf,
|
||||||
DEVICE_MSG_SERIALIZED_MAX_SIZE - head);
|
DEVICE_MSG_SERIALIZED_MAX_SIZE - head);
|
||||||
if (r <= 0) {
|
if (r <= 0) {
|
||||||
@ -72,7 +72,7 @@ run_receiver(void *data) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ssize_t consumed = process_msgs(receiver, buf, r);
|
ssize_t consumed = process_msgs(buf, r);
|
||||||
if (consumed == -1) {
|
if (consumed == -1) {
|
||||||
// an error occurred
|
// an error occurred
|
||||||
break;
|
break;
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
#include <SDL2/SDL_thread.h>
|
#include <SDL2/SDL_thread.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "net.h"
|
#include "util/net.h"
|
||||||
|
|
||||||
// receive events from the device
|
// receive events from the device
|
||||||
// managed by the controller
|
// managed by the controller
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
#include "recorder.h"
|
#include "recorder.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
#include <libavutil/time.h>
|
#include <libavutil/time.h>
|
||||||
#include <SDL2/SDL_assert.h>
|
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "compat.h"
|
#include "compat.h"
|
||||||
#include "lock_util.h"
|
#include "util/lock.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
|
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
|
||||||
|
|
||||||
@ -116,7 +116,7 @@ recorder_get_format_name(enum recorder_format format) {
|
|||||||
bool
|
bool
|
||||||
recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
|
recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
|
||||||
const char *format_name = recorder_get_format_name(recorder->format);
|
const char *format_name = recorder_get_format_name(recorder->format);
|
||||||
SDL_assert(format_name);
|
assert(format_name);
|
||||||
const AVOutputFormat *format = find_muxer(format_name);
|
const AVOutputFormat *format = find_muxer(format_name);
|
||||||
if (!format) {
|
if (!format) {
|
||||||
LOGE("Could not find muxer");
|
LOGE("Could not find muxer");
|
||||||
@ -174,9 +174,14 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
recorder_close(struct recorder *recorder) {
|
recorder_close(struct recorder *recorder) {
|
||||||
int ret = av_write_trailer(recorder->ctx);
|
if (recorder->header_written) {
|
||||||
if (ret < 0) {
|
int ret = av_write_trailer(recorder->ctx);
|
||||||
LOGE("Failed to write trailer to %s", recorder->filename);
|
if (ret < 0) {
|
||||||
|
LOGE("Failed to write trailer to %s", recorder->filename);
|
||||||
|
recorder->failed = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// the recorded file is empty
|
||||||
recorder->failed = true;
|
recorder->failed = true;
|
||||||
}
|
}
|
||||||
avio_close(recorder->ctx->pb);
|
avio_close(recorder->ctx->pb);
|
||||||
@ -296,8 +301,12 @@ run_recorder(void *data) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we now know the duration of the previous packet
|
// config packets have no PTS, we must ignore them
|
||||||
previous->packet.duration = rec->packet.pts - previous->packet.pts;
|
if (rec->packet.pts != AV_NOPTS_VALUE
|
||||||
|
&& previous->packet.pts != AV_NOPTS_VALUE) {
|
||||||
|
// we now know the duration of the previous packet
|
||||||
|
previous->packet.duration = rec->packet.pts - previous->packet.pts;
|
||||||
|
}
|
||||||
|
|
||||||
bool ok = recorder_write(recorder, &previous->packet);
|
bool ok = recorder_write(recorder, &previous->packet);
|
||||||
record_packet_delete(previous);
|
record_packet_delete(previous);
|
||||||
@ -348,7 +357,7 @@ recorder_join(struct recorder *recorder) {
|
|||||||
bool
|
bool
|
||||||
recorder_push(struct recorder *recorder, const AVPacket *packet) {
|
recorder_push(struct recorder *recorder, const AVPacket *packet) {
|
||||||
mutex_lock(recorder->mutex);
|
mutex_lock(recorder->mutex);
|
||||||
SDL_assert(!recorder->stopped);
|
assert(!recorder->stopped);
|
||||||
|
|
||||||
if (recorder->failed) {
|
if (recorder->failed) {
|
||||||
// reject any new packet (this will stop the stream)
|
// reject any new packet (this will stop the stream)
|
||||||
|
@ -8,10 +8,11 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "queue.h"
|
#include "util/queue.h"
|
||||||
|
|
||||||
enum recorder_format {
|
enum recorder_format {
|
||||||
RECORDER_FORMAT_MP4 = 1,
|
RECORDER_FORMAT_AUTO,
|
||||||
|
RECORDER_FORMAT_MP4,
|
||||||
RECORDER_FORMAT_MKV,
|
RECORDER_FORMAT_MKV,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,15 +18,15 @@
|
|||||||
#include "file_handler.h"
|
#include "file_handler.h"
|
||||||
#include "fps_counter.h"
|
#include "fps_counter.h"
|
||||||
#include "input_manager.h"
|
#include "input_manager.h"
|
||||||
#include "log.h"
|
|
||||||
#include "lock_util.h"
|
|
||||||
#include "net.h"
|
|
||||||
#include "recorder.h"
|
#include "recorder.h"
|
||||||
#include "screen.h"
|
#include "screen.h"
|
||||||
#include "server.h"
|
#include "server.h"
|
||||||
#include "stream.h"
|
#include "stream.h"
|
||||||
#include "tiny_xpm.h"
|
#include "tiny_xpm.h"
|
||||||
#include "video_buffer.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 server server = SERVER_INITIALIZER;
|
||||||
static struct screen screen = SCREEN_INITIALIZER;
|
static struct screen screen = SCREEN_INITIALIZER;
|
||||||
@ -42,6 +42,7 @@ static struct input_manager input_manager = {
|
|||||||
.controller = &controller,
|
.controller = &controller,
|
||||||
.video_buffer = &video_buffer,
|
.video_buffer = &video_buffer,
|
||||||
.screen = &screen,
|
.screen = &screen,
|
||||||
|
.prefer_text = false, // initialized later
|
||||||
};
|
};
|
||||||
|
|
||||||
// init SDL and set appropriate hints
|
// init SDL and set appropriate hints
|
||||||
@ -102,6 +103,7 @@ sdl_init_and_configure(bool display) {
|
|||||||
// <https://stackoverflow.com/a/40693139/1987178>
|
// <https://stackoverflow.com/a/40693139/1987178>
|
||||||
static int
|
static int
|
||||||
event_watcher(void *data, SDL_Event *event) {
|
event_watcher(void *data, SDL_Event *event) {
|
||||||
|
(void) data;
|
||||||
if (event->type == SDL_WINDOWEVENT
|
if (event->type == SDL_WINDOWEVENT
|
||||||
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
|
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||||
// called from another thread, not very safe, but it's a workaround!
|
// called from another thread, not very safe, but it's a workaround!
|
||||||
@ -143,12 +145,7 @@ handle_event(SDL_Event *event, bool control) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case SDL_WINDOWEVENT:
|
case SDL_WINDOWEVENT:
|
||||||
switch (event->window.event) {
|
screen_handle_window_event(&screen, &event->window);
|
||||||
case SDL_WINDOWEVENT_EXPOSED:
|
|
||||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
|
||||||
screen_render(&screen);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case SDL_TEXTINPUT:
|
case SDL_TEXTINPUT:
|
||||||
if (!control) {
|
if (!control) {
|
||||||
@ -205,6 +202,7 @@ handle_event(SDL_Event *event, bool control) {
|
|||||||
|
|
||||||
static bool
|
static bool
|
||||||
event_loop(bool display, bool control) {
|
event_loop(bool display, bool control) {
|
||||||
|
(void) display;
|
||||||
#ifdef CONTINUOUS_RESIZING_WORKAROUND
|
#ifdef CONTINUOUS_RESIZING_WORKAROUND
|
||||||
if (display) {
|
if (display) {
|
||||||
SDL_AddEventWatch(event_watcher, NULL);
|
SDL_AddEventWatch(event_watcher, NULL);
|
||||||
@ -217,6 +215,7 @@ event_loop(bool display, bool control) {
|
|||||||
case EVENT_RESULT_STOPPED_BY_USER:
|
case EVENT_RESULT_STOPPED_BY_USER:
|
||||||
return true;
|
return true;
|
||||||
case EVENT_RESULT_STOPPED_BY_EOS:
|
case EVENT_RESULT_STOPPED_BY_EOS:
|
||||||
|
LOGW("Device disconnected");
|
||||||
return false;
|
return false;
|
||||||
case EVENT_RESULT_CONTINUE:
|
case EVENT_RESULT_CONTINUE:
|
||||||
break;
|
break;
|
||||||
@ -259,6 +258,7 @@ sdl_priority_from_av_level(int level) {
|
|||||||
|
|
||||||
static void
|
static void
|
||||||
av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
||||||
|
(void) avcl;
|
||||||
SDL_LogPriority priority = sdl_priority_from_av_level(level);
|
SDL_LogPriority priority = sdl_priority_from_av_level(level);
|
||||||
if (priority == 0) {
|
if (priority == 0) {
|
||||||
return;
|
return;
|
||||||
@ -283,6 +283,7 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
.local_port = options->port,
|
.local_port = options->port,
|
||||||
.max_size = options->max_size,
|
.max_size = options->max_size,
|
||||||
.bit_rate = options->bit_rate,
|
.bit_rate = options->bit_rate,
|
||||||
|
.max_fps = options->max_fps,
|
||||||
.control = options->control,
|
.control = options->control,
|
||||||
};
|
};
|
||||||
if (!server_start(&server, options->serial, ¶ms)) {
|
if (!server_start(&server, options->serial, ¶ms)) {
|
||||||
@ -390,7 +391,10 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
options->window_title ? options->window_title : device_name;
|
options->window_title ? options->window_title : device_name;
|
||||||
|
|
||||||
if (!screen_init_rendering(&screen, window_title, frame_size,
|
if (!screen_init_rendering(&screen, window_title, frame_size,
|
||||||
options->always_on_top)) {
|
options->always_on_top, options->window_x,
|
||||||
|
options->window_y, options->window_width,
|
||||||
|
options->window_height,
|
||||||
|
options->window_borderless)) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,6 +418,8 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
show_touches_waited = true;
|
show_touches_waited = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input_manager.prefer_text = options->prefer_text;
|
||||||
|
|
||||||
ret = event_loop(options->display, options->control);
|
ret = event_loop(options->display, options->control);
|
||||||
LOGD("quit...");
|
LOGD("quit...");
|
||||||
|
|
||||||
|
@ -3,9 +3,10 @@
|
|||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <recorder.h>
|
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "input_manager.h"
|
||||||
|
#include "recorder.h"
|
||||||
|
|
||||||
struct scrcpy_options {
|
struct scrcpy_options {
|
||||||
const char *serial;
|
const char *serial;
|
||||||
@ -17,6 +18,11 @@ struct scrcpy_options {
|
|||||||
uint16_t port;
|
uint16_t port;
|
||||||
uint16_t max_size;
|
uint16_t max_size;
|
||||||
uint32_t bit_rate;
|
uint32_t bit_rate;
|
||||||
|
uint16_t max_fps;
|
||||||
|
int16_t window_x;
|
||||||
|
int16_t window_y;
|
||||||
|
uint16_t window_width;
|
||||||
|
uint16_t window_height;
|
||||||
bool show_touches;
|
bool show_touches;
|
||||||
bool fullscreen;
|
bool fullscreen;
|
||||||
bool always_on_top;
|
bool always_on_top;
|
||||||
@ -24,8 +30,36 @@ struct scrcpy_options {
|
|||||||
bool display;
|
bool display;
|
||||||
bool turn_screen_off;
|
bool turn_screen_off;
|
||||||
bool render_expired_frames;
|
bool render_expired_frames;
|
||||||
|
bool prefer_text;
|
||||||
|
bool window_borderless;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#define SCRCPY_OPTIONS_DEFAULT { \
|
||||||
|
.serial = NULL, \
|
||||||
|
.crop = NULL, \
|
||||||
|
.record_filename = NULL, \
|
||||||
|
.window_title = NULL, \
|
||||||
|
.push_target = NULL, \
|
||||||
|
.record_format = RECORDER_FORMAT_AUTO, \
|
||||||
|
.port = DEFAULT_LOCAL_PORT, \
|
||||||
|
.max_size = DEFAULT_MAX_SIZE, \
|
||||||
|
.bit_rate = DEFAULT_BIT_RATE, \
|
||||||
|
.max_fps = 0, \
|
||||||
|
.window_x = -1, \
|
||||||
|
.window_y = -1, \
|
||||||
|
.window_width = 0, \
|
||||||
|
.window_height = 0, \
|
||||||
|
.show_touches = false, \
|
||||||
|
.fullscreen = false, \
|
||||||
|
.always_on_top = false, \
|
||||||
|
.control = true, \
|
||||||
|
.display = true, \
|
||||||
|
.turn_screen_off = false, \
|
||||||
|
.render_expired_frames = false, \
|
||||||
|
.prefer_text = false, \
|
||||||
|
.window_borderless = false, \
|
||||||
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
scrcpy(const struct scrcpy_options *options);
|
scrcpy(const struct scrcpy_options *options);
|
||||||
|
|
||||||
|
153
app/src/screen.c
153
app/src/screen.c
@ -1,5 +1,6 @@
|
|||||||
#include "screen.h"
|
#include "screen.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
@ -7,10 +8,10 @@
|
|||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "compat.h"
|
#include "compat.h"
|
||||||
#include "icon.xpm"
|
#include "icon.xpm"
|
||||||
#include "lock_util.h"
|
|
||||||
#include "log.h"
|
|
||||||
#include "tiny_xpm.h"
|
#include "tiny_xpm.h"
|
||||||
#include "video_buffer.h"
|
#include "video_buffer.h"
|
||||||
|
#include "util/lock.h"
|
||||||
|
#include "util/log.h"
|
||||||
|
|
||||||
#define DISPLAY_MARGINS 96
|
#define DISPLAY_MARGINS 96
|
||||||
|
|
||||||
@ -30,23 +31,28 @@ get_window_size(SDL_Window *window) {
|
|||||||
// get the windowed window size
|
// get the windowed window size
|
||||||
static struct size
|
static struct size
|
||||||
get_windowed_window_size(const struct screen *screen) {
|
get_windowed_window_size(const struct screen *screen) {
|
||||||
if (screen->fullscreen) {
|
if (screen->fullscreen || screen->maximized) {
|
||||||
return screen->windowed_window_size;
|
return screen->windowed_window_size;
|
||||||
}
|
}
|
||||||
return get_window_size(screen->window);
|
return get_window_size(screen->window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply the windowed window size if fullscreen and maximized are disabled
|
||||||
|
static void
|
||||||
|
apply_windowed_size(struct screen *screen) {
|
||||||
|
if (!screen->fullscreen && !screen->maximized) {
|
||||||
|
SDL_SetWindowSize(screen->window, screen->windowed_window_size.width,
|
||||||
|
screen->windowed_window_size.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// set the window size to be applied when fullscreen is disabled
|
// set the window size to be applied when fullscreen is disabled
|
||||||
static void
|
static void
|
||||||
set_window_size(struct screen *screen, struct size new_size) {
|
set_window_size(struct screen *screen, struct size new_size) {
|
||||||
// setting the window size during fullscreen is implementation defined,
|
// setting the window size during fullscreen is implementation defined,
|
||||||
// so apply the resize only after fullscreen is disabled
|
// so apply the resize only after fullscreen is disabled
|
||||||
if (screen->fullscreen) {
|
screen->windowed_window_size = new_size;
|
||||||
// SDL_SetWindowSize will be called when fullscreen will be disabled
|
apply_windowed_size(screen);
|
||||||
screen->windowed_window_size = new_size;
|
|
||||||
} else {
|
|
||||||
SDL_SetWindowSize(screen->window, new_size.width, new_size.height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the preferred display bounds (i.e. the screen bounds with some margins)
|
// get the preferred display bounds (i.e. the screen bounds with some margins)
|
||||||
@ -105,7 +111,7 @@ get_optimal_size(struct size current_size, struct size frame_size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// w and h must fit into 16 bits
|
// 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};
|
return (struct size) {w, h};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,9 +123,30 @@ get_optimal_window_size(const struct screen *screen, struct size frame_size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initially, there is no current size, so use the frame size as current size
|
// initially, there is no current size, so use the frame size as current size
|
||||||
|
// req_width and req_height, if not 0, are the sizes requested by the user
|
||||||
static inline struct size
|
static inline struct size
|
||||||
get_initial_optimal_size(struct size frame_size) {
|
get_initial_optimal_size(struct size frame_size, uint16_t req_width,
|
||||||
return get_optimal_size(frame_size, frame_size);
|
uint16_t req_height) {
|
||||||
|
struct size window_size;
|
||||||
|
if (!req_width && !req_height) {
|
||||||
|
window_size = get_optimal_size(frame_size, frame_size);
|
||||||
|
} else {
|
||||||
|
if (req_width) {
|
||||||
|
window_size.width = req_width;
|
||||||
|
} else {
|
||||||
|
// compute from the requested height
|
||||||
|
window_size.width = (uint32_t) req_height * frame_size.width
|
||||||
|
/ frame_size.height;
|
||||||
|
}
|
||||||
|
if (req_height) {
|
||||||
|
window_size.height = req_height;
|
||||||
|
} else {
|
||||||
|
// compute from the requested width
|
||||||
|
window_size.height = (uint32_t) req_width * frame_size.height
|
||||||
|
/ frame_size.width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return window_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -136,10 +163,13 @@ create_texture(SDL_Renderer *renderer, struct size frame_size) {
|
|||||||
|
|
||||||
bool
|
bool
|
||||||
screen_init_rendering(struct screen *screen, const char *window_title,
|
screen_init_rendering(struct screen *screen, const char *window_title,
|
||||||
struct size frame_size, bool always_on_top) {
|
struct size frame_size, bool always_on_top,
|
||||||
|
int16_t window_x, int16_t window_y, uint16_t window_width,
|
||||||
|
uint16_t window_height, bool window_borderless) {
|
||||||
screen->frame_size = frame_size;
|
screen->frame_size = frame_size;
|
||||||
|
|
||||||
struct size window_size = get_initial_optimal_size(frame_size);
|
struct size window_size =
|
||||||
|
get_initial_optimal_size(frame_size, window_width, window_height);
|
||||||
uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
|
uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
|
||||||
#ifdef HIDPI_SUPPORT
|
#ifdef HIDPI_SUPPORT
|
||||||
window_flags |= SDL_WINDOW_ALLOW_HIGHDPI;
|
window_flags |= SDL_WINDOW_ALLOW_HIGHDPI;
|
||||||
@ -152,9 +182,13 @@ screen_init_rendering(struct screen *screen, const char *window_title,
|
|||||||
"(compile with SDL >= 2.0.5 to enable it)");
|
"(compile with SDL >= 2.0.5 to enable it)");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
if (window_borderless) {
|
||||||
|
window_flags |= SDL_WINDOW_BORDERLESS;
|
||||||
|
}
|
||||||
|
|
||||||
screen->window = SDL_CreateWindow(window_title, SDL_WINDOWPOS_UNDEFINED,
|
int x = window_x != -1 ? window_x : (int) SDL_WINDOWPOS_UNDEFINED;
|
||||||
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_size.width, window_size.height,
|
||||||
window_flags);
|
window_flags);
|
||||||
if (!screen->window) {
|
if (!screen->window) {
|
||||||
@ -194,6 +228,8 @@ screen_init_rendering(struct screen *screen, const char *window_title,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
screen->windowed_window_size = window_size;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,10 +323,6 @@ screen_render(struct screen *screen) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
screen_switch_fullscreen(struct screen *screen) {
|
screen_switch_fullscreen(struct screen *screen) {
|
||||||
if (!screen->fullscreen) {
|
|
||||||
// going to fullscreen, store the current windowed window size
|
|
||||||
screen->windowed_window_size = get_window_size(screen->window);
|
|
||||||
}
|
|
||||||
uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
|
uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||||
if (SDL_SetWindowFullscreen(screen->window, new_mode)) {
|
if (SDL_SetWindowFullscreen(screen->window, new_mode)) {
|
||||||
LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
|
LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
|
||||||
@ -298,11 +330,7 @@ screen_switch_fullscreen(struct screen *screen) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
screen->fullscreen = !screen->fullscreen;
|
screen->fullscreen = !screen->fullscreen;
|
||||||
if (!screen->fullscreen) {
|
apply_windowed_size(screen);
|
||||||
// fullscreen disabled, restore expected windowed window size
|
|
||||||
SDL_SetWindowSize(screen->window, screen->windowed_window_size.width,
|
|
||||||
screen->windowed_window_size.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
|
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
|
||||||
screen_render(screen);
|
screen_render(screen);
|
||||||
@ -310,20 +338,75 @@ screen_switch_fullscreen(struct screen *screen) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
screen_resize_to_fit(struct screen *screen) {
|
screen_resize_to_fit(struct screen *screen) {
|
||||||
if (!screen->fullscreen) {
|
if (screen->fullscreen) {
|
||||||
struct size optimal_size = get_optimal_window_size(screen,
|
return;
|
||||||
screen->frame_size);
|
|
||||||
SDL_SetWindowSize(screen->window, optimal_size.width,
|
|
||||||
optimal_size.height);
|
|
||||||
LOGD("Resized to optimal size");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (screen->maximized) {
|
||||||
|
SDL_RestoreWindow(screen->window);
|
||||||
|
screen->maximized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct size optimal_size =
|
||||||
|
get_optimal_window_size(screen, screen->frame_size);
|
||||||
|
SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height);
|
||||||
|
LOGD("Resized to optimal size");
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
screen_resize_to_pixel_perfect(struct screen *screen) {
|
screen_resize_to_pixel_perfect(struct screen *screen) {
|
||||||
if (!screen->fullscreen) {
|
if (screen->fullscreen) {
|
||||||
SDL_SetWindowSize(screen->window, screen->frame_size.width,
|
return;
|
||||||
screen->frame_size.height);
|
}
|
||||||
LOGD("Resized to pixel-perfect");
|
|
||||||
|
if (screen->maximized) {
|
||||||
|
SDL_RestoreWindow(screen->window);
|
||||||
|
screen->maximized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SetWindowSize(screen->window, screen->frame_size.width,
|
||||||
|
screen->frame_size.height);
|
||||||
|
LOGD("Resized to pixel-perfect");
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
screen_handle_window_event(struct screen *screen,
|
||||||
|
const SDL_WindowEvent *event) {
|
||||||
|
switch (event->event) {
|
||||||
|
case SDL_WINDOWEVENT_EXPOSED:
|
||||||
|
screen_render(screen);
|
||||||
|
break;
|
||||||
|
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||||
|
if (!screen->fullscreen && !screen->maximized) {
|
||||||
|
// Backup the previous size: if we receive the MAXIMIZED event,
|
||||||
|
// then the new size must be ignored (it's the maximized size).
|
||||||
|
// We could not rely on the window flags due to race conditions
|
||||||
|
// (they could be updated asynchronously, at least on X11).
|
||||||
|
screen->windowed_window_size_backup =
|
||||||
|
screen->windowed_window_size;
|
||||||
|
|
||||||
|
// Save the windowed size, so that it is available once the
|
||||||
|
// window is maximized or fullscreen is enabled.
|
||||||
|
screen->windowed_window_size = get_window_size(screen->window);
|
||||||
|
}
|
||||||
|
screen_render(screen);
|
||||||
|
break;
|
||||||
|
case SDL_WINDOWEVENT_MAXIMIZED:
|
||||||
|
// The backup size must be non-nul.
|
||||||
|
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
|
||||||
|
// Reset the backup to invalid values to detect unexpected usage
|
||||||
|
screen->windowed_window_size_backup.width = 0;
|
||||||
|
screen->windowed_window_size_backup.height = 0;
|
||||||
|
#endif
|
||||||
|
screen->maximized = true;
|
||||||
|
break;
|
||||||
|
case SDL_WINDOWEVENT_RESTORED:
|
||||||
|
screen->maximized = false;
|
||||||
|
apply_windowed_size(screen);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,28 +15,37 @@ struct screen {
|
|||||||
SDL_Renderer *renderer;
|
SDL_Renderer *renderer;
|
||||||
SDL_Texture *texture;
|
SDL_Texture *texture;
|
||||||
struct size frame_size;
|
struct size frame_size;
|
||||||
//used only in fullscreen mode to know the windowed window size
|
// The window size the last time it was not maximized or fullscreen.
|
||||||
struct size windowed_window_size;
|
struct size windowed_window_size;
|
||||||
|
// Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be
|
||||||
|
// able to revert the size to its non-maximized value.
|
||||||
|
struct size windowed_window_size_backup;
|
||||||
bool has_frame;
|
bool has_frame;
|
||||||
bool fullscreen;
|
bool fullscreen;
|
||||||
|
bool maximized;
|
||||||
bool no_window;
|
bool no_window;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define SCREEN_INITIALIZER { \
|
#define SCREEN_INITIALIZER { \
|
||||||
.window = NULL, \
|
.window = NULL, \
|
||||||
.renderer = NULL, \
|
.renderer = NULL, \
|
||||||
.texture = NULL, \
|
.texture = NULL, \
|
||||||
.frame_size = { \
|
.frame_size = { \
|
||||||
.width = 0, \
|
.width = 0, \
|
||||||
.height = 0, \
|
.height = 0, \
|
||||||
}, \
|
}, \
|
||||||
.windowed_window_size = { \
|
.windowed_window_size = { \
|
||||||
.width = 0, \
|
.width = 0, \
|
||||||
.height = 0, \
|
.height = 0, \
|
||||||
}, \
|
}, \
|
||||||
.has_frame = false, \
|
.windowed_window_size_backup = { \
|
||||||
.fullscreen = false, \
|
.width = 0, \
|
||||||
.no_window = false, \
|
.height = 0, \
|
||||||
|
}, \
|
||||||
|
.has_frame = false, \
|
||||||
|
.fullscreen = false, \
|
||||||
|
.maximized = false, \
|
||||||
|
.no_window = false, \
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize default values
|
// initialize default values
|
||||||
@ -46,7 +55,9 @@ screen_init(struct screen *screen);
|
|||||||
// initialize screen, create window, renderer and texture (window is hidden)
|
// initialize screen, create window, renderer and texture (window is hidden)
|
||||||
bool
|
bool
|
||||||
screen_init_rendering(struct screen *screen, const char *window_title,
|
screen_init_rendering(struct screen *screen, const char *window_title,
|
||||||
struct size frame_size, bool always_on_top);
|
struct size frame_size, bool always_on_top,
|
||||||
|
int16_t window_x, int16_t window_y, uint16_t window_width,
|
||||||
|
uint16_t window_height, bool window_borderless);
|
||||||
|
|
||||||
// show the window
|
// show the window
|
||||||
void
|
void
|
||||||
@ -76,4 +87,8 @@ screen_resize_to_fit(struct screen *screen);
|
|||||||
void
|
void
|
||||||
screen_resize_to_pixel_perfect(struct screen *screen);
|
screen_resize_to_pixel_perfect(struct screen *screen);
|
||||||
|
|
||||||
|
// react to window events
|
||||||
|
void
|
||||||
|
screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
#include "server.h"
|
#include "server.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
#include <libgen.h>
|
#include <libgen.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <SDL2/SDL_assert.h>
|
|
||||||
#include <SDL2/SDL_timer.h>
|
#include <SDL2/SDL_timer.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "command.h"
|
#include "command.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
#include "net.h"
|
#include "util/net.h"
|
||||||
|
|
||||||
#define SOCKET_NAME "scrcpy"
|
#define SOCKET_NAME "scrcpy"
|
||||||
#define SERVER_FILENAME "scrcpy-server"
|
#define SERVER_FILENAME "scrcpy-server"
|
||||||
|
|
||||||
#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME
|
#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 *
|
static const char *
|
||||||
get_server_path(void) {
|
get_server_path(void) {
|
||||||
@ -67,7 +67,12 @@ get_server_path(void) {
|
|||||||
|
|
||||||
static bool
|
static bool
|
||||||
push_server(const char *serial) {
|
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");
|
return process_check_success(process, "adb push");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,11 +123,13 @@ static process_t
|
|||||||
execute_server(struct server *server, const struct server_params *params) {
|
execute_server(struct server *server, const struct server_params *params) {
|
||||||
char max_size_string[6];
|
char max_size_string[6];
|
||||||
char bit_rate_string[11];
|
char bit_rate_string[11];
|
||||||
|
char max_fps_string[6];
|
||||||
sprintf(max_size_string, "%"PRIu16, params->max_size);
|
sprintf(max_size_string, "%"PRIu16, params->max_size);
|
||||||
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
|
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
|
||||||
|
sprintf(max_fps_string, "%"PRIu16, params->max_fps);
|
||||||
const char *const cmd[] = {
|
const char *const cmd[] = {
|
||||||
"shell",
|
"shell",
|
||||||
"CLASSPATH=/data/local/tmp/" SERVER_FILENAME,
|
"CLASSPATH=" DEVICE_SERVER_PATH,
|
||||||
"app_process",
|
"app_process",
|
||||||
#ifdef SERVER_DEBUGGER
|
#ifdef SERVER_DEBUGGER
|
||||||
# define SERVER_DEBUGGER_PORT "5005"
|
# define SERVER_DEBUGGER_PORT "5005"
|
||||||
@ -131,8 +138,10 @@ execute_server(struct server *server, const struct server_params *params) {
|
|||||||
#endif
|
#endif
|
||||||
"/", // unused
|
"/", // unused
|
||||||
"com.genymobile.scrcpy.Server",
|
"com.genymobile.scrcpy.Server",
|
||||||
|
SCRCPY_VERSION,
|
||||||
max_size_string,
|
max_size_string,
|
||||||
bit_rate_string,
|
bit_rate_string,
|
||||||
|
max_fps_string,
|
||||||
server->tunnel_forward ? "true" : "false",
|
server->tunnel_forward ? "true" : "false",
|
||||||
params->crop ? params->crop : "-",
|
params->crop ? params->crop : "-",
|
||||||
"true", // always send frame meta (packet boundaries + timestamp)
|
"true", // always send frame meta (packet boundaries + timestamp)
|
||||||
@ -195,7 +204,7 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) {
|
|||||||
|
|
||||||
static void
|
static void
|
||||||
close_socket(socket_t *socket) {
|
close_socket(socket_t *socket) {
|
||||||
SDL_assert(*socket != INVALID_SOCKET);
|
assert(*socket != INVALID_SOCKET);
|
||||||
net_shutdown(*socket, SHUT_RDWR);
|
net_shutdown(*socket, SHUT_RDWR);
|
||||||
if (!net_close(*socket)) {
|
if (!net_close(*socket)) {
|
||||||
LOGW("Could not close socket");
|
LOGW("Could not close socket");
|
||||||
@ -277,7 +286,7 @@ server_connect_to(struct server *server) {
|
|||||||
|
|
||||||
server->control_socket = net_accept(server->server_socket);
|
server->control_socket = net_accept(server->server_socket);
|
||||||
if (server->control_socket == INVALID_SOCKET) {
|
if (server->control_socket == INVALID_SOCKET) {
|
||||||
// the video_socket will be clean up on destroy
|
// the video_socket will be cleaned up on destroy
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,7 +328,7 @@ server_stop(struct server *server) {
|
|||||||
close_socket(&server->control_socket);
|
close_socket(&server->control_socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_assert(server->process != PROCESS_NONE);
|
assert(server->process != PROCESS_NONE);
|
||||||
|
|
||||||
if (!cmd_terminate(server->process)) {
|
if (!cmd_terminate(server->process)) {
|
||||||
LOGW("Could not terminate server");
|
LOGW("Could not terminate server");
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "command.h"
|
#include "command.h"
|
||||||
#include "net.h"
|
#include "util/net.h"
|
||||||
|
|
||||||
struct server {
|
struct server {
|
||||||
char *serial;
|
char *serial;
|
||||||
@ -35,6 +35,7 @@ struct server_params {
|
|||||||
uint16_t local_port;
|
uint16_t local_port;
|
||||||
uint16_t max_size;
|
uint16_t max_size;
|
||||||
uint32_t bit_rate;
|
uint32_t bit_rate;
|
||||||
|
uint16_t max_fps;
|
||||||
bool control;
|
bool control;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
#include "stream.h"
|
#include "stream.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
#include <libavutil/time.h>
|
#include <libavutil/time.h>
|
||||||
#include <SDL2/SDL_assert.h>
|
|
||||||
#include <SDL2/SDL_events.h>
|
#include <SDL2/SDL_events.h>
|
||||||
#include <SDL2/SDL_mutex.h>
|
#include <SDL2/SDL_mutex.h>
|
||||||
#include <SDL2/SDL_thread.h>
|
#include <SDL2/SDL_thread.h>
|
||||||
@ -10,12 +10,11 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "compat.h"
|
#include "compat.h"
|
||||||
#include "buffer_util.h"
|
|
||||||
#include "decoder.h"
|
#include "decoder.h"
|
||||||
#include "events.h"
|
#include "events.h"
|
||||||
#include "lock_util.h"
|
|
||||||
#include "log.h"
|
|
||||||
#include "recorder.h"
|
#include "recorder.h"
|
||||||
|
#include "util/buffer_util.h"
|
||||||
|
#include "util/log.h"
|
||||||
|
|
||||||
#define BUFSIZE 0x10000
|
#define BUFSIZE 0x10000
|
||||||
|
|
||||||
@ -44,7 +43,8 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) {
|
|||||||
|
|
||||||
uint64_t pts = buffer_read64be(header);
|
uint64_t pts = buffer_read64be(header);
|
||||||
uint32_t len = buffer_read32be(&header[8]);
|
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)) {
|
if (av_new_packet(packet, len)) {
|
||||||
LOGE("Could not allocate packet");
|
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);
|
r = net_recv_all(stream->socket, packet->data, len);
|
||||||
if (r < len) {
|
if (r < 0 || ((uint32_t) r) < len) {
|
||||||
av_packet_unref(packet);
|
av_packet_unref(packet);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
packet->pts = pts != NO_PTS ? pts : AV_NOPTS_VALUE;
|
packet->pts = pts != NO_PTS ? (int64_t) pts : AV_NOPTS_VALUE;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -107,8 +107,9 @@ stream_parse(struct stream *stream, AVPacket *packet) {
|
|||||||
AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1);
|
AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1);
|
||||||
|
|
||||||
// PARSER_FLAG_COMPLETE_FRAMES is set
|
// PARSER_FLAG_COMPLETE_FRAMES is set
|
||||||
SDL_assert(r == in_len);
|
assert(r == in_len);
|
||||||
SDL_assert(out_len == in_len);
|
(void) r;
|
||||||
|
assert(out_len == in_len);
|
||||||
|
|
||||||
if (stream->parser->key_frame == 1) {
|
if (stream->parser->key_frame == 1) {
|
||||||
packet->flags |= AV_PKT_FLAG_KEY;
|
packet->flags |= AV_PKT_FLAG_KEY;
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
#include <SDL2/SDL_thread.h>
|
#include <SDL2/SDL_thread.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "net.h"
|
#include "util/net.h"
|
||||||
|
|
||||||
struct video_buffer;
|
struct video_buffer;
|
||||||
|
|
||||||
|
@ -17,10 +17,11 @@
|
|||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include "log.h"
|
|
||||||
|
#include "util/log.h"
|
||||||
|
|
||||||
enum process_result
|
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];
|
int fd[2];
|
||||||
|
|
||||||
if (pipe(fd) == -1) {
|
if (pipe(fd) == -1) {
|
||||||
@ -51,7 +52,7 @@ cmd_execute(const char *path, const char *const argv[], pid_t *pid) {
|
|||||||
// child close read side
|
// child close read side
|
||||||
close(fd[0]);
|
close(fd[0]);
|
||||||
if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) {
|
if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) {
|
||||||
execvp(path, (char *const *)argv);
|
execvp(argv[0], (char *const *)argv);
|
||||||
if (errno == ENOENT) {
|
if (errno == ENOENT) {
|
||||||
ret = PROCESS_ERROR_MISSING_BINARY;
|
ret = PROCESS_ERROR_MISSING_BINARY;
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#include "net.h"
|
#include "util/net.h"
|
||||||
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
#include "command.h"
|
#include "command.h"
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
#include "str_util.h"
|
#include "util/str_util.h"
|
||||||
|
|
||||||
static int
|
static int
|
||||||
build_cmd(char *cmd, size_t len, const char *const argv[]) {
|
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
|
enum process_result
|
||||||
cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
|
cmd_execute(const char *const argv[], HANDLE *handle) {
|
||||||
STARTUPINFOW si;
|
STARTUPINFOW si;
|
||||||
PROCESS_INFORMATION pi;
|
PROCESS_INFORMATION pi;
|
||||||
memset(&si, 0, sizeof(si));
|
memset(&si, 0, sizeof(si));
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#include "net.h"
|
#include "util/net.h"
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
bool
|
bool
|
||||||
net_init(void) {
|
net_init(void) {
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
#include "tiny_xpm.h"
|
#include "tiny_xpm.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
struct index {
|
struct index {
|
||||||
char c;
|
char c;
|
||||||
@ -36,7 +37,7 @@ find_color(struct index *index, int len, char c, uint32_t *color) {
|
|||||||
// (non-const) "char *"
|
// (non-const) "char *"
|
||||||
SDL_Surface *
|
SDL_Surface *
|
||||||
read_xpm(char *xpm[]) {
|
read_xpm(char *xpm[]) {
|
||||||
#if SDL_ASSERT_LEVEL >= 2
|
#ifndef NDEBUG
|
||||||
// patch the XPM to change the icon color in debug mode
|
// patch the XPM to change the icon color in debug mode
|
||||||
xpm[2] = ". c #CC00CC";
|
xpm[2] = ". c #CC00CC";
|
||||||
#endif
|
#endif
|
||||||
@ -51,24 +52,26 @@ read_xpm(char *xpm[]) {
|
|||||||
int chars = strtol(endptr + 1, &endptr, 10);
|
int chars = strtol(endptr + 1, &endptr, 10);
|
||||||
|
|
||||||
// sanity checks
|
// sanity checks
|
||||||
SDL_assert(0 <= width && width < 256);
|
assert(0 <= width && width < 256);
|
||||||
SDL_assert(0 <= height && height < 256);
|
assert(0 <= height && height < 256);
|
||||||
SDL_assert(0 <= colors && colors < 256);
|
assert(0 <= colors && colors < 256);
|
||||||
SDL_assert(chars == 1); // this implementation does not support more
|
assert(chars == 1); // this implementation does not support more
|
||||||
|
|
||||||
|
(void) chars;
|
||||||
|
|
||||||
// init index
|
// init index
|
||||||
struct index index[colors];
|
struct index index[colors];
|
||||||
for (int i = 0; i < colors; ++i) {
|
for (int i = 0; i < colors; ++i) {
|
||||||
const char *line = xpm[1+i];
|
const char *line = xpm[1+i];
|
||||||
index[i].c = line[0];
|
index[i].c = line[0];
|
||||||
SDL_assert(line[1] == '\t');
|
assert(line[1] == '\t');
|
||||||
SDL_assert(line[2] == 'c');
|
assert(line[2] == 'c');
|
||||||
SDL_assert(line[3] == ' ');
|
assert(line[3] == ' ');
|
||||||
if (line[4] == '#') {
|
if (line[4] == '#') {
|
||||||
index[i].color = 0xff000000 | strtol(&line[5], &endptr, 0x10);
|
index[i].color = 0xff000000 | strtol(&line[5], &endptr, 0x10);
|
||||||
SDL_assert(*endptr == '\0');
|
assert(*endptr == '\0');
|
||||||
} else {
|
} else {
|
||||||
SDL_assert(!strcmp("None", &line[4]));
|
assert(!strcmp("None", &line[4]));
|
||||||
index[i].color = 0;
|
index[i].color = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,7 +88,8 @@ read_xpm(char *xpm[]) {
|
|||||||
char c = line[x];
|
char c = line[x];
|
||||||
uint32_t color;
|
uint32_t color;
|
||||||
bool color_found = find_color(index, colors, c, &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;
|
pixels[y * width + x] = color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,8 @@ buffer_read32be(const uint8_t *buf) {
|
|||||||
return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
|
return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline
|
static inline uint64_t
|
||||||
uint64_t buffer_read64be(const uint8_t *buf) {
|
buffer_read64be(const uint8_t *buf) {
|
||||||
uint32_t msb = buffer_read32be(buf);
|
uint32_t msb = buffer_read32be(buf);
|
||||||
uint32_t lsb = buffer_read32be(&buf[4]);
|
uint32_t lsb = buffer_read32be(&buf[4]);
|
||||||
return ((uint64_t) msb << 32) | lsb;
|
return ((uint64_t) msb << 32) | lsb;
|
74
app/src/util/lock.h
Normal file
74
app/src/util/lock.h
Normal 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
|
@ -2,9 +2,9 @@
|
|||||||
#ifndef QUEUE_H
|
#ifndef QUEUE_H
|
||||||
#define QUEUE_H
|
#define QUEUE_H
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <SDL2/SDL_assert.h>
|
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
@ -67,7 +67,7 @@
|
|||||||
// type so that we can "return" it)
|
// type so that we can "return" it)
|
||||||
#define queue_take(PQ, NEXTFIELD, PITEM) \
|
#define queue_take(PQ, NEXTFIELD, PITEM) \
|
||||||
(void) ({ \
|
(void) ({ \
|
||||||
SDL_assert(!queue_is_empty(PQ)); \
|
assert(!queue_is_empty(PQ)); \
|
||||||
*(PITEM) = (PQ)->first; \
|
*(PITEM) = (PQ)->first; \
|
||||||
(PQ)->first = (PQ)->first->NEXTFIELD; \
|
(PQ)->first = (PQ)->first->NEXTFIELD; \
|
||||||
})
|
})
|
@ -1,5 +1,7 @@
|
|||||||
#include "str_util.h"
|
#include "str_util.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
@ -60,6 +62,59 @@ strquote(const char *src) {
|
|||||||
return quoted;
|
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
|
size_t
|
||||||
utf8_truncation_index(const char *utf8, size_t max_len) {
|
utf8_truncation_index(const char *utf8, size_t max_len) {
|
||||||
size_t len = strlen(utf8);
|
size_t len = strlen(utf8);
|
@ -1,6 +1,7 @@
|
|||||||
#ifndef STRUTIL_H
|
#ifndef STRUTIL_H
|
||||||
#define STRUTIL_H
|
#define STRUTIL_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@ -25,6 +26,18 @@ xstrjoin(char *dst, const char *const tokens[], char sep, size_t n);
|
|||||||
char *
|
char *
|
||||||
strquote(const char *src);
|
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
|
// return the index to truncate a UTF-8 string at a valid position
|
||||||
size_t
|
size_t
|
||||||
utf8_truncation_index(const char *utf8, size_t max_len);
|
utf8_truncation_index(const char *utf8, size_t max_len);
|
@ -1,13 +1,13 @@
|
|||||||
#include "video_buffer.h"
|
#include "video_buffer.h"
|
||||||
|
|
||||||
#include <SDL2/SDL_assert.h>
|
#include <assert.h>
|
||||||
#include <SDL2/SDL_mutex.h>
|
#include <SDL2/SDL_mutex.h>
|
||||||
#include <libavutil/avutil.h>
|
#include <libavutil/avutil.h>
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "lock_util.h"
|
#include "util/lock.h"
|
||||||
#include "log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
bool
|
bool
|
||||||
video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter,
|
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 *
|
const AVFrame *
|
||||||
video_buffer_consume_rendered_frame(struct video_buffer *vb) {
|
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;
|
vb->rendering_frame_consumed = true;
|
||||||
fps_counter_add_rendered_frame(vb->fps_counter);
|
fps_counter_add_rendered_frame(vb->fps_counter);
|
||||||
if (vb->render_expired_frames) {
|
if (vb->render_expired_frames) {
|
||||||
|
76
app/tests/test_buffer_util.c
Normal file
76
app/tests/test_buffer_util.c
Normal 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;
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "cbuf.h"
|
#include "util/cbuf.h"
|
||||||
|
|
||||||
struct int_queue CBUF(int, 32);
|
struct int_queue CBUF(int, 32);
|
||||||
|
|
||||||
|
128
app/tests/test_cli.c
Normal file
128
app/tests/test_cli.c
Normal 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;
|
||||||
|
};
|
@ -236,6 +236,21 @@ static void test_serialize_set_screen_power_mode(void) {
|
|||||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
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) {
|
int main(void) {
|
||||||
test_serialize_inject_keycode();
|
test_serialize_inject_keycode();
|
||||||
test_serialize_inject_text();
|
test_serialize_inject_text();
|
||||||
@ -248,5 +263,6 @@ int main(void) {
|
|||||||
test_serialize_get_clipboard();
|
test_serialize_get_clipboard();
|
||||||
test_serialize_set_clipboard();
|
test_serialize_set_clipboard();
|
||||||
test_serialize_set_screen_power_mode();
|
test_serialize_set_screen_power_mode();
|
||||||
|
test_serialize_rotate_device();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
|
||||||
#include <queue.h>
|
#include "util/queue.h"
|
||||||
|
|
||||||
struct foo {
|
struct foo {
|
||||||
int value;
|
int value;
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
#include "str_util.h"
|
#include "util/str_util.h"
|
||||||
|
|
||||||
static void test_xstrncpy_simple(void) {
|
static void test_xstrncpy_simple(void) {
|
||||||
char s[] = "xxxxxxxxxx";
|
char s[] = "xxxxxxxxxx";
|
||||||
@ -126,6 +129,16 @@ static void test_xstrjoin_truncated_after_sep(void) {
|
|||||||
assert(!strcmp("abc de ", s));
|
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) {
|
static void test_utf8_truncate(void) {
|
||||||
const char *s = "aÉbÔc";
|
const char *s = "aÉbÔc";
|
||||||
assert(strlen(s) == 7); // É and Ô are 2 bytes-wide
|
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
|
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) {
|
int main(void) {
|
||||||
test_xstrncpy_simple();
|
test_xstrncpy_simple();
|
||||||
test_xstrncpy_just_fit();
|
test_xstrncpy_just_fit();
|
||||||
@ -166,6 +246,9 @@ int main(void) {
|
|||||||
test_xstrjoin_truncated_in_token();
|
test_xstrjoin_truncated_in_token();
|
||||||
test_xstrjoin_truncated_before_sep();
|
test_xstrjoin_truncated_before_sep();
|
||||||
test_xstrjoin_truncated_after_sep();
|
test_xstrjoin_truncated_after_sep();
|
||||||
|
test_strquote();
|
||||||
test_utf8_truncate();
|
test_utf8_truncate();
|
||||||
|
test_parse_integer();
|
||||||
|
test_parse_integer_with_suffix();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ page at http://checkstyle.sourceforge.net/config.html -->
|
|||||||
<module name="SuppressWarningsHolder"/>
|
<module name="SuppressWarningsHolder"/>
|
||||||
|
|
||||||
<!-- Checks for imports -->
|
<!-- Checks for imports -->
|
||||||
<!-- See http://checkstyle.sf.net/config_import.html -->
|
<!-- See http://checkstyle.sf.net/config_imports.html -->
|
||||||
<module name="AvoidStarImport">
|
<module name="AvoidStarImport">
|
||||||
<property name="allowStaticMemberImports" value="true" />
|
<property name="allowStaticMemberImports" value="true" />
|
||||||
</module>
|
</module>
|
||||||
@ -99,7 +99,7 @@ page at http://checkstyle.sourceforge.net/config.html -->
|
|||||||
<module name="WhitespaceAround" />
|
<module name="WhitespaceAround" />
|
||||||
|
|
||||||
<!-- Modifier Checks -->
|
<!-- Modifier Checks -->
|
||||||
<!-- See http://checkstyle.sf.net/config_modifiers.html -->
|
<!-- See http://checkstyle.sf.net/config_modifier.html -->
|
||||||
<module name="ModifierOrder" />
|
<module name="ModifierOrder" />
|
||||||
<module name="RedundantModifier" />
|
<module name="RedundantModifier" />
|
||||||
|
|
||||||
|
@ -15,6 +15,6 @@ cpu = 'i686'
|
|||||||
endian = 'little'
|
endian = 'little'
|
||||||
|
|
||||||
[properties]
|
[properties]
|
||||||
prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win32-shared'
|
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win32-shared'
|
||||||
prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win32-dev'
|
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win32-dev'
|
||||||
prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32'
|
prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32'
|
||||||
|
@ -15,6 +15,6 @@ cpu = 'x86_64'
|
|||||||
endian = 'little'
|
endian = 'little'
|
||||||
|
|
||||||
[properties]
|
[properties]
|
||||||
prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win64-shared'
|
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win64-shared'
|
||||||
prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win64-dev'
|
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win64-dev'
|
||||||
prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32'
|
prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32'
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
project('scrcpy', 'c',
|
project('scrcpy', 'c',
|
||||||
version: '1.10',
|
version: '1.12.1',
|
||||||
meson_version: '>= 0.37',
|
meson_version: '>= 0.37',
|
||||||
default_options: 'c_std=c11')
|
default_options: [
|
||||||
|
'c_std=c11',
|
||||||
|
'warning_level=2',
|
||||||
|
])
|
||||||
|
|
||||||
if get_option('compile_app')
|
if get_option('compile_app')
|
||||||
subdir('app')
|
subdir('app')
|
||||||
|
@ -10,24 +10,24 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32
|
|||||||
prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
|
prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
|
||||||
|
|
||||||
prepare-ffmpeg-shared-win32:
|
prepare-ffmpeg-shared-win32:
|
||||||
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1.4-win32-shared.zip \
|
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.1-win32-shared.zip \
|
||||||
596608277f6b937c3dea7c46e854665d75b3de56790bae07f655ca331440f003 \
|
9208255f409410d95147151d7e829b5699bf8d91bfe1e81c3f470f47c2fa66d2 \
|
||||||
ffmpeg-4.1.4-win32-shared
|
ffmpeg-4.2.1-win32-shared
|
||||||
|
|
||||||
prepare-ffmpeg-dev-win32:
|
prepare-ffmpeg-dev-win32:
|
||||||
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.4-win32-dev.zip \
|
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.1-win32-dev.zip \
|
||||||
a80c86e263cfad26e202edfa5e6e939a2c88843ae26f031d3e0d981a39fd03fb \
|
c3469e6c5f031cbcc8cba88dee92d6548c5c6b6ff14f4097f18f72a92d0d70c4 \
|
||||||
ffmpeg-4.1.4-win32-dev
|
ffmpeg-4.2.1-win32-dev
|
||||||
|
|
||||||
prepare-ffmpeg-shared-win64:
|
prepare-ffmpeg-shared-win64:
|
||||||
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.4-win64-shared.zip \
|
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.1-win64-shared.zip \
|
||||||
a90889871de2cab8a79b392591313a188189a353f69dde1db98aebe20b280989 \
|
55063d3cf750a75485c7bf196031773d81a1b25d0980c7db48ecfc7701a42331 \
|
||||||
ffmpeg-4.1.4-win64-shared
|
ffmpeg-4.2.1-win64-shared
|
||||||
|
|
||||||
prepare-ffmpeg-dev-win64:
|
prepare-ffmpeg-dev-win64:
|
||||||
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.4-win64-dev.zip \
|
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.1-win64-dev.zip \
|
||||||
6c9d53f9e94ce1821e975ec668e5b9d6e9deb4a45d0d7e30264685d3dfbbb068 \
|
5af393be5f25c0a71aa29efce768e477c35347f7f8e0d9696767d5b9d405b74e \
|
||||||
ffmpeg-4.1.4-win64-dev
|
ffmpeg-4.2.1-win64-dev
|
||||||
|
|
||||||
prepare-sdl2:
|
prepare-sdl2:
|
||||||
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \
|
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \
|
||||||
@ -35,6 +35,6 @@ prepare-sdl2:
|
|||||||
SDL2-2.0.10
|
SDL2-2.0.10
|
||||||
|
|
||||||
prepare-adb:
|
prepare-adb:
|
||||||
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.2-windows.zip \
|
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \
|
||||||
d78f02e5e2c9c4c1d046dcd4e6fbdf586e5f57ef66eb0da5c2b49d745d85d5ee \
|
2df06160056ec9a84c7334af2a1e42740befbb1a2e34370e7af544a2cc78152c \
|
||||||
platform-tools
|
platform-tools
|
||||||
|
@ -6,8 +6,8 @@ android {
|
|||||||
applicationId "com.genymobile.scrcpy"
|
applicationId "com.genymobile.scrcpy"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 11
|
versionCode 14
|
||||||
versionName "1.10"
|
versionName "1.12.1"
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -11,6 +11,9 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
SCRCPY_DEBUG=false
|
||||||
|
SCRCPY_VERSION_NAME=1.12.1
|
||||||
|
|
||||||
PLATFORM=${ANDROID_PLATFORM:-29}
|
PLATFORM=${ANDROID_PLATFORM:-29}
|
||||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}
|
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}
|
||||||
|
|
||||||
@ -30,13 +33,14 @@ mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy"
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
public final class BuildConfig {
|
public final class BuildConfig {
|
||||||
public static final boolean DEBUG = false;
|
public static final boolean DEBUG = $SCRCPY_DEBUG;
|
||||||
|
public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME";
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Generating java from aidl..."
|
echo "Generating java from aidl..."
|
||||||
cd "$SERVER_DIR/src/main/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
|
android/view/IRotationWatcher.aidl
|
||||||
|
|
||||||
echo "Compiling java sources..."
|
echo "Compiling java sources..."
|
||||||
|
@ -15,6 +15,7 @@ public final class ControlMessage {
|
|||||||
public static final int TYPE_GET_CLIPBOARD = 7;
|
public static final int TYPE_GET_CLIPBOARD = 7;
|
||||||
public static final int TYPE_SET_CLIPBOARD = 8;
|
public static final int TYPE_SET_CLIPBOARD = 8;
|
||||||
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
|
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
|
||||||
|
public static final int TYPE_ROTATE_DEVICE = 10;
|
||||||
|
|
||||||
private int type;
|
private int type;
|
||||||
private String text;
|
private String text;
|
||||||
@ -47,8 +48,7 @@ public final class ControlMessage {
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure,
|
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) {
|
||||||
int buttons) {
|
|
||||||
ControlMessage msg = new ControlMessage();
|
ControlMessage msg = new ControlMessage();
|
||||||
msg.type = TYPE_INJECT_TOUCH_EVENT;
|
msg.type = TYPE_INJECT_TOUCH_EVENT;
|
||||||
msg.action = action;
|
msg.action = action;
|
||||||
|
@ -76,6 +76,7 @@ public class ControlMessageReader {
|
|||||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||||
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
||||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||||
|
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||||
msg = ControlMessage.createEmpty(type);
|
msg = ControlMessage.createEmpty(type);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -13,6 +13,8 @@ import java.io.IOException;
|
|||||||
|
|
||||||
public class Controller {
|
public class Controller {
|
||||||
|
|
||||||
|
private static final int DEVICE_ID_VIRTUAL = -1;
|
||||||
|
|
||||||
private final Device device;
|
private final Device device;
|
||||||
private final DesktopConnection connection;
|
private final DesktopConnection connection;
|
||||||
private final DeviceMessageSender sender;
|
private final DeviceMessageSender sender;
|
||||||
@ -21,10 +23,8 @@ public class Controller {
|
|||||||
|
|
||||||
private long lastTouchDown;
|
private long lastTouchDown;
|
||||||
private final PointersState pointersState = new PointersState();
|
private final PointersState pointersState = new PointersState();
|
||||||
private final MotionEvent.PointerProperties[] pointerProperties =
|
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
|
||||||
new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
|
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
|
||||||
private final MotionEvent.PointerCoords[] pointerCoords =
|
|
||||||
new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
|
|
||||||
|
|
||||||
public Controller(Device device, DesktopConnection connection) {
|
public Controller(Device device, DesktopConnection connection) {
|
||||||
this.device = device;
|
this.device = device;
|
||||||
@ -106,6 +106,9 @@ public class Controller {
|
|||||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
||||||
device.setScreenPowerMode(msg.getAction());
|
device.setScreenPowerMode(msg.getAction());
|
||||||
break;
|
break;
|
||||||
|
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||||
|
device.rotateDevice();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
@ -176,8 +179,9 @@ public class Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties,
|
MotionEvent event = MotionEvent
|
||||||
pointerCoords, 0, buttons, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
|
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
||||||
|
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
||||||
return injectEvent(event);
|
return injectEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,15 +202,16 @@ public class Controller {
|
|||||||
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
|
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
|
||||||
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
|
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
|
||||||
|
|
||||||
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties,
|
MotionEvent event = MotionEvent
|
||||||
pointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0);
|
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
||||||
|
InputDevice.SOURCE_MOUSE, 0);
|
||||||
return injectEvent(event);
|
return injectEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
|
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
|
||||||
long now = SystemClock.uptimeMillis();
|
long now = SystemClock.uptimeMillis();
|
||||||
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD,
|
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
|
||||||
0, 0, InputDevice.SOURCE_KEYBOARD);
|
InputDevice.SOURCE_KEYBOARD);
|
||||||
return injectEvent(event);
|
return injectEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package com.genymobile.scrcpy;
|
|||||||
|
|
||||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||||
|
import com.genymobile.scrcpy.wrappers.WindowManager;
|
||||||
|
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@ -170,6 +171,27 @@ public final class Device {
|
|||||||
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
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) {
|
static Rect flipRect(Rect crop) {
|
||||||
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
|
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
|
||||||
}
|
}
|
||||||
|
@ -12,10 +12,7 @@ public final class Ln {
|
|||||||
private static final String PREFIX = "[server] ";
|
private static final String PREFIX = "[server] ";
|
||||||
|
|
||||||
enum Level {
|
enum Level {
|
||||||
DEBUG,
|
DEBUG, INFO, WARN, ERROR
|
||||||
INFO,
|
|
||||||
WARN,
|
|
||||||
ERROR;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO;
|
private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO;
|
||||||
|
@ -5,6 +5,7 @@ import android.graphics.Rect;
|
|||||||
public class Options {
|
public class Options {
|
||||||
private int maxSize;
|
private int maxSize;
|
||||||
private int bitRate;
|
private int bitRate;
|
||||||
|
private int maxFps;
|
||||||
private boolean tunnelForward;
|
private boolean tunnelForward;
|
||||||
private Rect crop;
|
private Rect crop;
|
||||||
private boolean sendFrameMeta; // send PTS so that the client may record properly
|
private boolean sendFrameMeta; // send PTS so that the client may record properly
|
||||||
@ -26,6 +27,14 @@ public class Options {
|
|||||||
this.bitRate = bitRate;
|
this.bitRate = bitRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getMaxFps() {
|
||||||
|
return maxFps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxFps(int maxFps) {
|
||||||
|
this.maxFps = maxFps;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isTunnelForward() {
|
public boolean isTunnelForward() {
|
||||||
return tunnelForward;
|
return tunnelForward;
|
||||||
}
|
}
|
||||||
|
@ -28,8 +28,7 @@ public class Point {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Point point = (Point) o;
|
Point point = (Point) o;
|
||||||
return x == point.x
|
return x == point.x && y == point.y;
|
||||||
&& y == point.y;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -39,9 +38,6 @@ public class Point {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Point{"
|
return "Point{" + "x=" + x + ", y=" + y + '}';
|
||||||
+ "x=" + x
|
|
||||||
+ ", y=" + y
|
|
||||||
+ '}';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,8 +32,7 @@ public class Position {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Position position = (Position) o;
|
Position position = (Position) o;
|
||||||
return Objects.equals(point, position.point)
|
return Objects.equals(point, position.point) && Objects.equals(screenSize, position.screenSize);
|
||||||
&& Objects.equals(screenSize, position.screenSize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -43,10 +42,7 @@ public class Position {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Position{"
|
return "Position{" + "point=" + point + ", screenSize=" + screenSize + '}';
|
||||||
+ "point=" + point
|
|
||||||
+ ", screenSize=" + screenSize
|
|
||||||
+ '}';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import android.graphics.Rect;
|
|||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
|
|
||||||
@ -16,32 +17,29 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
|
|
||||||
public class ScreenEncoder implements Device.RotationListener {
|
public class ScreenEncoder implements Device.RotationListener {
|
||||||
|
|
||||||
private static final int DEFAULT_FRAME_RATE = 60; // fps
|
|
||||||
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
|
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
|
||||||
|
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
|
||||||
|
|
||||||
private static final int REPEAT_FRAME_DELAY = 6; // repeat after 6 frames
|
|
||||||
|
|
||||||
private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000;
|
|
||||||
private static final int NO_PTS = -1;
|
private static final int NO_PTS = -1;
|
||||||
|
|
||||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||||
|
|
||||||
private int bitRate;
|
private int bitRate;
|
||||||
private int frameRate;
|
private int maxFps;
|
||||||
private int iFrameInterval;
|
private int iFrameInterval;
|
||||||
private boolean sendFrameMeta;
|
private boolean sendFrameMeta;
|
||||||
private long ptsOrigin;
|
private long ptsOrigin;
|
||||||
|
|
||||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int frameRate, int iFrameInterval) {
|
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) {
|
||||||
this.sendFrameMeta = sendFrameMeta;
|
this.sendFrameMeta = sendFrameMeta;
|
||||||
this.bitRate = bitRate;
|
this.bitRate = bitRate;
|
||||||
this.frameRate = frameRate;
|
this.maxFps = maxFps;
|
||||||
this.iFrameInterval = iFrameInterval;
|
this.iFrameInterval = iFrameInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate) {
|
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) {
|
||||||
this(sendFrameMeta, bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
|
this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -54,7 +52,10 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||||
MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval);
|
Workarounds.prepareMainLooper();
|
||||||
|
Workarounds.fillAppInfo();
|
||||||
|
|
||||||
|
MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval);
|
||||||
device.setRotationListener(this);
|
device.setRotationListener(this);
|
||||||
boolean alive;
|
boolean alive;
|
||||||
try {
|
try {
|
||||||
@ -137,15 +138,24 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
return MediaCodec.createEncoderByType("video/avc");
|
return MediaCodec.createEncoderByType("video/avc");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaFormat createFormat(int bitRate, int frameRate, int iFrameInterval) throws IOException {
|
@SuppressWarnings("checkstyle:MagicNumber")
|
||||||
|
private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) {
|
||||||
MediaFormat format = new MediaFormat();
|
MediaFormat format = new MediaFormat();
|
||||||
format.setString(MediaFormat.KEY_MIME, "video/avc");
|
format.setString(MediaFormat.KEY_MIME, "video/avc");
|
||||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
||||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
|
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
|
||||||
|
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
|
||||||
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
|
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
|
||||||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
|
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
|
||||||
// display the very first frame, and recover from bad quality when no new frames
|
// display the very first frame, and recover from bad quality when no new frames
|
||||||
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate); // µs
|
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs
|
||||||
|
if (maxFps > 0) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
format.setFloat(MediaFormat.KEY_MAX_FPS_TO_ENCODER, maxFps);
|
||||||
|
} else {
|
||||||
|
Ln.w("Max FPS is only supported since Android 10, the option has been ignored");
|
||||||
|
}
|
||||||
|
}
|
||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
import android.media.MediaCodec;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public final class Server {
|
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() {
|
private Server() {
|
||||||
// not instantiable
|
// not instantiable
|
||||||
@ -17,7 +19,7 @@ public final class Server {
|
|||||||
final Device device = new Device(options);
|
final Device device = new Device(options);
|
||||||
boolean tunnelForward = options.isTunnelForward();
|
boolean tunnelForward = options.isTunnelForward();
|
||||||
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
||||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate());
|
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps());
|
||||||
|
|
||||||
if (options.getControl()) {
|
if (options.getControl()) {
|
||||||
Controller controller = new Controller(device, connection);
|
Controller controller = new Controller(device, connection);
|
||||||
@ -67,29 +69,42 @@ public final class Server {
|
|||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
@SuppressWarnings("checkstyle:MagicNumber")
|
||||||
private static Options createOptions(String... args) {
|
private static Options createOptions(String... args) {
|
||||||
if (args.length != 6) {
|
if (args.length < 1) {
|
||||||
throw new IllegalArgumentException("Expecting 6 parameters");
|
throw new IllegalArgumentException("Missing client version");
|
||||||
|
}
|
||||||
|
|
||||||
|
String clientVersion = args[0];
|
||||||
|
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length != 8) {
|
||||||
|
throw new IllegalArgumentException("Expecting 8 parameters");
|
||||||
}
|
}
|
||||||
|
|
||||||
Options options = new Options();
|
Options options = new Options();
|
||||||
|
|
||||||
int maxSize = Integer.parseInt(args[0]) & ~7; // multiple of 8
|
int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8
|
||||||
options.setMaxSize(maxSize);
|
options.setMaxSize(maxSize);
|
||||||
|
|
||||||
int bitRate = Integer.parseInt(args[1]);
|
int bitRate = Integer.parseInt(args[2]);
|
||||||
options.setBitRate(bitRate);
|
options.setBitRate(bitRate);
|
||||||
|
|
||||||
|
int maxFps = Integer.parseInt(args[3]);
|
||||||
|
options.setMaxFps(maxFps);
|
||||||
|
|
||||||
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
|
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
|
||||||
boolean tunnelForward = Boolean.parseBoolean(args[2]);
|
boolean tunnelForward = Boolean.parseBoolean(args[4]);
|
||||||
options.setTunnelForward(tunnelForward);
|
options.setTunnelForward(tunnelForward);
|
||||||
|
|
||||||
Rect crop = parseCrop(args[3]);
|
Rect crop = parseCrop(args[5]);
|
||||||
options.setCrop(crop);
|
options.setCrop(crop);
|
||||||
|
|
||||||
boolean sendFrameMeta = Boolean.parseBoolean(args[4]);
|
boolean sendFrameMeta = Boolean.parseBoolean(args[6]);
|
||||||
options.setSendFrameMeta(sendFrameMeta);
|
options.setSendFrameMeta(sendFrameMeta);
|
||||||
|
|
||||||
boolean control = Boolean.parseBoolean(args[5]);
|
boolean control = Boolean.parseBoolean(args[7]);
|
||||||
options.setControl(control);
|
options.setControl(control);
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
@ -120,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 {
|
public static void main(String... args) throws Exception {
|
||||||
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
|
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
|
||||||
@Override
|
@Override
|
||||||
public void uncaughtException(Thread t, Throwable e) {
|
public void uncaughtException(Thread t, Throwable e) {
|
||||||
Ln.e("Exception on thread " + t, e);
|
Ln.e("Exception on thread " + t, e);
|
||||||
|
suggestFix(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -38,8 +38,7 @@ public final class Size {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Size size = (Size) o;
|
Size size = (Size) o;
|
||||||
return width == size.width
|
return width == size.width && height == size.height;
|
||||||
&& height == size.height;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -49,9 +48,6 @@ public final class Size {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Size{"
|
return "Size{" + "width=" + width + ", height=" + height + '}';
|
||||||
+ "width=" + width
|
|
||||||
+ ", height=" + height
|
|
||||||
+ '}';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
79
server/src/main/java/com/genymobile/scrcpy/Workarounds.java
Normal file
79
server/src/main/java/com/genymobile/scrcpy/Workarounds.java
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
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() {
|
||||||
|
// not instantiable
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void prepareMainLooper() {
|
||||||
|
// Some devices internally create a Handler when creating an input Surface, causing an exception:
|
||||||
|
// "Can't create handler inside thread that has not called Looper.prepare()"
|
||||||
|
// <https://github.com/Genymobile/scrcpy/issues/240>
|
||||||
|
//
|
||||||
|
// Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException:
|
||||||
|
// "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue'
|
||||||
|
// on a null object reference"
|
||||||
|
// <https://github.com/Genymobile/scrcpy/issues/921>
|
||||||
|
Looper.prepareMainLooper();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("PrivateApi")
|
||||||
|
public static void fillAppInfo() {
|
||||||
|
try {
|
||||||
|
// ActivityThread activityThread = new ActivityThread();
|
||||||
|
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
|
||||||
|
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
|
||||||
|
activityThreadConstructor.setAccessible(true);
|
||||||
|
Object activityThread = activityThreadConstructor.newInstance();
|
||||||
|
|
||||||
|
// ActivityThread.sCurrentActivityThread = activityThread;
|
||||||
|
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
|
||||||
|
sCurrentActivityThreadField.setAccessible(true);
|
||||||
|
sCurrentActivityThreadField.set(null, activityThread);
|
||||||
|
|
||||||
|
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
|
||||||
|
Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
|
||||||
|
Constructor<?> appBindDataConstructor = appBindDataClass.getDeclaredConstructor();
|
||||||
|
appBindDataConstructor.setAccessible(true);
|
||||||
|
Object appBindData = appBindDataConstructor.newInstance();
|
||||||
|
|
||||||
|
ApplicationInfo applicationInfo = new ApplicationInfo();
|
||||||
|
applicationInfo.packageName = "com.genymobile.scrcpy";
|
||||||
|
|
||||||
|
// appBindData.appInfo = applicationInfo;
|
||||||
|
Field 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,47 +22,37 @@ public class ClipboardManager {
|
|||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Method getGetPrimaryClipMethod() {
|
private Method getGetPrimaryClipMethod() throws NoSuchMethodException {
|
||||||
if (getPrimaryClipMethod == null) {
|
if (getPrimaryClipMethod == null) {
|
||||||
try {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
|
||||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
|
} else {
|
||||||
} else {
|
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
|
||||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
|
|
||||||
}
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
Ln.e("Could not find method", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getPrimaryClipMethod;
|
return getPrimaryClipMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Method getSetPrimaryClipMethod() {
|
private Method getSetPrimaryClipMethod() throws NoSuchMethodException {
|
||||||
if (setPrimaryClipMethod == null) {
|
if (setPrimaryClipMethod == null) {
|
||||||
try {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
|
||||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
|
} else {
|
||||||
} else {
|
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
|
||||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class,
|
|
||||||
String.class, int.class);
|
|
||||||
}
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
Ln.e("Could not find method", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return setPrimaryClipMethod;
|
return setPrimaryClipMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException,
|
private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException {
|
||||||
IllegalAccessException {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
return (ClipData) method.invoke(manager, PACKAGE_NAME);
|
return (ClipData) method.invoke(manager, PACKAGE_NAME);
|
||||||
}
|
}
|
||||||
return (ClipData) method.invoke(manager, PACKAGE_NAME, USER_ID);
|
return (ClipData) method.invoke(manager, PACKAGE_NAME, USER_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) throws InvocationTargetException,
|
private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData)
|
||||||
IllegalAccessException {
|
throws InvocationTargetException, IllegalAccessException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
method.invoke(manager, clipData, PACKAGE_NAME);
|
method.invoke(manager, clipData, PACKAGE_NAME);
|
||||||
} else {
|
} else {
|
||||||
@ -71,32 +61,26 @@ public class ClipboardManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CharSequence getText() {
|
public CharSequence getText() {
|
||||||
Method method = getGetPrimaryClipMethod();
|
|
||||||
if (method == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
Method method = getGetPrimaryClipMethod();
|
||||||
ClipData clipData = getPrimaryClip(method, manager);
|
ClipData clipData = getPrimaryClip(method, manager);
|
||||||
if (clipData == null || clipData.getItemCount() == 0) {
|
if (clipData == null || clipData.getItemCount() == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return clipData.getItemAt(0).getText();
|
return clipData.getItemAt(0).getText();
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
Ln.e("Could not invoke method", e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setText(CharSequence text) {
|
public void setText(CharSequence text) {
|
||||||
Method method = getSetPrimaryClipMethod();
|
|
||||||
if (method == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ClipData clipData = ClipData.newPlainText(null, text);
|
|
||||||
try {
|
try {
|
||||||
|
Method method = getSetPrimaryClipMethod();
|
||||||
|
ClipData clipData = ClipData.newPlainText(null, text);
|
||||||
setPrimaryClip(method, manager, clipData);
|
setPrimaryClip(method, manager, clipData);
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
Ln.e("Could not invoke method", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,26 +21,19 @@ public final class InputManager {
|
|||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Method getInjectInputEventMethod() {
|
private Method getInjectInputEventMethod() throws NoSuchMethodException {
|
||||||
if (injectInputEventMethod == null) {
|
if (injectInputEventMethod == null) {
|
||||||
try {
|
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
|
||||||
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
Ln.e("Could not find method", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return injectInputEventMethod;
|
return injectInputEventMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
|
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
|
||||||
Method method = getInjectInputEventMethod();
|
|
||||||
if (method == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
return (Boolean) method.invoke(manager, inputEvent, mode);
|
Method method = getInjectInputEventMethod();
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
return (boolean) method.invoke(manager, inputEvent, mode);
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
|
Ln.e("Could not invoke method", e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,28 +17,21 @@ public final class PowerManager {
|
|||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Method getIsScreenOnMethod() {
|
private Method getIsScreenOnMethod() throws NoSuchMethodException {
|
||||||
if (isScreenOnMethod == null) {
|
if (isScreenOnMethod == null) {
|
||||||
try {
|
@SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future
|
||||||
@SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future
|
String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
|
||||||
String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
|
isScreenOnMethod = manager.getClass().getMethod(methodName);
|
||||||
isScreenOnMethod = manager.getClass().getMethod(methodName);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
Ln.e("Could not find method", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return isScreenOnMethod;
|
return isScreenOnMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isScreenOn() {
|
public boolean isScreenOn() {
|
||||||
Method method = getIsScreenOnMethod();
|
|
||||||
if (method == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
return (Boolean) method.invoke(manager);
|
Method method = getIsScreenOnMethod();
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
return (boolean) method.invoke(manager);
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
|
Ln.e("Could not invoke method", e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,49 +17,35 @@ public class StatusBarManager {
|
|||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Method getExpandNotificationsPanelMethod() {
|
private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException {
|
||||||
if (expandNotificationsPanelMethod == null) {
|
if (expandNotificationsPanelMethod == null) {
|
||||||
try {
|
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
|
||||||
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
Ln.e("Could not find method", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return expandNotificationsPanelMethod;
|
return expandNotificationsPanelMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Method getCollapsePanelsMethod() {
|
private Method getCollapsePanelsMethod() throws NoSuchMethodException {
|
||||||
if (collapsePanelsMethod == null) {
|
if (collapsePanelsMethod == null) {
|
||||||
try {
|
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
|
||||||
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
Ln.e("Could not find method", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return collapsePanelsMethod;
|
return collapsePanelsMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void expandNotificationsPanel() {
|
public void expandNotificationsPanel() {
|
||||||
Method method = getExpandNotificationsPanelMethod();
|
|
||||||
if (method == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
Method method = getExpandNotificationsPanelMethod();
|
||||||
method.invoke(manager);
|
method.invoke(manager);
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
Ln.e("Could not invoke method", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void collapsePanels() {
|
public void collapsePanels() {
|
||||||
Method method = getCollapsePanelsMethod();
|
|
||||||
if (method == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
Method method = getCollapsePanelsMethod();
|
||||||
method.invoke(manager);
|
method.invoke(manager);
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
Ln.e("Could not invoke method", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,29 +84,23 @@ public final class SurfaceControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Method getGetBuiltInDisplayMethod() {
|
private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException {
|
||||||
if (getBuiltInDisplayMethod == null) {
|
if (getBuiltInDisplayMethod == null) {
|
||||||
try {
|
// the method signature has changed in Android Q
|
||||||
// the method signature has changed in Android Q
|
// <https://github.com/Genymobile/scrcpy/issues/586>
|
||||||
// <https://github.com/Genymobile/scrcpy/issues/586>
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class);
|
||||||
getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class);
|
} else {
|
||||||
} else {
|
getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken");
|
||||||
getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken");
|
|
||||||
}
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
Ln.e("Could not find method", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getBuiltInDisplayMethod;
|
return getBuiltInDisplayMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IBinder getBuiltInDisplay() {
|
public static IBinder getBuiltInDisplay() {
|
||||||
Method method = getGetBuiltInDisplayMethod();
|
|
||||||
if (method == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
Method method = getGetBuiltInDisplayMethod();
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
// call getBuiltInDisplay(0)
|
// call getBuiltInDisplay(0)
|
||||||
return (IBinder) method.invoke(null, 0);
|
return (IBinder) method.invoke(null, 0);
|
||||||
@ -114,32 +108,25 @@ public final class SurfaceControl {
|
|||||||
|
|
||||||
// call getInternalDisplayToken()
|
// call getInternalDisplayToken()
|
||||||
return (IBinder) method.invoke(null);
|
return (IBinder) method.invoke(null);
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
Ln.e("Could not invoke method", e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Method getSetDisplayPowerModeMethod() {
|
private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException {
|
||||||
if (setDisplayPowerModeMethod == null) {
|
if (setDisplayPowerModeMethod == null) {
|
||||||
try {
|
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);
|
||||||
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
Ln.e("Could not find method", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return setDisplayPowerModeMethod;
|
return setDisplayPowerModeMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setDisplayPowerMode(IBinder displayToken, int mode) {
|
public static void setDisplayPowerMode(IBinder displayToken, int mode) {
|
||||||
Method method = getSetDisplayPowerModeMethod();
|
|
||||||
if (method == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
Method method = getSetDisplayPowerModeMethod();
|
||||||
method.invoke(null, displayToken, mode);
|
method.invoke(null, displayToken, mode);
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
Ln.e("Could not invoke method", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,27 +1,95 @@
|
|||||||
package com.genymobile.scrcpy.wrappers;
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.Ln;
|
||||||
|
|
||||||
import android.os.IInterface;
|
import android.os.IInterface;
|
||||||
import android.view.IRotationWatcher;
|
import android.view.IRotationWatcher;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
public final class WindowManager {
|
public final class WindowManager {
|
||||||
private final IInterface manager;
|
private final IInterface manager;
|
||||||
|
private Method getRotationMethod;
|
||||||
|
private Method freezeRotationMethod;
|
||||||
|
private Method isRotationFrozenMethod;
|
||||||
|
private Method thawRotationMethod;
|
||||||
|
|
||||||
public WindowManager(IInterface manager) {
|
public WindowManager(IInterface manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getRotation() {
|
private Method getGetRotationMethod() throws NoSuchMethodException {
|
||||||
try {
|
if (getRotationMethod == null) {
|
||||||
Class<?> cls = manager.getClass();
|
Class<?> cls = manager.getClass();
|
||||||
try {
|
try {
|
||||||
return (Integer) cls.getMethod("getRotation").invoke(manager);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
// method changed since this commit:
|
// method changed since this commit:
|
||||||
// https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2
|
// 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 {
|
try {
|
||||||
Class<?> cls = manager.getClass();
|
Class<?> cls = manager.getClass();
|
||||||
try {
|
try {
|
||||||
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
// display parameter added since this commit:
|
// display parameter added since this commit:
|
||||||
// https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
|
// https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
|
||||||
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0);
|
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0);
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
// old version
|
||||||
|
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
|
@ -240,6 +240,22 @@ public class ControlMessageReaderTest {
|
|||||||
Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction());
|
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
|
@Test
|
||||||
public void testMultiEvents() throws IOException {
|
public void testMultiEvents() throws IOException {
|
||||||
ControlMessageReader reader = new ControlMessageReader();
|
ControlMessageReader reader = new ControlMessageReader();
|
||||||
|
Reference in New Issue
Block a user