Compare commits
4 Commits
v1.11
...
window-par
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad6b8847d4 | ||
|
|
fba89e6f73 | ||
|
|
4bf2b80c75 | ||
|
|
f983ec67fa |
30
DEVELOP.md
30
DEVELOP.md
@@ -268,33 +268,3 @@ For more details, go read the code!
|
|||||||
|
|
||||||
If you find a bug, or have an awesome idea to implement, please discuss and
|
If you find a bug, or have an awesome idea to implement, please discuss and
|
||||||
contribute ;-)
|
contribute ;-)
|
||||||
|
|
||||||
|
|
||||||
### Debug the server
|
|
||||||
|
|
||||||
The server is pushed to the device by the client on startup.
|
|
||||||
|
|
||||||
To debug it, enable the server debugger during configuration:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
meson x -Dserver_debugger=true
|
|
||||||
# or, if x is already configured
|
|
||||||
meson configure x -Dserver_debugger=true
|
|
||||||
```
|
|
||||||
|
|
||||||
Then recompile.
|
|
||||||
|
|
||||||
When you start scrcpy, it will start a debugger on port 5005 on the device.
|
|
||||||
Redirect that port to the computer:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
adb forward tcp:5005 tcp:5005
|
|
||||||
```
|
|
||||||
|
|
||||||
In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on
|
|
||||||
`+`, _Remote_, and fill the form:
|
|
||||||
|
|
||||||
- Host: `localhost`
|
|
||||||
- Port: `5005`
|
|
||||||
|
|
||||||
Then click on _Debug_.
|
|
||||||
|
|||||||
@@ -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.2.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
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/avcodec-58.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/avformat-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/swresample-3.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/swscale-5.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/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.2.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
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/avcodec-58.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/avformat-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/swresample-3.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/swscale-5.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/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)/"
|
||||||
|
|||||||
249
README.md
249
README.md
@@ -108,9 +108,8 @@ 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.
|
||||||
@@ -126,7 +125,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):
|
||||||
|
|
||||||
@@ -135,15 +134,8 @@ scrcpy --bit-rate 2M
|
|||||||
scrcpy -b 2M # short version
|
scrcpy -b 2M # short version
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Limit frame rate
|
|
||||||
|
|
||||||
On devices with Android >= 10, the capture frame rate can be limited:
|
### Crop
|
||||||
|
|
||||||
```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.
|
||||||
|
|
||||||
@@ -151,12 +143,35 @@ 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.
|
||||||
|
|
||||||
|
|
||||||
### Recording
|
### Wireless
|
||||||
|
|
||||||
|
_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
|
||||||
|
device over TCP/IP:
|
||||||
|
|
||||||
|
1. Connect the device to the same Wi-Fi as your computer.
|
||||||
|
2. Get your device IP address (in Settings → About phone → Status).
|
||||||
|
3. Enable adb over TCP/IP on your device: `adb tcpip 5555`.
|
||||||
|
4. Unplug your device.
|
||||||
|
5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_.
|
||||||
|
6. Run `scrcpy` as usual.
|
||||||
|
|
||||||
|
It may be useful to decrease the bit-rate and the definition:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --bit-rate 2M --max-size 800
|
||||||
|
scrcpy -b2M -m800 # short version
|
||||||
|
```
|
||||||
|
|
||||||
|
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
|
||||||
|
|
||||||
|
|
||||||
|
### Record screen
|
||||||
|
|
||||||
It is possible to record the screen while mirroring:
|
It is possible to record the screen while mirroring:
|
||||||
|
|
||||||
@@ -181,31 +196,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
### Connection
|
### Multi-devices
|
||||||
|
|
||||||
#### 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_:
|
||||||
|
|
||||||
@@ -216,65 +207,8 @@ scrcpy -s 0123456789abcdef # short version
|
|||||||
|
|
||||||
You can start several instances of _scrcpy_ for several devices.
|
You can start several instances of _scrcpy_ for several devices.
|
||||||
|
|
||||||
#### SSH tunnel
|
|
||||||
|
|
||||||
To connect to a remote device, it is possible to connect a local `adb` client to
|
### Fullscreen
|
||||||
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:
|
||||||
|
|
||||||
@@ -286,45 +220,17 @@ scrcpy -f # short version
|
|||||||
Fullscreen can then be toggled dynamically with `Ctrl`+`f`.
|
Fullscreen can then be toggled dynamically with `Ctrl`+`f`.
|
||||||
|
|
||||||
|
|
||||||
### Other mirroring options
|
### Always on top
|
||||||
|
|
||||||
#### Read-only
|
The window of app can always be above others by:
|
||||||
|
|
||||||
To disable controls (everything which can interact with the device: input keys,
|
|
||||||
mouse events, drag&drop files):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --no-control
|
scrcpy --always-on-top
|
||||||
scrcpy -n
|
scrcpy -T # short version
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Turn screen off
|
|
||||||
|
|
||||||
It is possible to turn the device screen off while mirroring on start with a
|
### Show touches
|
||||||
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).
|
||||||
@@ -341,43 +247,7 @@ 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).
|
||||||
|
|
||||||
|
|
||||||
### Input control
|
### Install APK
|
||||||
|
|
||||||
#### 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.
|
||||||
@@ -385,7 +255,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.
|
||||||
@@ -398,8 +268,53 @@ The target directory can be changed on start:
|
|||||||
scrcpy --push-target /sdcard/foo/bar/
|
scrcpy --push-target /sdcard/foo/bar/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Read-only
|
||||||
|
|
||||||
### Audio forwarding
|
To disable controls (everything which can interact with the device: input keys,
|
||||||
|
mouse events, drag&drop files):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --no-control
|
||||||
|
scrcpy -n
|
||||||
|
```
|
||||||
|
|
||||||
|
### Turn screen off
|
||||||
|
|
||||||
|
It is possible to turn the device screen off while mirroring on start with a
|
||||||
|
command-line option:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --turn-screen-off
|
||||||
|
scrcpy -S
|
||||||
|
```
|
||||||
|
|
||||||
|
Or by pressing `Ctrl`+`o` at any time.
|
||||||
|
|
||||||
|
To turn it back on, press `POWER` (or `Ctrl`+`p`).
|
||||||
|
|
||||||
|
|
||||||
|
### Render expired frames
|
||||||
|
|
||||||
|
By default, to minimize latency, _scrcpy_ always renders the last decoded frame
|
||||||
|
available, and drops any previous one.
|
||||||
|
|
||||||
|
To force the rendering of all frames (at a cost of a possible increased
|
||||||
|
latency), use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --render-expired-frames
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom window title
|
||||||
|
|
||||||
|
By default, the window title is the device model. It can be changed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --window-title 'My device'
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Forward audio
|
||||||
|
|
||||||
Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only).
|
Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only).
|
||||||
|
|
||||||
|
|||||||
@@ -115,16 +115,15 @@ conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))
|
|||||||
# disable console on Windows
|
# disable console on Windows
|
||||||
conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole'))
|
conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole'))
|
||||||
|
|
||||||
# run a server debugger and wait for a client to be attached
|
|
||||||
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
|
|
||||||
|
|
||||||
configure_file(configuration: conf, output: 'config.h')
|
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')
|
||||||
link_args = [ '-Wl,--subsystem,windows' ]
|
c_args = [ '-mwindows' ]
|
||||||
|
link_args = [ '-mwindows' ]
|
||||||
else
|
else
|
||||||
|
c_args = []
|
||||||
link_args = []
|
link_args = []
|
||||||
endif
|
endif
|
||||||
|
|
||||||
@@ -132,7 +131,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')
|
||||||
|
|||||||
35
app/scrcpy.1
35
app/scrcpy.1
@@ -15,10 +15,6 @@ 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).
|
||||||
@@ -26,7 +22,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 \-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
.BI "\-c, \-\-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
|
||||||
@@ -38,12 +34,12 @@ value is computed on the cropped size.
|
|||||||
Start in fullscreen.
|
Start in fullscreen.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-h, \-\-help
|
.BI "\-F, \-\-record\-format " format
|
||||||
Print this help.
|
Force recording format (either mp4 or mkv).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI \-\-max\-fps " value
|
.B \-h, \-\-help
|
||||||
Limit the framerate of screen capture (only supported on devices with Android >= 10).
|
Print this help.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-m, \-\-max\-size " value
|
.BI "\-m, \-\-max\-size " value
|
||||||
@@ -65,13 +61,6 @@ 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".
|
||||||
@@ -84,13 +73,9 @@ Record screen to
|
|||||||
.IR file .
|
.IR file .
|
||||||
|
|
||||||
The format is determined by the
|
The format is determined by the
|
||||||
.B \-\-record\-format
|
.B \-F/\-\-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.
|
||||||
@@ -110,12 +95,12 @@ 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
|
.TP
|
||||||
.B \-v, \-\-version
|
.B \-T, \-\-always\-on\-top
|
||||||
Print the version of scrcpy.
|
Make scrcpy window always on top (above other windows).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-\-window\-borderless
|
.B \-v, \-\-version
|
||||||
Disable window decorations (display borderless window).
|
Print the version of scrcpy.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI \-\-window\-title " text
|
.BI \-\-window\-title " text
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#define MAP(FROM, TO) case FROM: *to = TO; return true
|
#define MAP(FROM, TO) case FROM: *to = TO; return true
|
||||||
#define FAIL default: return false
|
#define FAIL default: return false
|
||||||
|
|
||||||
bool
|
static bool
|
||||||
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
|
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
|
||||||
switch (from) {
|
switch (from) {
|
||||||
MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN);
|
MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN);
|
||||||
@@ -33,7 +33,7 @@ autocomplete_metastate(enum android_metastate metastate) {
|
|||||||
return metastate;
|
return metastate;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum android_metastate
|
static enum android_metastate
|
||||||
convert_meta_state(SDL_Keymod mod) {
|
convert_meta_state(SDL_Keymod mod) {
|
||||||
enum android_metastate metastate = 0;
|
enum android_metastate metastate = 0;
|
||||||
if (mod & KMOD_LSHIFT) {
|
if (mod & KMOD_LSHIFT) {
|
||||||
@@ -74,9 +74,8 @@ convert_meta_state(SDL_Keymod mod) {
|
|||||||
return autocomplete_metastate(metastate);
|
return autocomplete_metastate(metastate);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
static 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);
|
||||||
@@ -93,12 +92,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -135,7 +128,7 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum android_motionevent_buttons
|
static enum android_motionevent_buttons
|
||||||
convert_mouse_buttons(uint32_t state) {
|
convert_mouse_buttons(uint32_t state) {
|
||||||
enum android_motionevent_buttons buttons = 0;
|
enum android_motionevent_buttons buttons = 0;
|
||||||
if (state & SDL_BUTTON_LMASK) {
|
if (state & SDL_BUTTON_LMASK) {
|
||||||
@@ -157,6 +150,24 @@ convert_mouse_buttons(uint32_t state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) {
|
||||||
|
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
|
||||||
|
|
||||||
|
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t mod = from->keysym.mod;
|
||||||
|
if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
to->inject_keycode.metastate = convert_meta_state(mod);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
|
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
|
||||||
switch (from) {
|
switch (from) {
|
||||||
MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN);
|
MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN);
|
||||||
@@ -166,6 +177,41 @@ convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size,
|
||||||
|
struct control_msg *to) {
|
||||||
|
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
|
||||||
|
|
||||||
|
if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
|
||||||
|
to->inject_touch_event.position.screen_size = screen_size;
|
||||||
|
to->inject_touch_event.position.point.x = from->x;
|
||||||
|
to->inject_touch_event.position.point.y = from->y;
|
||||||
|
to->inject_touch_event.pressure = 1.f;
|
||||||
|
to->inject_touch_event.buttons =
|
||||||
|
convert_mouse_buttons(SDL_BUTTON(from->button));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size,
|
||||||
|
struct control_msg *to) {
|
||||||
|
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
|
||||||
|
to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE;
|
||||||
|
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
|
||||||
|
to->inject_touch_event.position.screen_size = screen_size;
|
||||||
|
to->inject_touch_event.position.point.x = from->x;
|
||||||
|
to->inject_touch_event.position.point.y = from->y;
|
||||||
|
to->inject_touch_event.pressure = 1.f;
|
||||||
|
to->inject_touch_event.buttons = convert_mouse_buttons(from->state);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) {
|
convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) {
|
||||||
switch (from) {
|
switch (from) {
|
||||||
MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE);
|
MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE);
|
||||||
@@ -174,3 +220,39 @@ convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) {
|
|||||||
FAIL;
|
FAIL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size,
|
||||||
|
struct control_msg *to) {
|
||||||
|
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
|
||||||
|
|
||||||
|
if (!convert_touch_action(from->type, &to->inject_touch_event.action)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
to->inject_touch_event.pointer_id = from->fingerId;
|
||||||
|
to->inject_touch_event.position.screen_size = screen_size;
|
||||||
|
// SDL touch event coordinates are normalized in the range [0; 1]
|
||||||
|
to->inject_touch_event.position.point.x = from->x * screen_size.width;
|
||||||
|
to->inject_touch_event.position.point.y = from->y * screen_size.height;
|
||||||
|
to->inject_touch_event.pressure = from->pressure;
|
||||||
|
to->inject_touch_event.buttons = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position,
|
||||||
|
struct control_msg *to) {
|
||||||
|
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
|
||||||
|
|
||||||
|
to->inject_scroll_event.position = position;
|
||||||
|
|
||||||
|
int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
|
||||||
|
// SDL behavior seems inconsistent between horizontal and vertical scrolling
|
||||||
|
// so reverse the horizontal
|
||||||
|
// <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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,23 +7,36 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "control_msg.h"
|
#include "control_msg.h"
|
||||||
|
|
||||||
bool
|
struct complete_mouse_motion_event {
|
||||||
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to);
|
SDL_MouseMotionEvent *mouse_motion_event;
|
||||||
|
struct size screen_size;
|
||||||
|
};
|
||||||
|
|
||||||
enum android_metastate
|
struct complete_mouse_wheel_event {
|
||||||
convert_meta_state(SDL_Keymod mod);
|
SDL_MouseWheelEvent *mouse_wheel_event;
|
||||||
|
struct point position;
|
||||||
|
};
|
||||||
|
|
||||||
bool
|
bool
|
||||||
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
|
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to);
|
||||||
bool prefer_text);
|
|
||||||
|
|
||||||
enum android_motionevent_buttons
|
|
||||||
convert_mouse_buttons(uint32_t state);
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to);
|
convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size,
|
||||||
|
struct control_msg *to);
|
||||||
|
|
||||||
|
// the video size may be different from the real device size, so we need the
|
||||||
|
// size to which the absolute position apply, to scale it accordingly
|
||||||
|
bool
|
||||||
|
convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size,
|
||||||
|
struct control_msg *to);
|
||||||
|
|
||||||
bool
|
bool
|
||||||
convert_touch_action(SDL_EventType from, enum android_motionevent_action *to);
|
convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size,
|
||||||
|
struct control_msg *to);
|
||||||
|
|
||||||
|
// on Android, a scroll event requires the current mouse position
|
||||||
|
bool
|
||||||
|
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position,
|
||||||
|
struct control_msg *to);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -212,17 +212,14 @@ clipboard_paste(struct controller *controller) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_text_input(struct input_manager *im,
|
input_manager_process_text_input(struct input_manager *input_manager,
|
||||||
const SDL_TextInputEvent *event) {
|
const SDL_TextInputEvent *event) {
|
||||||
if (!im->prefer_text) {
|
|
||||||
char c = event->text[0];
|
char c = event->text[0];
|
||||||
if (isalpha(c) || c == ' ') {
|
if (isalpha(c) || c == ' ') {
|
||||||
SDL_assert(event->text[1] == '\0');
|
SDL_assert(event->text[1] == '\0');
|
||||||
// letters and space are handled as raw key event
|
// letters and space are handled as raw key event
|
||||||
return;
|
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);
|
||||||
@@ -230,34 +227,14 @@ input_manager_process_text_input(struct input_manager *im,
|
|||||||
LOGW("Could not strdup input text");
|
LOGW("Could not strdup input text");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!controller_push_msg(im->controller, &msg)) {
|
if (!controller_push_msg(input_manager->controller, &msg)) {
|
||||||
SDL_free(msg.inject_text.text);
|
SDL_free(msg.inject_text.text);
|
||||||
LOGW("Could not request 'inject text'");
|
LOGW("Could not request 'inject text'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
|
||||||
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
|
|
||||||
bool prefer_text) {
|
|
||||||
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
|
|
||||||
|
|
||||||
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t mod = from->keysym.mod;
|
|
||||||
if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod,
|
|
||||||
prefer_text)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
to->inject_keycode.metastate = convert_meta_state(mod);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_key(struct input_manager *im,
|
input_manager_process_key(struct input_manager *input_manager,
|
||||||
const SDL_KeyboardEvent *event,
|
const SDL_KeyboardEvent *event,
|
||||||
bool control) {
|
bool control) {
|
||||||
// control: indicates the state of the command-line option --no-control
|
// control: indicates the state of the command-line option --no-control
|
||||||
@@ -284,7 +261,7 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct controller *controller = im->controller;
|
struct controller *controller = input_manager->controller;
|
||||||
|
|
||||||
// capture all Ctrl events
|
// capture all Ctrl events
|
||||||
if (ctrl || cmd) {
|
if (ctrl || cmd) {
|
||||||
@@ -359,23 +336,23 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
return;
|
return;
|
||||||
case SDLK_f:
|
case SDLK_f:
|
||||||
if (!shift && cmd && !repeat && down) {
|
if (!shift && cmd && !repeat && down) {
|
||||||
screen_switch_fullscreen(im->screen);
|
screen_switch_fullscreen(input_manager->screen);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_x:
|
case SDLK_x:
|
||||||
if (!shift && cmd && !repeat && down) {
|
if (!shift && cmd && !repeat && down) {
|
||||||
screen_resize_to_fit(im->screen);
|
screen_resize_to_fit(input_manager->screen);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_g:
|
case SDLK_g:
|
||||||
if (!shift && cmd && !repeat && down) {
|
if (!shift && cmd && !repeat && down) {
|
||||||
screen_resize_to_pixel_perfect(im->screen);
|
screen_resize_to_pixel_perfect(input_manager->screen);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_i:
|
case SDLK_i:
|
||||||
if (!shift && cmd && !repeat && down) {
|
if (!shift && cmd && !repeat && down) {
|
||||||
struct fps_counter *fps_counter =
|
struct fps_counter *fps_counter =
|
||||||
im->video_buffer->fps_counter;
|
input_manager->video_buffer->fps_counter;
|
||||||
switch_fps_counter_state(fps_counter);
|
switch_fps_counter_state(fps_counter);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -398,30 +375,15 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
if (convert_input_key(event, &msg, im->prefer_text)) {
|
if (convert_input_key(event, &msg)) {
|
||||||
if (!controller_push_msg(controller, &msg)) {
|
if (!controller_push_msg(controller, &msg)) {
|
||||||
LOGW("Could not request 'inject keycode'");
|
LOGW("Could not request 'inject keycode'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
|
||||||
convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen,
|
|
||||||
struct control_msg *to) {
|
|
||||||
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
|
|
||||||
to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE;
|
|
||||||
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
|
|
||||||
to->inject_touch_event.position.screen_size = screen->frame_size;
|
|
||||||
to->inject_touch_event.position.point.x = from->x;
|
|
||||||
to->inject_touch_event.position.point.y = from->y;
|
|
||||||
to->inject_touch_event.pressure = 1.f;
|
|
||||||
to->inject_touch_event.buttons = convert_mouse_buttons(from->state);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_mouse_motion(struct input_manager *im,
|
input_manager_process_mouse_motion(struct input_manager *input_manager,
|
||||||
const SDL_MouseMotionEvent *event) {
|
const SDL_MouseMotionEvent *event) {
|
||||||
if (!event->state) {
|
if (!event->state) {
|
||||||
// do not send motion events when no button is pressed
|
// do not send motion events when no button is pressed
|
||||||
@@ -432,74 +394,33 @@ input_manager_process_mouse_motion(struct input_manager *im,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
if (convert_mouse_motion(event, im->screen, &msg)) {
|
if (convert_mouse_motion(event, input_manager->screen->frame_size, &msg)) {
|
||||||
if (!controller_push_msg(im->controller, &msg)) {
|
if (!controller_push_msg(input_manager->controller, &msg)) {
|
||||||
LOGW("Could not request 'inject mouse motion event'");
|
LOGW("Could not request 'inject mouse motion event'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
|
||||||
convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen,
|
|
||||||
struct control_msg *to) {
|
|
||||||
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
|
|
||||||
|
|
||||||
if (!convert_touch_action(from->type, &to->inject_touch_event.action)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct size frame_size = screen->frame_size;
|
|
||||||
|
|
||||||
to->inject_touch_event.pointer_id = from->fingerId;
|
|
||||||
to->inject_touch_event.position.screen_size = frame_size;
|
|
||||||
// SDL touch event coordinates are normalized in the range [0; 1]
|
|
||||||
to->inject_touch_event.position.point.x = from->x * frame_size.width;
|
|
||||||
to->inject_touch_event.position.point.y = from->y * frame_size.height;
|
|
||||||
to->inject_touch_event.pressure = from->pressure;
|
|
||||||
to->inject_touch_event.buttons = 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_touch(struct input_manager *im,
|
input_manager_process_touch(struct input_manager *input_manager,
|
||||||
const SDL_TouchFingerEvent *event) {
|
const SDL_TouchFingerEvent *event) {
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
if (convert_touch(event, im->screen, &msg)) {
|
if (convert_touch(event, input_manager->screen->frame_size, &msg)) {
|
||||||
if (!controller_push_msg(im->controller, &msg)) {
|
if (!controller_push_msg(input_manager->controller, &msg)) {
|
||||||
LOGW("Could not request 'inject touch event'");
|
LOGW("Could not request 'inject touch event'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
is_outside_device_screen(struct input_manager *im, int x, int y)
|
is_outside_device_screen(struct input_manager *input_manager, int x, int y)
|
||||||
{
|
{
|
||||||
return x < 0 || x >= im->screen->frame_size.width ||
|
return x < 0 || x >= input_manager->screen->frame_size.width ||
|
||||||
y < 0 || y >= im->screen->frame_size.height;
|
y < 0 || y >= input_manager->screen->frame_size.height;
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
|
|
||||||
struct control_msg *to) {
|
|
||||||
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
|
|
||||||
|
|
||||||
if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
|
|
||||||
to->inject_touch_event.position.screen_size = screen->frame_size;
|
|
||||||
to->inject_touch_event.position.point.x = from->x;
|
|
||||||
to->inject_touch_event.position.point.y = from->y;
|
|
||||||
to->inject_touch_event.pressure = 1.f;
|
|
||||||
to->inject_touch_event.buttons =
|
|
||||||
convert_mouse_buttons(SDL_BUTTON(from->button));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_mouse_button(struct input_manager *im,
|
input_manager_process_mouse_button(struct input_manager *input_manager,
|
||||||
const SDL_MouseButtonEvent *event,
|
const SDL_MouseButtonEvent *event,
|
||||||
bool control) {
|
bool control) {
|
||||||
if (event->which == SDL_TOUCH_MOUSEID) {
|
if (event->which == SDL_TOUCH_MOUSEID) {
|
||||||
@@ -508,19 +429,19 @@ input_manager_process_mouse_button(struct input_manager *im,
|
|||||||
}
|
}
|
||||||
if (event->type == SDL_MOUSEBUTTONDOWN) {
|
if (event->type == SDL_MOUSEBUTTONDOWN) {
|
||||||
if (control && event->button == SDL_BUTTON_RIGHT) {
|
if (control && event->button == SDL_BUTTON_RIGHT) {
|
||||||
press_back_or_turn_screen_on(im->controller);
|
press_back_or_turn_screen_on(input_manager->controller);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (control && event->button == SDL_BUTTON_MIDDLE) {
|
if (control && event->button == SDL_BUTTON_MIDDLE) {
|
||||||
action_home(im->controller, ACTION_DOWN | ACTION_UP);
|
action_home(input_manager->controller, ACTION_DOWN | ACTION_UP);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// double-click on black borders resize to fit the device screen
|
// double-click on black borders resize to fit the device screen
|
||||||
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
|
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
|
||||||
bool outside =
|
bool outside =
|
||||||
is_outside_device_screen(im, event->x, event->y);
|
is_outside_device_screen(input_manager, event->x, event->y);
|
||||||
if (outside) {
|
if (outside) {
|
||||||
screen_resize_to_fit(im->screen);
|
screen_resize_to_fit(input_manager->screen);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -532,41 +453,23 @@ input_manager_process_mouse_button(struct input_manager *im,
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
if (convert_mouse_button(event, im->screen, &msg)) {
|
if (convert_mouse_button(event, input_manager->screen->frame_size, &msg)) {
|
||||||
if (!controller_push_msg(im->controller, &msg)) {
|
if (!controller_push_msg(input_manager->controller, &msg)) {
|
||||||
LOGW("Could not request 'inject mouse button event'");
|
LOGW("Could not request 'inject mouse button event'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
|
||||||
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
|
|
||||||
struct control_msg *to) {
|
|
||||||
struct position position = {
|
|
||||||
.screen_size = screen->frame_size,
|
|
||||||
.point = get_mouse_point(screen),
|
|
||||||
};
|
|
||||||
|
|
||||||
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
|
|
||||||
|
|
||||||
to->inject_scroll_event.position = position;
|
|
||||||
|
|
||||||
int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
|
|
||||||
// SDL behavior seems inconsistent between horizontal and vertical scrolling
|
|
||||||
// so reverse the horizontal
|
|
||||||
// <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;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_mouse_wheel(struct input_manager *im,
|
input_manager_process_mouse_wheel(struct input_manager *input_manager,
|
||||||
const SDL_MouseWheelEvent *event) {
|
const SDL_MouseWheelEvent *event) {
|
||||||
|
struct position position = {
|
||||||
|
.screen_size = input_manager->screen->frame_size,
|
||||||
|
.point = get_mouse_point(input_manager->screen),
|
||||||
|
};
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
if (convert_mouse_wheel(event, im->screen, &msg)) {
|
if (convert_mouse_wheel(event, position, &msg)) {
|
||||||
if (!controller_push_msg(im->controller, &msg)) {
|
if (!controller_push_msg(input_manager->controller, &msg)) {
|
||||||
LOGW("Could not request 'inject mouse wheel event'");
|
LOGW("Could not request 'inject mouse wheel event'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,33 +14,32 @@ 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
|
||||||
input_manager_process_text_input(struct input_manager *im,
|
input_manager_process_text_input(struct input_manager *input_manager,
|
||||||
const SDL_TextInputEvent *event);
|
const SDL_TextInputEvent *event);
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_key(struct input_manager *im,
|
input_manager_process_key(struct input_manager *input_manager,
|
||||||
const SDL_KeyboardEvent *event,
|
const SDL_KeyboardEvent *event,
|
||||||
bool control);
|
bool control);
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_mouse_motion(struct input_manager *im,
|
input_manager_process_mouse_motion(struct input_manager *input_manager,
|
||||||
const SDL_MouseMotionEvent *event);
|
const SDL_MouseMotionEvent *event);
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_touch(struct input_manager *im,
|
input_manager_process_touch(struct input_manager *input_manager,
|
||||||
const SDL_TouchFingerEvent *event);
|
const SDL_TouchFingerEvent *event);
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_mouse_button(struct input_manager *im,
|
input_manager_process_mouse_button(struct input_manager *input_manager,
|
||||||
const SDL_MouseButtonEvent *event,
|
const SDL_MouseButtonEvent *event,
|
||||||
bool control);
|
bool control);
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_mouse_wheel(struct input_manager *im,
|
input_manager_process_mouse_wheel(struct input_manager *input_manager,
|
||||||
const SDL_MouseWheelEvent *event);
|
const SDL_MouseWheelEvent *event);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
238
app/src/main.c
238
app/src/main.c
@@ -14,9 +14,29 @@
|
|||||||
#include "recorder.h"
|
#include "recorder.h"
|
||||||
|
|
||||||
struct args {
|
struct args {
|
||||||
struct scrcpy_options opts;
|
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 help;
|
||||||
bool version;
|
bool version;
|
||||||
|
bool show_touches;
|
||||||
|
uint16_t port;
|
||||||
|
uint16_t max_size;
|
||||||
|
uint32_t bit_rate;
|
||||||
|
int16_t window_x;
|
||||||
|
int16_t window_y;
|
||||||
|
uint16_t window_width;
|
||||||
|
uint16_t window_height;
|
||||||
|
bool always_on_top;
|
||||||
|
bool turn_screen_off;
|
||||||
|
bool render_expired_frames;
|
||||||
|
bool window_borderless;
|
||||||
};
|
};
|
||||||
|
|
||||||
static void usage(const char *arg0) {
|
static void usage(const char *arg0) {
|
||||||
@@ -30,15 +50,12 @@ static void usage(const char *arg0) {
|
|||||||
"\n"
|
"\n"
|
||||||
"Options:\n"
|
"Options:\n"
|
||||||
"\n"
|
"\n"
|
||||||
" --always-on-top\n"
|
|
||||||
" Make scrcpy window always on top (above other windows).\n"
|
|
||||||
"\n"
|
|
||||||
" -b, --bit-rate value\n"
|
" -b, --bit-rate value\n"
|
||||||
" Encode the video at the given bit-rate, expressed in bits/s.\n"
|
" Encode the video at the given bit-rate, expressed in bits/s.\n"
|
||||||
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||||
" Default is %d.\n"
|
" Default is %d.\n"
|
||||||
"\n"
|
"\n"
|
||||||
" --crop width:height:x:y\n"
|
" -c, --crop width:height:x:y\n"
|
||||||
" Crop the device screen on the server.\n"
|
" Crop the device screen on the server.\n"
|
||||||
" The values are expressed in the device natural orientation\n"
|
" The values are expressed in the device natural orientation\n"
|
||||||
" (typically, portrait for a phone, landscape for a tablet).\n"
|
" (typically, portrait for a phone, landscape for a tablet).\n"
|
||||||
@@ -47,13 +64,12 @@ static void usage(const char *arg0) {
|
|||||||
" -f, --fullscreen\n"
|
" -f, --fullscreen\n"
|
||||||
" Start in fullscreen.\n"
|
" Start in fullscreen.\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" -F, --record-format format\n"
|
||||||
|
" Force recording format (either mp4 or mkv).\n"
|
||||||
|
"\n"
|
||||||
" -h, --help\n"
|
" -h, --help\n"
|
||||||
" Print this help.\n"
|
" Print this help.\n"
|
||||||
"\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"
|
" -m, --max-size value\n"
|
||||||
" Limit both the width and height of the video to value. The\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"
|
" other dimension is computed so that the device aspect-ratio\n"
|
||||||
@@ -71,13 +87,6 @@ static void usage(const char *arg0) {
|
|||||||
" Set the TCP port the client listens on.\n"
|
" Set the TCP port the client listens on.\n"
|
||||||
" Default is %d.\n"
|
" Default is %d.\n"
|
||||||
"\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"
|
" --push-target path\n"
|
||||||
" Set the target directory for pushing files to the device by\n"
|
" Set the target directory for pushing files to the device by\n"
|
||||||
" drag & drop. It is passed as-is to \"adb push\".\n"
|
" drag & drop. It is passed as-is to \"adb push\".\n"
|
||||||
@@ -85,12 +94,9 @@ static void usage(const char *arg0) {
|
|||||||
"\n"
|
"\n"
|
||||||
" -r, --record file.mp4\n"
|
" -r, --record file.mp4\n"
|
||||||
" Record screen to file.\n"
|
" Record screen to file.\n"
|
||||||
" The format is determined by the --record-format option if\n"
|
" The format is determined by the -F/--record-format option if\n"
|
||||||
" set, or by the file extension (.mp4 or .mkv).\n"
|
" set, or by the file extension (.mp4 or .mkv).\n"
|
||||||
"\n"
|
"\n"
|
||||||
" --record-format format\n"
|
|
||||||
" Force recording format (either mp4 or mkv).\n"
|
|
||||||
"\n"
|
|
||||||
" --render-expired-frames\n"
|
" --render-expired-frames\n"
|
||||||
" By default, to minimize latency, scrcpy always renders the\n"
|
" By default, to minimize latency, scrcpy always renders the\n"
|
||||||
" last available decoded frame, and drops any previous ones.\n"
|
" last available decoded frame, and drops any previous ones.\n"
|
||||||
@@ -108,10 +114,13 @@ static void usage(const char *arg0) {
|
|||||||
" Enable \"show touches\" on start, disable on quit.\n"
|
" Enable \"show touches\" on start, disable on quit.\n"
|
||||||
" It only shows physical touches (not clicks from scrcpy).\n"
|
" It only shows physical touches (not clicks from scrcpy).\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" -T, --always-on-top\n"
|
||||||
|
" Make scrcpy window always on top (above other windows).\n"
|
||||||
|
"\n"
|
||||||
" -v, --version\n"
|
" -v, --version\n"
|
||||||
" Print the version of scrcpy.\n"
|
" Print the version of scrcpy.\n"
|
||||||
"\n"
|
"\n"
|
||||||
" --window-borderless\n"
|
" --window_borderless\n"
|
||||||
" Disable window decorations (display borderless window).\n"
|
" Disable window decorations (display borderless window).\n"
|
||||||
"\n"
|
"\n"
|
||||||
" --window-title text\n"
|
" --window-title text\n"
|
||||||
@@ -127,11 +136,11 @@ static void usage(const char *arg0) {
|
|||||||
"\n"
|
"\n"
|
||||||
" --window-width value\n"
|
" --window-width value\n"
|
||||||
" Set the initial window width.\n"
|
" Set the initial window width.\n"
|
||||||
" Default is 0 (automatic).\n"
|
" Default is -1 (automatic).\n"
|
||||||
"\n"
|
"\n"
|
||||||
" --window-height value\n"
|
" --window-height value\n"
|
||||||
" Set the initial window width.\n"
|
" Set the initial window width.\n"
|
||||||
" Default is 0 (automatic).\n"
|
" Default is -1 (automatic).\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Shortcuts:\n"
|
"Shortcuts:\n"
|
||||||
"\n"
|
"\n"
|
||||||
@@ -273,28 +282,6 @@ parse_max_size(char *optarg, uint16_t *max_size) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
|
||||||
parse_max_fps(const char *optarg, uint16_t *max_fps) {
|
|
||||||
char *endptr;
|
|
||||||
if (*optarg == '\0') {
|
|
||||||
LOGE("Max FPS parameter is empty");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
long value = strtol(optarg, &endptr, 0);
|
|
||||||
if (*endptr != '\0') {
|
|
||||||
LOGE("Invalid max FPS: %s", optarg);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (value & ~0xffff) {
|
|
||||||
// in practice, it should not be higher than 60
|
|
||||||
LOGE("Max FPS value is invalid: %ld", value);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
*max_fps = (uint16_t) value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
parse_window_position(char *optarg, int16_t *position) {
|
parse_window_position(char *optarg, int16_t *position) {
|
||||||
char *endptr;
|
char *endptr;
|
||||||
@@ -341,7 +328,7 @@ static bool
|
|||||||
parse_port(char *optarg, uint16_t *port) {
|
parse_port(char *optarg, uint16_t *port) {
|
||||||
char *endptr;
|
char *endptr;
|
||||||
if (*optarg == '\0') {
|
if (*optarg == '\0') {
|
||||||
LOGE("Port parameter is empty");
|
LOGE("Invalid port parameter is empty");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
long value = strtol(optarg, &endptr, 0);
|
long value = strtol(optarg, &endptr, 0);
|
||||||
@@ -389,159 +376,133 @@ guess_record_format(const char *filename) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#define OPT_RENDER_EXPIRED_FRAMES 1000
|
#define OPT_RENDER_EXPIRED_FRAMES 1000
|
||||||
#define OPT_WINDOW_TITLE 1001
|
#define OPT_PUSH_TARGET 1001
|
||||||
#define OPT_PUSH_TARGET 1002
|
#define OPT_WINDOW_TITLE 1002
|
||||||
#define OPT_ALWAYS_ON_TOP 1003
|
#define OPT_WINDOW_X 1003
|
||||||
#define OPT_CROP 1004
|
#define OPT_WINDOW_Y 1004
|
||||||
#define OPT_RECORD_FORMAT 1005
|
#define OPT_WINDOW_WIDTH 1005
|
||||||
#define OPT_PREFER_TEXT 1006
|
#define OPT_WINDOW_HEIGHT 1006
|
||||||
#define OPT_WINDOW_X 1007
|
#define OPT_WINDOW_BORDERLESS 1007
|
||||||
#define OPT_WINDOW_Y 1008
|
|
||||||
#define OPT_WINDOW_WIDTH 1009
|
|
||||||
#define OPT_WINDOW_HEIGHT 1010
|
|
||||||
#define OPT_WINDOW_BORDERLESS 1011
|
|
||||||
#define OPT_MAX_FPS 1012
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
parse_args(struct args *args, int argc, char *argv[]) {
|
parse_args(struct args *args, int argc, char *argv[]) {
|
||||||
static const struct option long_options[] = {
|
static const struct option long_options[] = {
|
||||||
{"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
|
{"always-on-top", no_argument, NULL, 'T'},
|
||||||
{"bit-rate", required_argument, NULL, 'b'},
|
{"bit-rate", required_argument, NULL, 'b'},
|
||||||
{"crop", required_argument, NULL, OPT_CROP},
|
{"crop", required_argument, NULL, 'c'},
|
||||||
{"fullscreen", no_argument, NULL, 'f'},
|
{"fullscreen", no_argument, NULL, 'f'},
|
||||||
{"help", no_argument, NULL, 'h'},
|
{"help", no_argument, NULL, 'h'},
|
||||||
{"max-fps", required_argument, NULL, OPT_MAX_FPS},
|
|
||||||
{"max-size", required_argument, NULL, 'm'},
|
{"max-size", required_argument, NULL, 'm'},
|
||||||
{"no-control", no_argument, NULL, 'n'},
|
{"no-control", no_argument, NULL, 'n'},
|
||||||
{"no-display", no_argument, NULL, 'N'},
|
{"no-display", no_argument, NULL, 'N'},
|
||||||
|
{"window-borderless", no_argument, NULL,
|
||||||
|
OPT_WINDOW_BORDERLESS},
|
||||||
{"port", required_argument, NULL, 'p'},
|
{"port", required_argument, NULL, 'p'},
|
||||||
{"push-target", required_argument, NULL, OPT_PUSH_TARGET},
|
{"push-target", required_argument, NULL,
|
||||||
|
OPT_PUSH_TARGET},
|
||||||
{"record", required_argument, NULL, 'r'},
|
{"record", required_argument, NULL, 'r'},
|
||||||
{"record-format", required_argument, NULL, OPT_RECORD_FORMAT},
|
{"record-format", required_argument, NULL, 'F'},
|
||||||
{"render-expired-frames", no_argument, NULL,
|
{"render-expired-frames", no_argument, NULL,
|
||||||
OPT_RENDER_EXPIRED_FRAMES},
|
OPT_RENDER_EXPIRED_FRAMES},
|
||||||
{"serial", required_argument, NULL, 's'},
|
{"serial", required_argument, NULL, 's'},
|
||||||
{"show-touches", no_argument, NULL, 't'},
|
{"show-touches", no_argument, NULL, 't'},
|
||||||
{"turn-screen-off", no_argument, NULL, 'S'},
|
{"turn-screen-off", no_argument, NULL, 'S'},
|
||||||
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
|
|
||||||
{"version", no_argument, NULL, 'v'},
|
{"version", no_argument, NULL, 'v'},
|
||||||
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
|
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
|
||||||
{"window-x", required_argument, NULL, OPT_WINDOW_X},
|
{"window-x", required_argument, NULL, OPT_WINDOW_X},
|
||||||
{"window-y", required_argument, NULL, OPT_WINDOW_Y},
|
{"window-y", required_argument, NULL, OPT_WINDOW_Y},
|
||||||
{"window-width", required_argument, NULL, OPT_WINDOW_WIDTH},
|
{"window-width", required_argument, NULL, OPT_WINDOW_WIDTH},
|
||||||
{"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT},
|
{"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT},
|
||||||
{"window-borderless", no_argument, NULL,
|
|
||||||
OPT_WINDOW_BORDERLESS},
|
|
||||||
{NULL, 0, NULL, 0 },
|
{NULL, 0, NULL, 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
struct scrcpy_options *opts = &args->opts;
|
|
||||||
|
|
||||||
int c;
|
int c;
|
||||||
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options,
|
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options,
|
||||||
NULL)) != -1) {
|
NULL)) != -1) {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case 'b':
|
case 'b':
|
||||||
if (!parse_bit_rate(optarg, &opts->bit_rate)) {
|
if (!parse_bit_rate(optarg, &args->bit_rate)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'c':
|
case 'c':
|
||||||
LOGW("Deprecated option -c. Use --crop instead.");
|
args->crop = optarg;
|
||||||
// fall through
|
|
||||||
case OPT_CROP:
|
|
||||||
opts->crop = optarg;
|
|
||||||
break;
|
break;
|
||||||
case 'f':
|
case 'f':
|
||||||
opts->fullscreen = true;
|
args->fullscreen = true;
|
||||||
break;
|
break;
|
||||||
case 'F':
|
case 'F':
|
||||||
LOGW("Deprecated option -F. Use --record-format instead.");
|
if (!parse_record_format(optarg, &args->record_format)) {
|
||||||
// fall through
|
|
||||||
case OPT_RECORD_FORMAT:
|
|
||||||
if (!parse_record_format(optarg, &opts->record_format)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'h':
|
case 'h':
|
||||||
args->help = true;
|
args->help = true;
|
||||||
break;
|
break;
|
||||||
case OPT_MAX_FPS:
|
|
||||||
if (!parse_max_fps(optarg, &opts->max_fps)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'm':
|
case 'm':
|
||||||
if (!parse_max_size(optarg, &opts->max_size)) {
|
if (!parse_max_size(optarg, &args->max_size)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'n':
|
case 'n':
|
||||||
opts->control = false;
|
args->no_control = true;
|
||||||
break;
|
break;
|
||||||
case 'N':
|
case 'N':
|
||||||
opts->display = false;
|
args->no_display = true;
|
||||||
break;
|
break;
|
||||||
case 'p':
|
case 'p':
|
||||||
if (!parse_port(optarg, &opts->port)) {
|
if (!parse_port(optarg, &args->port)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'r':
|
case 'r':
|
||||||
opts->record_filename = optarg;
|
args->record_filename = optarg;
|
||||||
break;
|
break;
|
||||||
case 's':
|
case 's':
|
||||||
opts->serial = optarg;
|
args->serial = optarg;
|
||||||
break;
|
break;
|
||||||
case 'S':
|
case 'S':
|
||||||
opts->turn_screen_off = true;
|
args->turn_screen_off = true;
|
||||||
break;
|
break;
|
||||||
case 't':
|
case 't':
|
||||||
opts->show_touches = true;
|
args->show_touches = true;
|
||||||
break;
|
break;
|
||||||
case 'T':
|
case 'T':
|
||||||
LOGW("Deprecated option -T. Use --always-on-top instead.");
|
args->always_on_top = true;
|
||||||
// fall through
|
|
||||||
case OPT_ALWAYS_ON_TOP:
|
|
||||||
opts->always_on_top = true;
|
|
||||||
break;
|
break;
|
||||||
case 'v':
|
case 'v':
|
||||||
args->version = true;
|
args->version = true;
|
||||||
break;
|
break;
|
||||||
case OPT_RENDER_EXPIRED_FRAMES:
|
case OPT_RENDER_EXPIRED_FRAMES:
|
||||||
opts->render_expired_frames = true;
|
args->render_expired_frames = true;
|
||||||
break;
|
break;
|
||||||
case OPT_WINDOW_TITLE:
|
case OPT_WINDOW_TITLE:
|
||||||
opts->window_title = optarg;
|
args->window_title = optarg;
|
||||||
break;
|
break;
|
||||||
case OPT_WINDOW_X:
|
case OPT_WINDOW_X:
|
||||||
if (!parse_window_position(optarg, &opts->window_x)) {
|
if (!parse_window_position(optarg, &args->window_x)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case OPT_WINDOW_Y:
|
case OPT_WINDOW_Y:
|
||||||
if (!parse_window_position(optarg, &opts->window_y)) {
|
if (!parse_window_position(optarg, &args->window_y)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case OPT_WINDOW_WIDTH:
|
case OPT_WINDOW_WIDTH:
|
||||||
if (!parse_window_dimension(optarg, &opts->window_width)) {
|
if (!parse_window_dimension(optarg, &args->window_width)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case OPT_WINDOW_HEIGHT:
|
case OPT_WINDOW_HEIGHT:
|
||||||
if (!parse_window_dimension(optarg, &opts->window_height)) {
|
if (!parse_window_dimension(optarg, &args->window_height)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case OPT_WINDOW_BORDERLESS:
|
case OPT_WINDOW_BORDERLESS:
|
||||||
opts->window_borderless = true;
|
args->window_borderless = true;
|
||||||
break;
|
break;
|
||||||
case OPT_PUSH_TARGET:
|
case OPT_PUSH_TARGET:
|
||||||
opts->push_target = optarg;
|
args->push_target = optarg;
|
||||||
break;
|
|
||||||
case OPT_PREFER_TEXT:
|
|
||||||
opts->prefer_text = true;
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// getopt prints the error message on stderr
|
// getopt prints the error message on stderr
|
||||||
@@ -549,12 +510,12 @@ parse_args(struct args *args, int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts->display && !opts->record_filename) {
|
if (args->no_display && !args->record_filename) {
|
||||||
LOGE("-N/--no-display requires screen recording (-r/--record)");
|
LOGE("-N/--no-display requires screen recording (-r/--record)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts->display && opts->fullscreen) {
|
if (args->no_display && args->fullscreen) {
|
||||||
LOGE("-f/--fullscreen-window is incompatible with -N/--no-display");
|
LOGE("-f/--fullscreen-window is incompatible with -N/--no-display");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -565,21 +526,21 @@ parse_args(struct args *args, int argc, char *argv[]) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts->record_format && !opts->record_filename) {
|
if (args->record_format && !args->record_filename) {
|
||||||
LOGE("Record format specified without recording");
|
LOGE("Record format specified without recording");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts->record_filename && !opts->record_format) {
|
if (args->record_filename && !args->record_format) {
|
||||||
opts->record_format = guess_record_format(opts->record_filename);
|
args->record_format = guess_record_format(args->record_filename);
|
||||||
if (!opts->record_format) {
|
if (!args->record_format) {
|
||||||
LOGE("No format specified for \"%s\" (try with -F mkv)",
|
LOGE("No format specified for \"%s\" (try with -F mkv)",
|
||||||
opts->record_filename);
|
args->record_filename);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts->control && opts->turn_screen_off) {
|
if (args->no_control && args->turn_screen_off) {
|
||||||
LOGE("Could not request to turn screen off if control is disabled");
|
LOGE("Could not request to turn screen off if control is disabled");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -596,11 +557,29 @@ main(int argc, char *argv[]) {
|
|||||||
setbuf(stderr, NULL);
|
setbuf(stderr, NULL);
|
||||||
#endif
|
#endif
|
||||||
struct args args = {
|
struct args args = {
|
||||||
.opts = SCRCPY_OPTIONS_DEFAULT,
|
.serial = NULL,
|
||||||
|
.crop = NULL,
|
||||||
|
.record_filename = NULL,
|
||||||
|
.window_title = NULL,
|
||||||
|
.push_target = NULL,
|
||||||
|
.record_format = 0,
|
||||||
.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,
|
||||||
|
.window_x = -1,
|
||||||
|
.window_y = -1,
|
||||||
|
.window_width = 0,
|
||||||
|
.window_height = 0,
|
||||||
|
.always_on_top = false,
|
||||||
|
.no_control = false,
|
||||||
|
.no_display = false,
|
||||||
|
.turn_screen_off = false,
|
||||||
|
.render_expired_frames = false,
|
||||||
|
.window_borderless = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!parse_args(&args, argc, argv)) {
|
if (!parse_args(&args, argc, argv)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -629,7 +608,30 @@ main(int argc, char *argv[]) {
|
|||||||
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
|
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
int res = scrcpy(&args.opts) ? 0 : 1;
|
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,
|
||||||
|
.window_x = args.window_x,
|
||||||
|
.window_y = args.window_y,
|
||||||
|
.window_width = args.window_width,
|
||||||
|
.window_height = args.window_height,
|
||||||
|
.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,
|
||||||
|
.window_borderless = args.window_borderless,
|
||||||
|
};
|
||||||
|
int res = scrcpy(&options) ? 0 : 1;
|
||||||
|
|
||||||
avformat_network_deinit(); // ignore failure
|
avformat_network_deinit(); // ignore failure
|
||||||
|
|
||||||
|
|||||||
@@ -174,16 +174,11 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
recorder_close(struct recorder *recorder) {
|
recorder_close(struct recorder *recorder) {
|
||||||
if (recorder->header_written) {
|
|
||||||
int ret = av_write_trailer(recorder->ctx);
|
int ret = av_write_trailer(recorder->ctx);
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
LOGE("Failed to write trailer to %s", recorder->filename);
|
LOGE("Failed to write trailer to %s", recorder->filename);
|
||||||
recorder->failed = true;
|
recorder->failed = true;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// the recorded file is empty
|
|
||||||
recorder->failed = true;
|
|
||||||
}
|
|
||||||
avio_close(recorder->ctx->pb);
|
avio_close(recorder->ctx->pb);
|
||||||
avformat_free_context(recorder->ctx);
|
avformat_free_context(recorder->ctx);
|
||||||
|
|
||||||
@@ -301,12 +296,8 @@ run_recorder(void *data) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// config packets have no PTS, we must ignore them
|
|
||||||
if (rec->packet.pts != AV_NOPTS_VALUE
|
|
||||||
&& previous->packet.pts != AV_NOPTS_VALUE) {
|
|
||||||
// we now know the duration of the previous packet
|
// we now know the duration of the previous packet
|
||||||
previous->packet.duration = rec->packet.pts - previous->packet.pts;
|
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);
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
#include "queue.h"
|
#include "queue.h"
|
||||||
|
|
||||||
enum recorder_format {
|
enum recorder_format {
|
||||||
RECORDER_FORMAT_AUTO,
|
RECORDER_FORMAT_MP4 = 1,
|
||||||
RECORDER_FORMAT_MP4,
|
|
||||||
RECORDER_FORMAT_MKV,
|
RECORDER_FORMAT_MKV,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ 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
|
||||||
@@ -144,7 +143,12 @@ handle_event(SDL_Event *event, bool control) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case SDL_WINDOWEVENT:
|
case SDL_WINDOWEVENT:
|
||||||
screen_handle_window_event(&screen, &event->window);
|
switch (event->window.event) {
|
||||||
|
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) {
|
||||||
@@ -213,7 +217,6 @@ 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;
|
||||||
@@ -280,7 +283,6 @@ 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)) {
|
||||||
@@ -415,8 +417,6 @@ 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,10 +3,9 @@
|
|||||||
|
|
||||||
#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;
|
||||||
@@ -18,7 +17,6 @@ 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_x;
|
||||||
int16_t window_y;
|
int16_t window_y;
|
||||||
uint16_t window_width;
|
uint16_t window_width;
|
||||||
@@ -30,36 +28,9 @@ 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;
|
bool window_borderless;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define SCRCPY_OPTIONS_DEFAULT { \
|
|
||||||
.serial = NULL, \
|
|
||||||
.crop = NULL, \
|
|
||||||
.record_filename = NULL, \
|
|
||||||
.window_title = NULL, \
|
|
||||||
.push_target = NULL, \
|
|
||||||
.record_format = RECORDER_FORMAT_AUTO, \
|
|
||||||
.port = DEFAULT_LOCAL_PORT, \
|
|
||||||
.max_size = DEFAULT_LOCAL_PORT, \
|
|
||||||
.bit_rate = DEFAULT_BIT_RATE, \
|
|
||||||
.max_fps = 0, \
|
|
||||||
.window_x = -1, \
|
|
||||||
.window_y = -1, \
|
|
||||||
.window_width = 0, \
|
|
||||||
.window_height = 0, \
|
|
||||||
.show_touches = false, \
|
|
||||||
.fullscreen = false, \
|
|
||||||
.always_on_top = false, \
|
|
||||||
.control = true, \
|
|
||||||
.display = true, \
|
|
||||||
.turn_screen_off = false, \
|
|
||||||
.render_expired_frames = false, \
|
|
||||||
.prefer_text = false, \
|
|
||||||
.window_borderless = false, \
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
scrcpy(const struct scrcpy_options *options);
|
scrcpy(const struct scrcpy_options *options);
|
||||||
|
|
||||||
|
|||||||
@@ -30,28 +30,23 @@ 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 || screen->maximized) {
|
if (screen->fullscreen) {
|
||||||
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) {
|
||||||
|
// SDL_SetWindowSize will be called when fullscreen will be disabled
|
||||||
screen->windowed_window_size = new_size;
|
screen->windowed_window_size = new_size;
|
||||||
apply_windowed_size(screen);
|
} 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)
|
||||||
@@ -227,8 +222,6 @@ screen_init_rendering(struct screen *screen, const char *window_title,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
screen->windowed_window_size = window_size;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +315,10 @@ 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());
|
||||||
@@ -329,7 +326,11 @@ screen_switch_fullscreen(struct screen *screen) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
screen->fullscreen = !screen->fullscreen;
|
screen->fullscreen = !screen->fullscreen;
|
||||||
apply_windowed_size(screen);
|
if (!screen->fullscreen) {
|
||||||
|
// 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);
|
||||||
@@ -337,75 +338,20 @@ 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) {
|
||||||
return;
|
struct size optimal_size = get_optimal_window_size(screen,
|
||||||
}
|
screen->frame_size);
|
||||||
|
SDL_SetWindowSize(screen->window, optimal_size.width,
|
||||||
if (screen->maximized) {
|
optimal_size.height);
|
||||||
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");
|
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) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (screen->maximized) {
|
|
||||||
SDL_RestoreWindow(screen->window);
|
|
||||||
screen->maximized = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_SetWindowSize(screen->window, screen->frame_size.width,
|
SDL_SetWindowSize(screen->window, screen->frame_size.width,
|
||||||
screen->frame_size.height);
|
screen->frame_size.height);
|
||||||
LOGD("Resized to pixel-perfect");
|
LOGD("Resized to pixel-perfect");
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
screen_handle_window_event(struct screen *screen,
|
|
||||||
const SDL_WindowEvent *event) {
|
|
||||||
switch (event->event) {
|
|
||||||
case SDL_WINDOWEVENT_EXPOSED:
|
|
||||||
screen_render(screen);
|
|
||||||
break;
|
|
||||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
|
||||||
if (!screen->fullscreen && !screen->maximized) {
|
|
||||||
// Backup the previous size: if we receive the MAXIMIZED event,
|
|
||||||
// then the new size must be ignored (it's the maximized size).
|
|
||||||
// We could not rely on the window flags due to race conditions
|
|
||||||
// (they could be updated asynchronously, at least on X11).
|
|
||||||
screen->windowed_window_size_backup =
|
|
||||||
screen->windowed_window_size;
|
|
||||||
|
|
||||||
// Save the windowed size, so that it is available once the
|
|
||||||
// window is maximized or fullscreen is enabled.
|
|
||||||
screen->windowed_window_size = get_window_size(screen->window);
|
|
||||||
}
|
|
||||||
screen_render(screen);
|
|
||||||
break;
|
|
||||||
case SDL_WINDOWEVENT_MAXIMIZED:
|
|
||||||
// The backup size must be non-nul.
|
|
||||||
SDL_assert(screen->windowed_window_size_backup.width);
|
|
||||||
SDL_assert(screen->windowed_window_size_backup.height);
|
|
||||||
// Revert the last size, it was updated while screen was maximized.
|
|
||||||
screen->windowed_window_size = screen->windowed_window_size_backup;
|
|
||||||
#ifdef DEBUG
|
|
||||||
// Reset the backup to invalid values to detect unexpected usage
|
|
||||||
screen->windowed_window_size_backup.width = 0;
|
|
||||||
screen->windowed_window_size_backup.height = 0;
|
|
||||||
#endif
|
|
||||||
screen->maximized = true;
|
|
||||||
break;
|
|
||||||
case SDL_WINDOWEVENT_RESTORED:
|
|
||||||
screen->maximized = false;
|
|
||||||
apply_windowed_size(screen);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,14 +15,10 @@ struct screen {
|
|||||||
SDL_Renderer *renderer;
|
SDL_Renderer *renderer;
|
||||||
SDL_Texture *texture;
|
SDL_Texture *texture;
|
||||||
struct size frame_size;
|
struct size frame_size;
|
||||||
// The window size the last time it was not maximized or fullscreen.
|
//used only in fullscreen mode to know the windowed window size
|
||||||
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,13 +34,8 @@ struct screen {
|
|||||||
.width = 0, \
|
.width = 0, \
|
||||||
.height = 0, \
|
.height = 0, \
|
||||||
}, \
|
}, \
|
||||||
.windowed_window_size_backup = { \
|
|
||||||
.width = 0, \
|
|
||||||
.height = 0, \
|
|
||||||
}, \
|
|
||||||
.has_frame = false, \
|
.has_frame = false, \
|
||||||
.fullscreen = false, \
|
.fullscreen = false, \
|
||||||
.maximized = false, \
|
|
||||||
.no_window = false, \
|
.no_window = false, \
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,8 +78,4 @@ 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
|
||||||
|
|||||||
@@ -118,41 +118,21 @@ 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=/data/local/tmp/" SERVER_FILENAME,
|
||||||
"app_process",
|
"app_process",
|
||||||
#ifdef SERVER_DEBUGGER
|
|
||||||
# define SERVER_DEBUGGER_PORT "5005"
|
|
||||||
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
|
|
||||||
SERVER_DEBUGGER_PORT,
|
|
||||||
#endif
|
|
||||||
"/", // unused
|
"/", // 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)
|
||||||
params->control ? "true" : "false",
|
params->control ? "true" : "false",
|
||||||
};
|
};
|
||||||
#ifdef SERVER_DEBUGGER
|
|
||||||
LOGI("Server debugger waiting for a client on device port "
|
|
||||||
SERVER_DEBUGGER_PORT "...");
|
|
||||||
// From the computer, run
|
|
||||||
// adb forward tcp:5005 tcp:5005
|
|
||||||
// Then, from Android Studio: Run > Debug > Edit configurations...
|
|
||||||
// On the left, click on '+', "Remote", with:
|
|
||||||
// Host: localhost
|
|
||||||
// Port: 5005
|
|
||||||
// Then click on "Debug"
|
|
||||||
#endif
|
|
||||||
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
|
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +261,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 cleaned up on destroy
|
// the video_socket will be clean up on destroy
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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_imports.html -->
|
<!-- See http://checkstyle.sf.net/config_import.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_modifier.html -->
|
<!-- See http://checkstyle.sf.net/config_modifiers.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.2.1-win32-shared'
|
prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win32-shared'
|
||||||
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win32-dev'
|
prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-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.2.1-win64-shared'
|
prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win64-shared'
|
||||||
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win64-dev'
|
prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win64-dev'
|
||||||
prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32'
|
prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
project('scrcpy', 'c',
|
project('scrcpy', 'c',
|
||||||
version: '1.11',
|
version: '1.10',
|
||||||
meson_version: '>= 0.37',
|
meson_version: '>= 0.37',
|
||||||
default_options: 'c_std=c11')
|
default_options: 'c_std=c11')
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,3 @@ option('windows_noconsole', type: 'boolean', value: false, description: 'Disable
|
|||||||
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
|
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
|
||||||
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
|
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
|
||||||
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')
|
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')
|
||||||
option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached')
|
|
||||||
|
|||||||
@@ -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.2.1-win32-shared.zip \
|
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1.4-win32-shared.zip \
|
||||||
9208255f409410d95147151d7e829b5699bf8d91bfe1e81c3f470f47c2fa66d2 \
|
596608277f6b937c3dea7c46e854665d75b3de56790bae07f655ca331440f003 \
|
||||||
ffmpeg-4.2.1-win32-shared
|
ffmpeg-4.1.4-win32-shared
|
||||||
|
|
||||||
prepare-ffmpeg-dev-win32:
|
prepare-ffmpeg-dev-win32:
|
||||||
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.1-win32-dev.zip \
|
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.4-win32-dev.zip \
|
||||||
c3469e6c5f031cbcc8cba88dee92d6548c5c6b6ff14f4097f18f72a92d0d70c4 \
|
a80c86e263cfad26e202edfa5e6e939a2c88843ae26f031d3e0d981a39fd03fb \
|
||||||
ffmpeg-4.2.1-win32-dev
|
ffmpeg-4.1.4-win32-dev
|
||||||
|
|
||||||
prepare-ffmpeg-shared-win64:
|
prepare-ffmpeg-shared-win64:
|
||||||
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.1-win64-shared.zip \
|
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.4-win64-shared.zip \
|
||||||
55063d3cf750a75485c7bf196031773d81a1b25d0980c7db48ecfc7701a42331 \
|
a90889871de2cab8a79b392591313a188189a353f69dde1db98aebe20b280989 \
|
||||||
ffmpeg-4.2.1-win64-shared
|
ffmpeg-4.1.4-win64-shared
|
||||||
|
|
||||||
prepare-ffmpeg-dev-win64:
|
prepare-ffmpeg-dev-win64:
|
||||||
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.1-win64-dev.zip \
|
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.4-win64-dev.zip \
|
||||||
5af393be5f25c0a71aa29efce768e477c35347f7f8e0d9696767d5b9d405b74e \
|
6c9d53f9e94ce1821e975ec668e5b9d6e9deb4a45d0d7e30264685d3dfbbb068 \
|
||||||
ffmpeg-4.2.1-win64-dev
|
ffmpeg-4.1.4-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.5-windows.zip \
|
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.2-windows.zip \
|
||||||
2df06160056ec9a84c7334af2a1e42740befbb1a2e34370e7af544a2cc78152c \
|
d78f02e5e2c9c4c1d046dcd4e6fbdf586e5f57ef66eb0da5c2b49d745d85d5ee \
|
||||||
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 12
|
versionCode 11
|
||||||
versionName "1.11"
|
versionName "1.10"
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -11,9 +11,6 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRCPY_DEBUG=false
|
|
||||||
SCRCPY_VERSION_NAME=1.11
|
|
||||||
|
|
||||||
PLATFORM=${ANDROID_PLATFORM:-29}
|
PLATFORM=${ANDROID_PLATFORM:-29}
|
||||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}
|
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}
|
||||||
|
|
||||||
@@ -33,8 +30,7 @@ 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 = $SCRCPY_DEBUG;
|
public static final boolean DEBUG = false;
|
||||||
public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME";
|
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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
|
||||||
@@ -27,14 +26,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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;
|
||||||
|
|
||||||
@@ -17,29 +16,32 @@ 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 maxFps;
|
private int frameRate;
|
||||||
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 maxFps, int iFrameInterval) {
|
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int frameRate, int iFrameInterval) {
|
||||||
this.sendFrameMeta = sendFrameMeta;
|
this.sendFrameMeta = sendFrameMeta;
|
||||||
this.bitRate = bitRate;
|
this.bitRate = bitRate;
|
||||||
this.maxFps = maxFps;
|
this.frameRate = frameRate;
|
||||||
this.iFrameInterval = iFrameInterval;
|
this.iFrameInterval = iFrameInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) {
|
public ScreenEncoder(boolean sendFrameMeta, int bitRate) {
|
||||||
this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL);
|
this(sendFrameMeta, bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -52,10 +54,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||||
Workarounds.prepareMainLooper();
|
MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval);
|
||||||
Workarounds.fillAppInfo();
|
|
||||||
|
|
||||||
MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval);
|
|
||||||
device.setRotationListener(this);
|
device.setRotationListener(this);
|
||||||
boolean alive;
|
boolean alive;
|
||||||
try {
|
try {
|
||||||
@@ -138,24 +137,15 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
return MediaCodec.createEncoderByType("video/avc");
|
return MediaCodec.createEncoderByType("video/avc");
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
private static MediaFormat createFormat(int bitRate, int frameRate, int iFrameInterval) throws IOException {
|
||||||
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);
|
||||||
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
|
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
|
||||||
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, REPEAT_FRAME_DELAY_US); // µs
|
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate); // µ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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,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(), options.getMaxFps());
|
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate());
|
||||||
|
|
||||||
if (options.getControl()) {
|
if (options.getControl()) {
|
||||||
Controller controller = new Controller(device, connection);
|
Controller controller = new Controller(device, connection);
|
||||||
@@ -67,42 +67,29 @@ 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 < 1) {
|
if (args.length != 6) {
|
||||||
throw new IllegalArgumentException("Missing client version");
|
throw new IllegalArgumentException("Expecting 6 parameters");
|
||||||
}
|
|
||||||
|
|
||||||
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[1]) & ~7; // multiple of 8
|
int maxSize = Integer.parseInt(args[0]) & ~7; // multiple of 8
|
||||||
options.setMaxSize(maxSize);
|
options.setMaxSize(maxSize);
|
||||||
|
|
||||||
int bitRate = Integer.parseInt(args[2]);
|
int bitRate = Integer.parseInt(args[1]);
|
||||||
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[4]);
|
boolean tunnelForward = Boolean.parseBoolean(args[2]);
|
||||||
options.setTunnelForward(tunnelForward);
|
options.setTunnelForward(tunnelForward);
|
||||||
|
|
||||||
Rect crop = parseCrop(args[5]);
|
Rect crop = parseCrop(args[3]);
|
||||||
options.setCrop(crop);
|
options.setCrop(crop);
|
||||||
|
|
||||||
boolean sendFrameMeta = Boolean.parseBoolean(args[6]);
|
boolean sendFrameMeta = Boolean.parseBoolean(args[4]);
|
||||||
options.setSendFrameMeta(sendFrameMeta);
|
options.setSendFrameMeta(sendFrameMeta);
|
||||||
|
|
||||||
boolean control = Boolean.parseBoolean(args[7]);
|
boolean control = Boolean.parseBoolean(args[5]);
|
||||||
options.setControl(control);
|
options.setControl(control);
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
package com.genymobile.scrcpy;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
|
||||||
import android.os.Looper;
|
|
||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
|
|
||||||
public final class Workarounds {
|
|
||||||
private Workarounds() {
|
|
||||||
// not instantiable
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void prepareMainLooper() {
|
|
||||||
// Some devices internally create a Handler when creating an input Surface, causing an exception:
|
|
||||||
// "Can't create handler inside thread that has not called Looper.prepare()"
|
|
||||||
// <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 appInfo = appBindDataClass.getDeclaredField("appInfo");
|
|
||||||
appInfo.setAccessible(true);
|
|
||||||
appInfo.set(appBindData, applicationInfo);
|
|
||||||
|
|
||||||
// activityThread.mBoundApplication = appBindData;
|
|
||||||
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
|
|
||||||
mBoundApplicationField.setAccessible(true);
|
|
||||||
mBoundApplicationField.set(activityThread, appBindData);
|
|
||||||
} catch (Throwable throwable) {
|
|
||||||
// this is a workaround, so failing is not an error
|
|
||||||
Ln.w("Could not fill app info: " + throwable.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user