Compare commits
39 Commits
v1.9
...
cmd_macos.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f049b6bc1b | ||
|
|
6abb4902c6 | ||
|
|
d4ed8b6f26 | ||
|
|
35d9185f6c | ||
|
|
63af7fbafe | ||
|
|
a90ccbdf3b | ||
|
|
ca970e8aa6 | ||
|
|
6b3d9e3eab | ||
|
|
02692ffa42 | ||
|
|
3b69463e61 | ||
|
|
9dea6d2384 | ||
|
|
3c55d0c69b | ||
|
|
4961256123 | ||
|
|
e4ac943d86 | ||
|
|
4b997239f3 | ||
|
|
8e65c10720 | ||
|
|
056e47e752 | ||
|
|
439b009a79 | ||
|
|
91ecb4f218 | ||
|
|
bfb3f0842f | ||
|
|
87d7a157a9 | ||
|
|
b91ecf5225 | ||
|
|
1807de4955 | ||
|
|
0a233fd27f | ||
|
|
4940746bcb | ||
|
|
fe758e6e15 | ||
|
|
b29a568f08 | ||
|
|
b769083a5b | ||
|
|
8ca36406b9 | ||
|
|
53310a925a | ||
|
|
0cb902d58b | ||
|
|
bcd0a876f7 | ||
|
|
de2016a48e | ||
|
|
19ca6a0d66 | ||
|
|
e2996e85c0 | ||
|
|
c2df0228a3 | ||
|
|
a13524e7f9 | ||
|
|
f3f3433163 | ||
|
|
232aaa386e |
14
BUILD.md
14
BUILD.md
@@ -12,7 +12,7 @@ case, use the [prebuilt server] (so you will not need Java or the Android SDK).
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
You need [adb]. It is available in the [Android SDK platform
|
You need [adb]. It is available in the [Android SDK platform
|
||||||
tools][platform-tools], or packaged in your distribution (`android-adb-tools`).
|
tools][platform-tools], or packaged in your distribution (`adb`).
|
||||||
|
|
||||||
On Windows, download the [platform-tools][platform-tools-windows] and extract
|
On Windows, download the [platform-tools][platform-tools-windows] and extract
|
||||||
the following files to a directory accessible from your `PATH`:
|
the following files to a directory accessible from your `PATH`:
|
||||||
@@ -40,10 +40,10 @@ Install the required packages from your package manager.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# runtime dependencies
|
# runtime dependencies
|
||||||
sudo apt install ffmpeg libsdl2-2.0.0
|
sudo apt install ffmpeg libsdl2-2.0-0
|
||||||
|
|
||||||
# client build dependencies
|
# client build dependencies
|
||||||
sudo apt install make gcc pkg-config meson ninja-build \
|
sudo apt install make gcc git pkg-config meson ninja-build \
|
||||||
libavcodec-dev libavformat-dev libavutil-dev \
|
libavcodec-dev libavformat-dev libavutil-dev \
|
||||||
libsdl2-dev
|
libsdl2-dev
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-rele
|
|||||||
sudo dnf install SDL2-devel ffms2-devel meson gcc make
|
sudo dnf install SDL2-devel ffms2-devel meson gcc make
|
||||||
|
|
||||||
# server build dependencies
|
# server build dependencies
|
||||||
sudo dnf install java
|
sudo dnf install java-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -234,10 +234,10 @@ You can then [run](README.md#run) _scrcpy_.
|
|||||||
|
|
||||||
## Prebuilt server
|
## Prebuilt server
|
||||||
|
|
||||||
- [`scrcpy-server-v1.8.jar`][direct-scrcpy-server]
|
- [`scrcpy-server-v1.9.jar`][direct-scrcpy-server]
|
||||||
_(SHA-256: 839055ef905903bf98ead1b9b8a127fe402b39ad657a81f9a914b2dbcb2ce5c0)_
|
_(SHA-256: ad7e539f100e48259b646f26982bc63e0a60a81ac87ae135e242855bef69bd1a)_
|
||||||
|
|
||||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-server-v1.8.jar
|
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-server-v1.9.jar
|
||||||
|
|
||||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||||
configuration:
|
configuration:
|
||||||
|
|||||||
1
LICENSE
1
LICENSE
@@ -188,6 +188,7 @@
|
|||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright (C) 2018 Genymobile
|
Copyright (C) 2018 Genymobile
|
||||||
|
Copyright (C) 2018-2019 Romain Vimont
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ clean:
|
|||||||
build-server:
|
build-server:
|
||||||
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
|
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
|
||||||
meson "$(SERVER_BUILD_DIR)" \
|
meson "$(SERVER_BUILD_DIR)" \
|
||||||
--buildtype release -Dbuild_app=false )
|
--buildtype release -Dcompile_app=false )
|
||||||
ninja -C "$(SERVER_BUILD_DIR)"
|
ninja -C "$(SERVER_BUILD_DIR)"
|
||||||
|
|
||||||
prepare-deps-win32:
|
prepare-deps-win32:
|
||||||
@@ -56,7 +56,7 @@ build-win32: prepare-deps-win32
|
|||||||
--cross-file cross_win32.txt \
|
--cross-file cross_win32.txt \
|
||||||
--buildtype release --strip -Db_lto=true \
|
--buildtype release --strip -Db_lto=true \
|
||||||
-Dcrossbuild_windows=true \
|
-Dcrossbuild_windows=true \
|
||||||
-Dbuild_server=false \
|
-Dcompile_server=false \
|
||||||
-Dportable=true )
|
-Dportable=true )
|
||||||
ninja -C "$(WIN32_BUILD_DIR)"
|
ninja -C "$(WIN32_BUILD_DIR)"
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ build-win32-noconsole: prepare-deps-win32
|
|||||||
--cross-file cross_win32.txt \
|
--cross-file cross_win32.txt \
|
||||||
--buildtype release --strip -Db_lto=true \
|
--buildtype release --strip -Db_lto=true \
|
||||||
-Dcrossbuild_windows=true \
|
-Dcrossbuild_windows=true \
|
||||||
-Dbuild_server=false \
|
-Dcompile_server=false \
|
||||||
-Dwindows_noconsole=true \
|
-Dwindows_noconsole=true \
|
||||||
-Dportable=true )
|
-Dportable=true )
|
||||||
ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)"
|
ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)"
|
||||||
@@ -80,7 +80,7 @@ build-win64: prepare-deps-win64
|
|||||||
--cross-file cross_win64.txt \
|
--cross-file cross_win64.txt \
|
||||||
--buildtype release --strip -Db_lto=true \
|
--buildtype release --strip -Db_lto=true \
|
||||||
-Dcrossbuild_windows=true \
|
-Dcrossbuild_windows=true \
|
||||||
-Dbuild_server=false \
|
-Dcompile_server=false \
|
||||||
-Dportable=true )
|
-Dportable=true )
|
||||||
ninja -C "$(WIN64_BUILD_DIR)"
|
ninja -C "$(WIN64_BUILD_DIR)"
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ build-win64-noconsole: prepare-deps-win64
|
|||||||
--cross-file cross_win64.txt \
|
--cross-file cross_win64.txt \
|
||||||
--buildtype release --strip -Db_lto=true \
|
--buildtype release --strip -Db_lto=true \
|
||||||
-Dcrossbuild_windows=true \
|
-Dcrossbuild_windows=true \
|
||||||
-Dbuild_server=false \
|
-Dcompile_server=false \
|
||||||
-Dwindows_noconsole=true \
|
-Dwindows_noconsole=true \
|
||||||
-Dportable=true )
|
-Dportable=true )
|
||||||
ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)"
|
ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)"
|
||||||
|
|||||||
101
README.md
101
README.md
@@ -1,15 +1,15 @@
|
|||||||
# scrcpy (v1.8)
|
# scrcpy (v1.9)
|
||||||
|
|
||||||
This application provides display and control of Android devices connected on
|
This application provides display and control of Android devices connected on
|
||||||
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
|
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
|
||||||
It works on _GNU/Linux_, _Windows_ and _MacOS_.
|
It works on _GNU/Linux_, _Windows_ and _macOS_.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
The Android part requires at least API 21 (Android 5.0).
|
The Android device requires at least API 21 (Android 5.0).
|
||||||
|
|
||||||
Make sure you [enabled adb debugging][enable-adb] on your device(s).
|
Make sure you [enabled adb debugging][enable-adb] on your device(s).
|
||||||
|
|
||||||
@@ -29,6 +29,12 @@ control it using keyboard and mouse.
|
|||||||
On Linux, you typically need to [build the app manually][BUILD]. Don't worry,
|
On Linux, you typically need to [build the app manually][BUILD]. Don't worry,
|
||||||
it's not that hard.
|
it's not that hard.
|
||||||
|
|
||||||
|
A [Snap] package is available: [`scrcpy`][snap-link].
|
||||||
|
|
||||||
|
[snap-link]: https://snapstats.org/snaps/scrcpy
|
||||||
|
|
||||||
|
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
|
||||||
|
|
||||||
For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link].
|
For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link].
|
||||||
|
|
||||||
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||||
@@ -45,18 +51,18 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
|
|||||||
For Windows, for simplicity, prebuilt archives with all the dependencies
|
For Windows, for simplicity, prebuilt archives with all the dependencies
|
||||||
(including `adb`) are available:
|
(including `adb`) are available:
|
||||||
|
|
||||||
- [`scrcpy-win32-v1.8.zip`][direct-win32]
|
- [`scrcpy-win32-v1.9.zip`][direct-win32]
|
||||||
_(SHA-256: c0c29ed1c66deaa73bdadacd09e598aafb3a117929cf7a314cce1cc45e34de53)_
|
_(SHA-256: 3234f7fbcc26b9e399f50b5ca9ed085708954c87fda1b0dd32719d6e7dd861ef)_
|
||||||
- [`scrcpy-win64-v1.8.zip`][direct-win64]
|
- [`scrcpy-win64-v1.9.zip`][direct-win64]
|
||||||
_(SHA-256: 9cc980d07bd8f036ae4e91d0bc6fc3281d7fa8f9752d4913b643c0fb72a19fb7)_
|
_(SHA-256: 0088eca1811ea7c7ac350d636c8465b266e6c830bb268770ff88fddbb493077e)_
|
||||||
|
|
||||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win32-v1.8.zip
|
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win32-v1.9.zip
|
||||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win64-v1.8.zip
|
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win64-v1.9.zip
|
||||||
|
|
||||||
You can also [build the app manually][BUILD].
|
You can also [build the app manually][BUILD].
|
||||||
|
|
||||||
|
|
||||||
### Mac OS
|
### macOS
|
||||||
|
|
||||||
The application is available in [Homebrew]. Just install it:
|
The application is available in [Homebrew]. Just install it:
|
||||||
|
|
||||||
@@ -95,22 +101,22 @@ scrcpy --help
|
|||||||
### 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 performances.
|
increase performance.
|
||||||
|
|
||||||
To limit both width and height to some value (e.g. 1024):
|
To limit both the width and height to some value (e.g. 1024):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --max-size 1024
|
scrcpy --max-size 1024
|
||||||
scrcpy -m 1024 # short version
|
scrcpy -m 1024 # short version
|
||||||
```
|
```
|
||||||
|
|
||||||
The other dimension is computed to that the device aspect-ratio is preserved.
|
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 8Mbps. To change the video bitrate (e.g. to 2Mbps):
|
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --bit-rate 2M
|
scrcpy --bit-rate 2M
|
||||||
@@ -122,7 +128,7 @@ scrcpy -b 2M # short version
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
This is useful for example to mirror only 1 eye of the Oculus Go:
|
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)
|
||||||
@@ -245,6 +251,11 @@ _scrcpy_ 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.
|
||||||
|
|
||||||
|
The target directory can be changed on start:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --push-target /sdcard/foo/bar/
|
||||||
|
```
|
||||||
|
|
||||||
### Read-only
|
### Read-only
|
||||||
|
|
||||||
@@ -283,42 +294,47 @@ latency), use:
|
|||||||
scrcpy --render-expired-frames
|
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
|
### Forward audio
|
||||||
|
|
||||||
Audio is not forwarded by _scrcpy_.
|
Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only).
|
||||||
|
|
||||||
There is a limited solution using [AOA], implemented in the [`audio`] branch. If
|
Also see [issue #14].
|
||||||
you are interested, see [issue 14].
|
|
||||||
|
|
||||||
|
[USBaudio]: https://github.com/rom1v/usbaudio
|
||||||
[AOA]: https://source.android.com/devices/accessories/aoa2
|
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||||
[`audio`]: https://github.com/Genymobile/scrcpy/commits/audio
|
|
||||||
[issue 14]: https://github.com/Genymobile/scrcpy/issues/14
|
|
||||||
|
|
||||||
|
|
||||||
## Shortcuts
|
## Shortcuts
|
||||||
|
|
||||||
| Action | Shortcut |
|
| Action | Shortcut | Shortcut (macOS)
|
||||||
| -------------------------------------- |:---------------------------- |
|
| -------------------------------------- |:----------------------------- |:-----------------------------
|
||||||
| switch fullscreen mode | `Ctrl`+`f` |
|
| Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f`
|
||||||
| resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` |
|
| Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g`
|
||||||
| resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ |
|
| Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_
|
||||||
| click on `HOME` | `Ctrl`+`h` \| _Middle-click_ |
|
| Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_
|
||||||
| click on `BACK` | `Ctrl`+`b` \| _Right-click²_ |
|
| Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_
|
||||||
| click on `APP_SWITCH` | `Ctrl`+`s` |
|
| Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s`
|
||||||
| click on `MENU` | `Ctrl`+`m` |
|
| Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m`
|
||||||
| click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ (`Cmd`+`↑` on MacOS) |
|
| Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_
|
||||||
| click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on MacOS) |
|
| Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_
|
||||||
| click on `POWER` | `Ctrl`+`p` |
|
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
|
||||||
| power on | _Right-click²_ |
|
| Power on | _Right-click²_ | _Right-click²_
|
||||||
| turn device screen off (keep mirroring)| `Ctrl`+`o` |
|
| Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o`
|
||||||
| expand notification panel | `Ctrl`+`n` |
|
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
|
||||||
| collapse notification panel | `Ctrl`+`Shift`+`n` |
|
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
|
||||||
| copy device clipboard to computer | `Ctrl`+`c` |
|
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`
|
||||||
| paste computer clipboard to device | `Ctrl`+`v` |
|
| Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v`
|
||||||
| copy computer clipboard to device | `Ctrl`+`Shift+`v` |
|
| Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v`
|
||||||
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
|
| Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i`
|
||||||
|
|
||||||
_¹Double-click on black borders to remove them._
|
_¹Double-click on black borders to remove them._
|
||||||
_²Right-click turns the screen on if it was off, presses BACK otherwise._
|
_²Right-click turns the screen on if it was off, presses BACK otherwise._
|
||||||
@@ -369,6 +385,7 @@ Read the [developers page].
|
|||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
Copyright (C) 2018 Genymobile
|
Copyright (C) 2018 Genymobile
|
||||||
|
Copyright (C) 2018-2019 Romain Vimont
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ run_controller(void *data) {
|
|||||||
bool ok = process_msg(controller, &msg);
|
bool ok = process_msg(controller, &msg);
|
||||||
control_msg_destroy(&msg);
|
control_msg_destroy(&msg);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
LOGD("Cannot write msg to socket");
|
LOGD("Could not write msg to socket");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
#include "net.h"
|
#include "net.h"
|
||||||
|
|
||||||
#define DEVICE_NAME_FIELD_LENGTH 64
|
#define DEVICE_NAME_FIELD_LENGTH 64
|
||||||
#define DEVICE_SDCARD_PATH "/sdcard/"
|
|
||||||
|
|
||||||
// name must be at least DEVICE_NAME_FIELD_LENGTH bytes
|
// name must be at least DEVICE_NAME_FIELD_LENGTH bytes
|
||||||
bool
|
bool
|
||||||
|
|||||||
@@ -5,17 +5,19 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "command.h"
|
#include "command.h"
|
||||||
#include "device.h"
|
|
||||||
#include "lock_util.h"
|
#include "lock_util.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
|
|
||||||
|
#define DEFAULT_PUSH_TARGET "/sdcard/"
|
||||||
|
|
||||||
static void
|
static void
|
||||||
file_handler_request_destroy(struct file_handler_request *req) {
|
file_handler_request_destroy(struct file_handler_request *req) {
|
||||||
SDL_free(req->file);
|
SDL_free(req->file);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
file_handler_init(struct file_handler *file_handler, const char *serial) {
|
file_handler_init(struct file_handler *file_handler, const char *serial,
|
||||||
|
const char *push_target) {
|
||||||
|
|
||||||
cbuf_init(&file_handler->queue);
|
cbuf_init(&file_handler->queue);
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ file_handler_init(struct file_handler *file_handler, const char *serial) {
|
|||||||
if (serial) {
|
if (serial) {
|
||||||
file_handler->serial = SDL_strdup(serial);
|
file_handler->serial = SDL_strdup(serial);
|
||||||
if (!file_handler->serial) {
|
if (!file_handler->serial) {
|
||||||
LOGW("Cannot strdup serial");
|
LOGW("Could not strdup serial");
|
||||||
SDL_DestroyCond(file_handler->event_cond);
|
SDL_DestroyCond(file_handler->event_cond);
|
||||||
SDL_DestroyMutex(file_handler->mutex);
|
SDL_DestroyMutex(file_handler->mutex);
|
||||||
return false;
|
return false;
|
||||||
@@ -46,6 +48,8 @@ file_handler_init(struct file_handler *file_handler, const char *serial) {
|
|||||||
file_handler->stopped = false;
|
file_handler->stopped = false;
|
||||||
file_handler->current_process = PROCESS_NONE;
|
file_handler->current_process = PROCESS_NONE;
|
||||||
|
|
||||||
|
file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,8 +71,8 @@ install_apk(const char *serial, const char *file) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static process_t
|
static process_t
|
||||||
push_file(const char *serial, const char *file) {
|
push_file(const char *serial, const char *file, const char *push_target) {
|
||||||
return adb_push(serial, file, DEVICE_SDCARD_PATH);
|
return adb_push(serial, file, push_target);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
@@ -124,7 +128,8 @@ run_file_handler(void *data) {
|
|||||||
process = install_apk(file_handler->serial, req.file);
|
process = install_apk(file_handler->serial, req.file);
|
||||||
} else {
|
} else {
|
||||||
LOGI("Pushing %s...", req.file);
|
LOGI("Pushing %s...", req.file);
|
||||||
process = push_file(file_handler->serial, req.file);
|
process = push_file(file_handler->serial, req.file,
|
||||||
|
file_handler->push_target);
|
||||||
}
|
}
|
||||||
file_handler->current_process = process;
|
file_handler->current_process = process;
|
||||||
mutex_unlock(file_handler->mutex);
|
mutex_unlock(file_handler->mutex);
|
||||||
@@ -137,9 +142,11 @@ run_file_handler(void *data) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (process_check_success(process, "adb push")) {
|
if (process_check_success(process, "adb push")) {
|
||||||
LOGI("%s successfully pushed to /sdcard/", req.file);
|
LOGI("%s successfully pushed to %s", req.file,
|
||||||
|
file_handler->push_target);
|
||||||
} else {
|
} else {
|
||||||
LOGE("Failed to push %s to /sdcard/", req.file);
|
LOGE("Failed to push %s to %s", req.file,
|
||||||
|
file_handler->push_target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +176,7 @@ file_handler_stop(struct file_handler *file_handler) {
|
|||||||
cond_signal(file_handler->event_cond);
|
cond_signal(file_handler->event_cond);
|
||||||
if (file_handler->current_process != PROCESS_NONE) {
|
if (file_handler->current_process != PROCESS_NONE) {
|
||||||
if (!cmd_terminate(file_handler->current_process)) {
|
if (!cmd_terminate(file_handler->current_process)) {
|
||||||
LOGW("Cannot terminate install process");
|
LOGW("Could not terminate install process");
|
||||||
}
|
}
|
||||||
cmd_simple_wait(file_handler->current_process, NULL);
|
cmd_simple_wait(file_handler->current_process, NULL);
|
||||||
file_handler->current_process = PROCESS_NONE;
|
file_handler->current_process = PROCESS_NONE;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ struct file_handler_request_queue CBUF(struct file_handler_request, 16);
|
|||||||
|
|
||||||
struct file_handler {
|
struct file_handler {
|
||||||
char *serial;
|
char *serial;
|
||||||
|
const char *push_target;
|
||||||
SDL_Thread *thread;
|
SDL_Thread *thread;
|
||||||
SDL_mutex *mutex;
|
SDL_mutex *mutex;
|
||||||
SDL_cond *event_cond;
|
SDL_cond *event_cond;
|
||||||
@@ -32,7 +33,8 @@ struct file_handler {
|
|||||||
};
|
};
|
||||||
|
|
||||||
bool
|
bool
|
||||||
file_handler_init(struct file_handler *file_handler, const char *serial);
|
file_handler_init(struct file_handler *file_handler, const char *serial,
|
||||||
|
const char *push_target);
|
||||||
|
|
||||||
void
|
void
|
||||||
file_handler_destroy(struct file_handler *file_handler);
|
file_handler_destroy(struct file_handler *file_handler);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ send_keycode(struct controller *controller, enum android_keycode keycode,
|
|||||||
if (actions & ACTION_DOWN) {
|
if (actions & ACTION_DOWN) {
|
||||||
msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN;
|
msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN;
|
||||||
if (!controller_push_msg(controller, &msg)) {
|
if (!controller_push_msg(controller, &msg)) {
|
||||||
LOGW("Cannot request 'inject %s (DOWN)'", name);
|
LOGW("Could not request 'inject %s (DOWN)'", name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ send_keycode(struct controller *controller, enum android_keycode keycode,
|
|||||||
if (actions & ACTION_UP) {
|
if (actions & ACTION_UP) {
|
||||||
msg.inject_keycode.action = AKEY_EVENT_ACTION_UP;
|
msg.inject_keycode.action = AKEY_EVENT_ACTION_UP;
|
||||||
if (!controller_push_msg(controller, &msg)) {
|
if (!controller_push_msg(controller, &msg)) {
|
||||||
LOGW("Cannot request 'inject %s (UP)'", name);
|
LOGW("Could not request 'inject %s (UP)'", name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ press_back_or_turn_screen_on(struct controller *controller) {
|
|||||||
msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
|
msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
|
||||||
|
|
||||||
if (!controller_push_msg(controller, &msg)) {
|
if (!controller_push_msg(controller, &msg)) {
|
||||||
LOGW("Cannot request 'turn screen on'");
|
LOGW("Could not request 'turn screen on'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ expand_notification_panel(struct controller *controller) {
|
|||||||
msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL;
|
msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL;
|
||||||
|
|
||||||
if (!controller_push_msg(controller, &msg)) {
|
if (!controller_push_msg(controller, &msg)) {
|
||||||
LOGW("Cannot request 'expand notification panel'");
|
LOGW("Could not request 'expand notification panel'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ collapse_notification_panel(struct controller *controller) {
|
|||||||
msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL;
|
msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL;
|
||||||
|
|
||||||
if (!controller_push_msg(controller, &msg)) {
|
if (!controller_push_msg(controller, &msg)) {
|
||||||
LOGW("Cannot request 'collapse notification panel'");
|
LOGW("Could not request 'collapse notification panel'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ request_device_clipboard(struct controller *controller) {
|
|||||||
msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD;
|
msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD;
|
||||||
|
|
||||||
if (!controller_push_msg(controller, &msg)) {
|
if (!controller_push_msg(controller, &msg)) {
|
||||||
LOGW("Cannot request device clipboard");
|
LOGW("Could not request device clipboard");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ static void
|
|||||||
set_device_clipboard(struct controller *controller) {
|
set_device_clipboard(struct controller *controller) {
|
||||||
char *text = SDL_GetClipboardText();
|
char *text = SDL_GetClipboardText();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
LOGW("Cannot get clipboard text: %s", SDL_GetError());
|
LOGW("Could not get clipboard text: %s", SDL_GetError());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!*text) {
|
if (!*text) {
|
||||||
@@ -155,7 +155,7 @@ set_device_clipboard(struct controller *controller) {
|
|||||||
|
|
||||||
if (!controller_push_msg(controller, &msg)) {
|
if (!controller_push_msg(controller, &msg)) {
|
||||||
SDL_free(text);
|
SDL_free(text);
|
||||||
LOGW("Cannot request 'set device clipboard'");
|
LOGW("Could not request 'set device clipboard'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ set_screen_power_mode(struct controller *controller,
|
|||||||
msg.set_screen_power_mode.mode = mode;
|
msg.set_screen_power_mode.mode = mode;
|
||||||
|
|
||||||
if (!controller_push_msg(controller, &msg)) {
|
if (!controller_push_msg(controller, &msg)) {
|
||||||
LOGW("Cannot request 'set screen power mode'");
|
LOGW("Could not request 'set screen power mode'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ static void
|
|||||||
clipboard_paste(struct controller *controller) {
|
clipboard_paste(struct controller *controller) {
|
||||||
char *text = SDL_GetClipboardText();
|
char *text = SDL_GetClipboardText();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
LOGW("Cannot get clipboard text: %s", SDL_GetError());
|
LOGW("Could not get clipboard text: %s", SDL_GetError());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!*text) {
|
if (!*text) {
|
||||||
@@ -205,7 +205,7 @@ clipboard_paste(struct controller *controller) {
|
|||||||
msg.inject_text.text = text;
|
msg.inject_text.text = text;
|
||||||
if (!controller_push_msg(controller, &msg)) {
|
if (!controller_push_msg(controller, &msg)) {
|
||||||
SDL_free(text);
|
SDL_free(text);
|
||||||
LOGW("Cannot request 'paste clipboard'");
|
LOGW("Could not request 'paste clipboard'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,12 +222,12 @@ input_manager_process_text_input(struct input_manager *input_manager,
|
|||||||
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);
|
||||||
if (!msg.inject_text.text) {
|
if (!msg.inject_text.text) {
|
||||||
LOGW("Cannot strdup input text");
|
LOGW("Could not strdup input text");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!controller_push_msg(input_manager->controller, &msg)) {
|
if (!controller_push_msg(input_manager->controller, &msg)) {
|
||||||
SDL_free(msg.inject_text.text);
|
SDL_free(msg.inject_text.text);
|
||||||
LOGW("Cannot request 'inject text'");
|
LOGW("Could not request 'inject text'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,16 +242,27 @@ input_manager_process_key(struct input_manager *input_manager,
|
|||||||
bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
|
bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
|
||||||
bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI);
|
bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI);
|
||||||
|
|
||||||
|
// use Cmd on macOS, Ctrl on other platforms
|
||||||
|
#ifdef __APPLE__
|
||||||
|
bool cmd = !ctrl && meta;
|
||||||
|
#else
|
||||||
|
if (meta) {
|
||||||
|
// no shortcuts involve Meta on platforms other than macOS, and it must
|
||||||
|
// not be forwarded to the device
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool cmd = ctrl; // && !meta, already guaranteed
|
||||||
|
#endif
|
||||||
|
|
||||||
if (alt) {
|
if (alt) {
|
||||||
// no shortcut involves Alt or Meta, and they should not be forwarded
|
// no shortcuts involve Alt, and it must not be forwarded to the device
|
||||||
// to the device
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct controller *controller = input_manager->controller;
|
struct controller *controller = input_manager->controller;
|
||||||
|
|
||||||
// capture all Ctrl events
|
// capture all Ctrl events
|
||||||
if (ctrl | meta) {
|
if (ctrl || cmd) {
|
||||||
SDL_Keycode keycode = event->keysym.sym;
|
SDL_Keycode keycode = event->keysym.sym;
|
||||||
bool down = event->type == SDL_KEYDOWN;
|
bool down = event->type == SDL_KEYDOWN;
|
||||||
int action = down ? ACTION_DOWN : ACTION_UP;
|
int action = down ? ACTION_DOWN : ACTION_UP;
|
||||||
@@ -259,63 +270,59 @@ input_manager_process_key(struct input_manager *input_manager,
|
|||||||
bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
|
bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
|
||||||
switch (keycode) {
|
switch (keycode) {
|
||||||
case SDLK_h:
|
case SDLK_h:
|
||||||
|
// Ctrl+h on all platform, since Cmd+h is already captured by
|
||||||
|
// the system on macOS to hide the window
|
||||||
if (control && ctrl && !meta && !shift && !repeat) {
|
if (control && ctrl && !meta && !shift && !repeat) {
|
||||||
action_home(controller, action);
|
action_home(controller, action);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_b: // fall-through
|
case SDLK_b: // fall-through
|
||||||
case SDLK_BACKSPACE:
|
case SDLK_BACKSPACE:
|
||||||
if (control && ctrl && !meta && !shift && !repeat) {
|
if (control && cmd && !shift && !repeat) {
|
||||||
action_back(controller, action);
|
action_back(controller, action);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_s:
|
case SDLK_s:
|
||||||
if (control && ctrl && !meta && !shift && !repeat) {
|
if (control && cmd && !shift && !repeat) {
|
||||||
action_app_switch(controller, action);
|
action_app_switch(controller, action);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_m:
|
case SDLK_m:
|
||||||
|
// Ctrl+m on all platform, since Cmd+m is already captured by
|
||||||
|
// the system on macOS to minimize the window
|
||||||
if (control && ctrl && !meta && !shift && !repeat) {
|
if (control && ctrl && !meta && !shift && !repeat) {
|
||||||
action_menu(controller, action);
|
action_menu(controller, action);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_p:
|
case SDLK_p:
|
||||||
if (control && ctrl && !meta && !shift && !repeat) {
|
if (control && cmd && !shift && !repeat) {
|
||||||
action_power(controller, action);
|
action_power(controller, action);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_o:
|
case SDLK_o:
|
||||||
if (control && ctrl && !shift && !meta && down) {
|
if (control && cmd && !shift && down) {
|
||||||
set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF);
|
set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_DOWN:
|
case SDLK_DOWN:
|
||||||
#ifdef __APPLE__
|
if (control && cmd && !shift) {
|
||||||
if (control && !ctrl && meta && !shift) {
|
|
||||||
#else
|
|
||||||
if (control && ctrl && !meta && !shift) {
|
|
||||||
#endif
|
|
||||||
// forward repeated events
|
// forward repeated events
|
||||||
action_volume_down(controller, action);
|
action_volume_down(controller, action);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_UP:
|
case SDLK_UP:
|
||||||
#ifdef __APPLE__
|
if (control && cmd && !shift) {
|
||||||
if (control && !ctrl && meta && !shift) {
|
|
||||||
#else
|
|
||||||
if (control && ctrl && !meta && !shift) {
|
|
||||||
#endif
|
|
||||||
// forward repeated events
|
// forward repeated events
|
||||||
action_volume_up(controller, action);
|
action_volume_up(controller, action);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_c:
|
case SDLK_c:
|
||||||
if (control && ctrl && !meta && !shift && !repeat && down) {
|
if (control && cmd && !shift && !repeat && down) {
|
||||||
request_device_clipboard(controller);
|
request_device_clipboard(controller);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_v:
|
case SDLK_v:
|
||||||
if (control && ctrl && !meta && !repeat && down) {
|
if (control && cmd && !repeat && down) {
|
||||||
if (shift) {
|
if (shift) {
|
||||||
// store the text in the device clipboard
|
// store the text in the device clipboard
|
||||||
set_device_clipboard(controller);
|
set_device_clipboard(controller);
|
||||||
@@ -326,29 +333,29 @@ input_manager_process_key(struct input_manager *input_manager,
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_f:
|
case SDLK_f:
|
||||||
if (ctrl && !meta && !shift && !repeat && down) {
|
if (!shift && cmd && !repeat && down) {
|
||||||
screen_switch_fullscreen(input_manager->screen);
|
screen_switch_fullscreen(input_manager->screen);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_x:
|
case SDLK_x:
|
||||||
if (ctrl && !meta && !shift && !repeat && down) {
|
if (!shift && cmd && !repeat && down) {
|
||||||
screen_resize_to_fit(input_manager->screen);
|
screen_resize_to_fit(input_manager->screen);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_g:
|
case SDLK_g:
|
||||||
if (ctrl && !meta && !shift && !repeat && down) {
|
if (!shift && cmd && !repeat && down) {
|
||||||
screen_resize_to_pixel_perfect(input_manager->screen);
|
screen_resize_to_pixel_perfect(input_manager->screen);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_i:
|
case SDLK_i:
|
||||||
if (ctrl && !meta && !shift && !repeat && down) {
|
if (!shift && cmd && !repeat && down) {
|
||||||
struct fps_counter *fps_counter =
|
struct fps_counter *fps_counter =
|
||||||
input_manager->video_buffer->fps_counter;
|
input_manager->video_buffer->fps_counter;
|
||||||
switch_fps_counter_state(fps_counter);
|
switch_fps_counter_state(fps_counter);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_n:
|
case SDLK_n:
|
||||||
if (control && ctrl && !meta && !repeat && down) {
|
if (control && cmd && !repeat && down) {
|
||||||
if (shift) {
|
if (shift) {
|
||||||
collapse_notification_panel(controller);
|
collapse_notification_panel(controller);
|
||||||
} else {
|
} else {
|
||||||
@@ -368,7 +375,7 @@ input_manager_process_key(struct input_manager *input_manager,
|
|||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
if (input_key_from_sdl_to_android(event, &msg)) {
|
if (input_key_from_sdl_to_android(event, &msg)) {
|
||||||
if (!controller_push_msg(controller, &msg)) {
|
if (!controller_push_msg(controller, &msg)) {
|
||||||
LOGW("Cannot request 'inject keycode'");
|
LOGW("Could not request 'inject keycode'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,7 +392,7 @@ input_manager_process_mouse_motion(struct input_manager *input_manager,
|
|||||||
input_manager->screen->frame_size,
|
input_manager->screen->frame_size,
|
||||||
&msg)) {
|
&msg)) {
|
||||||
if (!controller_push_msg(input_manager->controller, &msg)) {
|
if (!controller_push_msg(input_manager->controller, &msg)) {
|
||||||
LOGW("Cannot request 'inject mouse motion event'");
|
LOGW("Could not request 'inject mouse motion event'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -431,7 +438,7 @@ input_manager_process_mouse_button(struct input_manager *input_manager,
|
|||||||
input_manager->screen->frame_size,
|
input_manager->screen->frame_size,
|
||||||
&msg)) {
|
&msg)) {
|
||||||
if (!controller_push_msg(input_manager->controller, &msg)) {
|
if (!controller_push_msg(input_manager->controller, &msg)) {
|
||||||
LOGW("Cannot request 'inject mouse button event'");
|
LOGW("Could not request 'inject mouse button event'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -446,7 +453,7 @@ input_manager_process_mouse_wheel(struct input_manager *input_manager,
|
|||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
if (mouse_wheel_from_sdl_to_android(event, position, &msg)) {
|
if (mouse_wheel_from_sdl_to_android(event, position, &msg)) {
|
||||||
if (!controller_push_msg(input_manager->controller, &msg)) {
|
if (!controller_push_msg(input_manager->controller, &msg)) {
|
||||||
LOGW("Cannot request 'inject mouse wheel event'");
|
LOGW("Could not request 'inject mouse wheel event'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ struct args {
|
|||||||
const char *serial;
|
const char *serial;
|
||||||
const char *crop;
|
const char *crop;
|
||||||
const char *record_filename;
|
const char *record_filename;
|
||||||
|
const char *window_title;
|
||||||
|
const char *push_target;
|
||||||
enum recorder_format record_format;
|
enum recorder_format record_format;
|
||||||
bool fullscreen;
|
bool fullscreen;
|
||||||
bool no_control;
|
bool no_control;
|
||||||
@@ -33,6 +35,11 @@ struct args {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static void usage(const char *arg0) {
|
static void usage(const char *arg0) {
|
||||||
|
#ifdef __APPLE__
|
||||||
|
# define CTRL_OR_CMD "Cmd"
|
||||||
|
#else
|
||||||
|
# define CTRL_OR_CMD "Ctrl"
|
||||||
|
#endif
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"Usage: %s [options]\n"
|
"Usage: %s [options]\n"
|
||||||
"\n"
|
"\n"
|
||||||
@@ -75,6 +82,11 @@ 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"
|
||||||
|
" --push-target path\n"
|
||||||
|
" Set the target directory for pushing files to the device by\n"
|
||||||
|
" drag & drop. It is passed as-is to \"adb push\".\n"
|
||||||
|
" Default is \"/sdcard/\".\n"
|
||||||
|
"\n"
|
||||||
" -r, --record file.mp4\n"
|
" -r, --record file.mp4\n"
|
||||||
" Record screen to file.\n"
|
" Record screen to file.\n"
|
||||||
" The format is determined by the -F/--record-format option if\n"
|
" The format is determined by the -F/--record-format option if\n"
|
||||||
@@ -86,7 +98,7 @@ static void usage(const char *arg0) {
|
|||||||
" This flag forces to render all frames, at a cost of a\n"
|
" This flag forces to render all frames, at a cost of a\n"
|
||||||
" possible increased latency.\n"
|
" possible increased latency.\n"
|
||||||
"\n"
|
"\n"
|
||||||
" -s, --serial\n"
|
" -s, --serial serial\n"
|
||||||
" The device serial number. Mandatory only if several devices\n"
|
" The device serial number. Mandatory only if several devices\n"
|
||||||
" are connected to adb.\n"
|
" are connected to adb.\n"
|
||||||
"\n"
|
"\n"
|
||||||
@@ -103,15 +115,18 @@ static void usage(const char *arg0) {
|
|||||||
" -v, --version\n"
|
" -v, --version\n"
|
||||||
" Print the version of scrcpy.\n"
|
" Print the version of scrcpy.\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" --window-title text\n"
|
||||||
|
" Set a custom window title.\n"
|
||||||
|
"\n"
|
||||||
"Shortcuts:\n"
|
"Shortcuts:\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+f\n"
|
" " CTRL_OR_CMD "+f\n"
|
||||||
" switch fullscreen mode\n"
|
" switch fullscreen mode\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+g\n"
|
" " CTRL_OR_CMD "+g\n"
|
||||||
" resize window to 1:1 (pixel-perfect)\n"
|
" resize window to 1:1 (pixel-perfect)\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+x\n"
|
" " CTRL_OR_CMD "+x\n"
|
||||||
" Double-click on black borders\n"
|
" Double-click on black borders\n"
|
||||||
" resize window to remove black borders\n"
|
" resize window to remove black borders\n"
|
||||||
"\n"
|
"\n"
|
||||||
@@ -119,48 +134,48 @@ static void usage(const char *arg0) {
|
|||||||
" Middle-click\n"
|
" Middle-click\n"
|
||||||
" click on HOME\n"
|
" click on HOME\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+b\n"
|
" " CTRL_OR_CMD "+b\n"
|
||||||
" Ctrl+Backspace\n"
|
" " CTRL_OR_CMD "+Backspace\n"
|
||||||
" Right-click (when screen is on)\n"
|
" Right-click (when screen is on)\n"
|
||||||
" click on BACK\n"
|
" click on BACK\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+s\n"
|
" " CTRL_OR_CMD "+s\n"
|
||||||
" click on APP_SWITCH\n"
|
" click on APP_SWITCH\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+m\n"
|
" Ctrl+m\n"
|
||||||
" click on MENU\n"
|
" click on MENU\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+Up\n"
|
" " CTRL_OR_CMD "+Up\n"
|
||||||
" click on VOLUME_UP\n"
|
" click on VOLUME_UP\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+Down\n"
|
" " CTRL_OR_CMD "+Down\n"
|
||||||
" click on VOLUME_DOWN\n"
|
" click on VOLUME_DOWN\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+p\n"
|
" " CTRL_OR_CMD "+p\n"
|
||||||
" click on POWER (turn screen on/off)\n"
|
" click on POWER (turn screen on/off)\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Right-click (when screen is off)\n"
|
" Right-click (when screen is off)\n"
|
||||||
" power on\n"
|
" power on\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+o\n"
|
" " CTRL_OR_CMD "+o\n"
|
||||||
" turn device screen off (keep mirroring)\n"
|
" turn device screen off (keep mirroring)\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+n\n"
|
" " CTRL_OR_CMD "+n\n"
|
||||||
" expand notification panel\n"
|
" expand notification panel\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+Shift+n\n"
|
" " CTRL_OR_CMD "+Shift+n\n"
|
||||||
" collapse notification panel\n"
|
" collapse notification panel\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+c\n"
|
" " CTRL_OR_CMD "+c\n"
|
||||||
" copy device clipboard to computer\n"
|
" copy device clipboard to computer\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+v\n"
|
" " CTRL_OR_CMD "+v\n"
|
||||||
" paste computer clipboard to device\n"
|
" paste computer clipboard to device\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+Shift+v\n"
|
" " CTRL_OR_CMD "+Shift+v\n"
|
||||||
" copy computer clipboard to device\n"
|
" copy computer clipboard to device\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+i\n"
|
" " CTRL_OR_CMD "+i\n"
|
||||||
" enable/disable FPS counter (print frames/second in logs)\n"
|
" enable/disable FPS counter (print frames/second in logs)\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Drag & drop APK file\n"
|
" Drag & drop APK file\n"
|
||||||
@@ -295,6 +310,8 @@ 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 1002
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
parse_args(struct args *args, int argc, char *argv[]) {
|
parse_args(struct args *args, int argc, char *argv[]) {
|
||||||
@@ -308,6 +325,8 @@ parse_args(struct args *args, int argc, char *argv[]) {
|
|||||||
{"no-control", no_argument, NULL, 'n'},
|
{"no-control", no_argument, NULL, 'n'},
|
||||||
{"no-display", no_argument, NULL, 'N'},
|
{"no-display", no_argument, NULL, 'N'},
|
||||||
{"port", required_argument, NULL, 'p'},
|
{"port", required_argument, NULL, 'p'},
|
||||||
|
{"push-target", required_argument, NULL,
|
||||||
|
OPT_PUSH_TARGET},
|
||||||
{"record", required_argument, NULL, 'r'},
|
{"record", required_argument, NULL, 'r'},
|
||||||
{"record-format", required_argument, NULL, 'f'},
|
{"record-format", required_argument, NULL, 'f'},
|
||||||
{"render-expired-frames", no_argument, NULL,
|
{"render-expired-frames", no_argument, NULL,
|
||||||
@@ -316,6 +335,8 @@ parse_args(struct args *args, int argc, char *argv[]) {
|
|||||||
{"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'},
|
||||||
{"version", no_argument, NULL, 'v'},
|
{"version", no_argument, NULL, 'v'},
|
||||||
|
{"window-title", required_argument, NULL,
|
||||||
|
OPT_WINDOW_TITLE},
|
||||||
{NULL, 0, NULL, 0 },
|
{NULL, 0, NULL, 0 },
|
||||||
};
|
};
|
||||||
int c;
|
int c;
|
||||||
@@ -378,6 +399,12 @@ parse_args(struct args *args, int argc, char *argv[]) {
|
|||||||
case OPT_RENDER_EXPIRED_FRAMES:
|
case OPT_RENDER_EXPIRED_FRAMES:
|
||||||
args->render_expired_frames = true;
|
args->render_expired_frames = true;
|
||||||
break;
|
break;
|
||||||
|
case OPT_WINDOW_TITLE:
|
||||||
|
args->window_title = optarg;
|
||||||
|
break;
|
||||||
|
case OPT_PUSH_TARGET:
|
||||||
|
args->push_target = optarg;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// getopt prints the error message on stderr
|
// getopt prints the error message on stderr
|
||||||
return false;
|
return false;
|
||||||
@@ -414,6 +441,11 @@ parse_args(struct args *args, int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args->no_control && args->turn_screen_off) {
|
||||||
|
LOGE("Could not request to turn screen off if control is disabled");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,6 +461,8 @@ main(int argc, char *argv[]) {
|
|||||||
.serial = NULL,
|
.serial = NULL,
|
||||||
.crop = NULL,
|
.crop = NULL,
|
||||||
.record_filename = NULL,
|
.record_filename = NULL,
|
||||||
|
.window_title = NULL,
|
||||||
|
.push_target = NULL,
|
||||||
.record_format = 0,
|
.record_format = 0,
|
||||||
.help = false,
|
.help = false,
|
||||||
.version = false,
|
.version = false,
|
||||||
@@ -456,6 +490,8 @@ main(int argc, char *argv[]) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOGI("scrcpy " SCRCPY_VERSION " <https://github.com/Genymobile/scrcpy>");
|
||||||
|
|
||||||
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
|
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
|
||||||
av_register_all();
|
av_register_all();
|
||||||
#endif
|
#endif
|
||||||
@@ -473,6 +509,8 @@ main(int argc, char *argv[]) {
|
|||||||
.crop = args.crop,
|
.crop = args.crop,
|
||||||
.port = args.port,
|
.port = args.port,
|
||||||
.record_filename = args.record_filename,
|
.record_filename = args.record_filename,
|
||||||
|
.window_title = args.window_title,
|
||||||
|
.push_target = args.push_target,
|
||||||
.record_format = args.record_format,
|
.record_format = args.record_format,
|
||||||
.max_size = args.max_size,
|
.max_size = args.max_size,
|
||||||
.bit_rate = args.bit_rate,
|
.bit_rate = args.bit_rate,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include "compat.h"
|
#include "compat.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "lock_util.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
|
|
||||||
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
|
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
|
||||||
@@ -26,6 +27,82 @@ find_muxer(const char *name) {
|
|||||||
return oformat;
|
return oformat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static struct record_packet *
|
||||||
|
record_packet_new(const AVPacket *packet) {
|
||||||
|
struct record_packet *rec = SDL_malloc(sizeof(*rec));
|
||||||
|
if (!rec) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (av_packet_ref(&rec->packet, packet)) {
|
||||||
|
SDL_free(rec);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
rec->next = NULL;
|
||||||
|
return rec;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
record_packet_delete(struct record_packet *rec) {
|
||||||
|
av_packet_unref(&rec->packet);
|
||||||
|
SDL_free(rec);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
recorder_queue_init(struct recorder_queue *queue) {
|
||||||
|
queue->first = NULL;
|
||||||
|
// queue->last is undefined if queue->first == NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool
|
||||||
|
recorder_queue_is_empty(struct recorder_queue *queue) {
|
||||||
|
return !queue->first;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
recorder_queue_push(struct recorder_queue *queue, const AVPacket *packet) {
|
||||||
|
struct record_packet *rec = record_packet_new(packet);
|
||||||
|
if (!rec) {
|
||||||
|
LOGC("Could not allocate record packet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rec->next = NULL;
|
||||||
|
|
||||||
|
if (recorder_queue_is_empty(queue)) {
|
||||||
|
queue->first = queue->last = rec;
|
||||||
|
} else {
|
||||||
|
// chain rec after the (current) last packet
|
||||||
|
queue->last->next = rec;
|
||||||
|
// the last packet is now rec
|
||||||
|
queue->last = rec;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline struct record_packet *
|
||||||
|
recorder_queue_take(struct recorder_queue *queue) {
|
||||||
|
SDL_assert(!recorder_queue_is_empty(queue));
|
||||||
|
|
||||||
|
struct record_packet *rec = queue->first;
|
||||||
|
SDL_assert(rec);
|
||||||
|
|
||||||
|
queue->first = rec->next;
|
||||||
|
// no need to update queue->last if the queue is left empty:
|
||||||
|
// queue->last is undefined if queue->first == NULL
|
||||||
|
|
||||||
|
return rec;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
recorder_queue_clear(struct recorder_queue *queue) {
|
||||||
|
struct record_packet *rec = queue->first;
|
||||||
|
while (rec) {
|
||||||
|
struct record_packet *current = rec;
|
||||||
|
rec = rec->next;
|
||||||
|
record_packet_delete(current);
|
||||||
|
}
|
||||||
|
queue->first = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
recorder_init(struct recorder *recorder,
|
recorder_init(struct recorder *recorder,
|
||||||
const char *filename,
|
const char *filename,
|
||||||
@@ -33,10 +110,28 @@ recorder_init(struct recorder *recorder,
|
|||||||
struct size declared_frame_size) {
|
struct size declared_frame_size) {
|
||||||
recorder->filename = SDL_strdup(filename);
|
recorder->filename = SDL_strdup(filename);
|
||||||
if (!recorder->filename) {
|
if (!recorder->filename) {
|
||||||
LOGE("Cannot strdup filename");
|
LOGE("Could not strdup filename");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recorder->mutex = SDL_CreateMutex();
|
||||||
|
if (!recorder->mutex) {
|
||||||
|
LOGC("Could not create mutex");
|
||||||
|
SDL_free(recorder->filename);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder->queue_cond = SDL_CreateCond();
|
||||||
|
if (!recorder->queue_cond) {
|
||||||
|
LOGC("Could not create cond");
|
||||||
|
SDL_DestroyMutex(recorder->mutex);
|
||||||
|
SDL_free(recorder->filename);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder_queue_init(&recorder->queue);
|
||||||
|
recorder->stopped = false;
|
||||||
|
recorder->failed = false;
|
||||||
recorder->format = format;
|
recorder->format = format;
|
||||||
recorder->declared_frame_size = declared_frame_size;
|
recorder->declared_frame_size = declared_frame_size;
|
||||||
recorder->header_written = false;
|
recorder->header_written = false;
|
||||||
@@ -46,6 +141,8 @@ recorder_init(struct recorder *recorder,
|
|||||||
|
|
||||||
void
|
void
|
||||||
recorder_destroy(struct recorder *recorder) {
|
recorder_destroy(struct recorder *recorder) {
|
||||||
|
SDL_DestroyCond(recorder->queue_cond);
|
||||||
|
SDL_DestroyMutex(recorder->mutex);
|
||||||
SDL_free(recorder->filename);
|
SDL_free(recorder->filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,12 +216,17 @@ recorder_close(struct recorder *recorder) {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
avio_close(recorder->ctx->pb);
|
avio_close(recorder->ctx->pb);
|
||||||
avformat_free_context(recorder->ctx);
|
avformat_free_context(recorder->ctx);
|
||||||
|
|
||||||
const char *format_name = recorder_get_format_name(recorder->format);
|
if (recorder->failed) {
|
||||||
LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
|
LOGE("Recording failed to %s", recorder->filename);
|
||||||
|
} else {
|
||||||
|
const char *format_name = recorder_get_format_name(recorder->format);
|
||||||
|
LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
@@ -133,7 +235,7 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
|
|||||||
|
|
||||||
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
|
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
|
||||||
if (!extradata) {
|
if (!extradata) {
|
||||||
LOGC("Cannot allocate extradata");
|
LOGC("Could not allocate extradata");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,9 +253,6 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
|
|||||||
int ret = avformat_write_header(recorder->ctx, NULL);
|
int ret = avformat_write_header(recorder->ctx, NULL);
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
LOGE("Failed to write header to %s", recorder->filename);
|
LOGE("Failed to write header to %s", recorder->filename);
|
||||||
SDL_free(extradata);
|
|
||||||
avio_closep(&recorder->ctx->pb);
|
|
||||||
avformat_free_context(recorder->ctx);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,13 +268,110 @@ recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) {
|
|||||||
bool
|
bool
|
||||||
recorder_write(struct recorder *recorder, AVPacket *packet) {
|
recorder_write(struct recorder *recorder, AVPacket *packet) {
|
||||||
if (!recorder->header_written) {
|
if (!recorder->header_written) {
|
||||||
|
if (packet->pts != AV_NOPTS_VALUE) {
|
||||||
|
LOGE("The first packet is not a config packet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
bool ok = recorder_write_header(recorder, packet);
|
bool ok = recorder_write_header(recorder, packet);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
recorder->header_written = true;
|
recorder->header_written = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet->pts == AV_NOPTS_VALUE) {
|
||||||
|
// ignore config packets
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
recorder_rescale_packet(recorder, packet);
|
recorder_rescale_packet(recorder, packet);
|
||||||
return av_write_frame(recorder->ctx, packet) >= 0;
|
return av_write_frame(recorder->ctx, packet) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
run_recorder(void *data) {
|
||||||
|
struct recorder *recorder = data;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
mutex_lock(recorder->mutex);
|
||||||
|
|
||||||
|
while (!recorder->stopped &&
|
||||||
|
recorder_queue_is_empty(&recorder->queue)) {
|
||||||
|
cond_wait(recorder->queue_cond, recorder->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if stopped is set, continue to process the remaining events (to
|
||||||
|
// finish the recording) before actually stopping
|
||||||
|
|
||||||
|
if (recorder->stopped && recorder_queue_is_empty(&recorder->queue)) {
|
||||||
|
mutex_unlock(recorder->mutex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct record_packet *rec = recorder_queue_take(&recorder->queue);
|
||||||
|
|
||||||
|
mutex_unlock(recorder->mutex);
|
||||||
|
|
||||||
|
bool ok = recorder_write(recorder, &rec->packet);
|
||||||
|
record_packet_delete(rec);
|
||||||
|
if (!ok) {
|
||||||
|
LOGE("Could not record packet");
|
||||||
|
|
||||||
|
mutex_lock(recorder->mutex);
|
||||||
|
recorder->failed = true;
|
||||||
|
// discard pending packets
|
||||||
|
recorder_queue_clear(&recorder->queue);
|
||||||
|
mutex_unlock(recorder->mutex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Recorder thread ended");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
recorder_start(struct recorder *recorder) {
|
||||||
|
LOGD("Starting recorder thread");
|
||||||
|
|
||||||
|
recorder->thread = SDL_CreateThread(run_recorder, "recorder", recorder);
|
||||||
|
if (!recorder->thread) {
|
||||||
|
LOGC("Could not start recorder thread");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
recorder_stop(struct recorder *recorder) {
|
||||||
|
mutex_lock(recorder->mutex);
|
||||||
|
recorder->stopped = true;
|
||||||
|
cond_signal(recorder->queue_cond);
|
||||||
|
mutex_unlock(recorder->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
recorder_join(struct recorder *recorder) {
|
||||||
|
SDL_WaitThread(recorder->thread, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
recorder_push(struct recorder *recorder, const AVPacket *packet) {
|
||||||
|
mutex_lock(recorder->mutex);
|
||||||
|
SDL_assert(!recorder->stopped);
|
||||||
|
|
||||||
|
if (recorder->failed) {
|
||||||
|
// reject any new packet (this will stop the stream)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ok = recorder_queue_push(&recorder->queue, packet);
|
||||||
|
cond_signal(recorder->queue_cond);
|
||||||
|
|
||||||
|
mutex_unlock(recorder->mutex);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
|
#include <SDL2/SDL_mutex.h>
|
||||||
|
#include <SDL2/SDL_thread.h>
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
@@ -11,12 +13,29 @@ enum recorder_format {
|
|||||||
RECORDER_FORMAT_MKV,
|
RECORDER_FORMAT_MKV,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct record_packet {
|
||||||
|
AVPacket packet;
|
||||||
|
struct record_packet *next;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct recorder_queue {
|
||||||
|
struct record_packet *first;
|
||||||
|
struct record_packet *last; // undefined if first is NULL
|
||||||
|
};
|
||||||
|
|
||||||
struct recorder {
|
struct recorder {
|
||||||
char *filename;
|
char *filename;
|
||||||
enum recorder_format format;
|
enum recorder_format format;
|
||||||
AVFormatContext *ctx;
|
AVFormatContext *ctx;
|
||||||
struct size declared_frame_size;
|
struct size declared_frame_size;
|
||||||
bool header_written;
|
bool header_written;
|
||||||
|
|
||||||
|
SDL_Thread *thread;
|
||||||
|
SDL_mutex *mutex;
|
||||||
|
SDL_cond *queue_cond;
|
||||||
|
bool stopped; // set on recorder_stop() by the stream reader
|
||||||
|
bool failed; // set on packet write failure
|
||||||
|
struct recorder_queue queue;
|
||||||
};
|
};
|
||||||
|
|
||||||
bool
|
bool
|
||||||
@@ -33,6 +52,15 @@ void
|
|||||||
recorder_close(struct recorder *recorder);
|
recorder_close(struct recorder *recorder);
|
||||||
|
|
||||||
bool
|
bool
|
||||||
recorder_write(struct recorder *recorder, AVPacket *packet);
|
recorder_start(struct recorder *recorder);
|
||||||
|
|
||||||
|
void
|
||||||
|
recorder_stop(struct recorder *recorder);
|
||||||
|
|
||||||
|
void
|
||||||
|
recorder_join(struct recorder *recorder);
|
||||||
|
|
||||||
|
bool
|
||||||
|
recorder_push(struct recorder *recorder, const AVPacket *packet);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
|||||||
}
|
}
|
||||||
char *local_fmt = SDL_malloc(strlen(fmt) + 10);
|
char *local_fmt = SDL_malloc(strlen(fmt) + 10);
|
||||||
if (!local_fmt) {
|
if (!local_fmt) {
|
||||||
LOGC("Cannot allocate string");
|
LOGC("Could not allocate string");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// strcpy is safe here, the destination is large enough
|
// strcpy is safe here, the destination is large enough
|
||||||
@@ -277,7 +277,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,
|
||||||
.send_frame_meta = record,
|
|
||||||
.control = options->control,
|
.control = options->control,
|
||||||
};
|
};
|
||||||
if (!server_start(&server, options->serial, ¶ms)) {
|
if (!server_start(&server, options->serial, ¶ms)) {
|
||||||
@@ -334,7 +333,8 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
video_buffer_initialized = true;
|
video_buffer_initialized = true;
|
||||||
|
|
||||||
if (options->control) {
|
if (options->control) {
|
||||||
if (!file_handler_init(&file_handler, server.serial)) {
|
if (!file_handler_init(&file_handler, server.serial,
|
||||||
|
options->push_target)) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
file_handler_initialized = true;
|
file_handler_initialized = true;
|
||||||
@@ -380,7 +380,10 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
controller_started = true;
|
controller_started = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!screen_init_rendering(&screen, device_name, frame_size,
|
const char *window_title =
|
||||||
|
options->window_title ? options->window_title : device_name;
|
||||||
|
|
||||||
|
if (!screen_init_rendering(&screen, window_title, frame_size,
|
||||||
options->always_on_top)) {
|
options->always_on_top)) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
@@ -391,7 +394,7 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF;
|
msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF;
|
||||||
|
|
||||||
if (!controller_push_msg(&controller, &msg)) {
|
if (!controller_push_msg(&controller, &msg)) {
|
||||||
LOGW("Cannot request 'set screen power mode'");
|
LOGW("Could not request 'set screen power mode'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ struct scrcpy_options {
|
|||||||
const char *serial;
|
const char *serial;
|
||||||
const char *crop;
|
const char *crop;
|
||||||
const char *record_filename;
|
const char *record_filename;
|
||||||
|
const char *window_title;
|
||||||
|
const char *push_target;
|
||||||
enum recorder_format record_format;
|
enum recorder_format record_format;
|
||||||
uint16_t port;
|
uint16_t port;
|
||||||
uint16_t max_size;
|
uint16_t max_size;
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ get_optimal_size(struct size current_size, struct size frame_size) {
|
|||||||
uint32_t h;
|
uint32_t h;
|
||||||
|
|
||||||
if (!get_preferred_display_bounds(&display_size)) {
|
if (!get_preferred_display_bounds(&display_size)) {
|
||||||
// cannot get display bounds, do not constraint the size
|
// could not get display bounds, do not constraint the size
|
||||||
w = current_size.width;
|
w = current_size.width;
|
||||||
h = current_size.height;
|
h = current_size.height;
|
||||||
} else {
|
} else {
|
||||||
@@ -134,7 +134,7 @@ create_texture(SDL_Renderer *renderer, struct size frame_size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
screen_init_rendering(struct screen *screen, const char *device_name,
|
screen_init_rendering(struct screen *screen, const char *window_title,
|
||||||
struct size frame_size, bool always_on_top) {
|
struct size frame_size, bool always_on_top) {
|
||||||
screen->frame_size = frame_size;
|
screen->frame_size = frame_size;
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ screen_init_rendering(struct screen *screen, const char *device_name,
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
screen->window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED,
|
screen->window = SDL_CreateWindow(window_title, SDL_WINDOWPOS_UNDEFINED,
|
||||||
SDL_WINDOWPOS_UNDEFINED,
|
SDL_WINDOWPOS_UNDEFINED,
|
||||||
window_size.width, window_size.height,
|
window_size.width, window_size.height,
|
||||||
window_flags);
|
window_flags);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ screen_init(struct screen *screen);
|
|||||||
|
|
||||||
// initialize screen, create window, renderer and texture (window is hidden)
|
// initialize screen, create window, renderer and texture (window is hidden)
|
||||||
bool
|
bool
|
||||||
screen_init_rendering(struct screen *screen, const char *device_name,
|
screen_init_rendering(struct screen *screen, const char *window_title,
|
||||||
struct size frame_size, bool always_on_top);
|
struct size frame_size, bool always_on_top);
|
||||||
|
|
||||||
// show the window
|
// show the window
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
#define SOCKET_NAME "scrcpy"
|
#define SOCKET_NAME "scrcpy"
|
||||||
#define SERVER_FILENAME "scrcpy-server.jar"
|
#define SERVER_FILENAME "scrcpy-server.jar"
|
||||||
|
|
||||||
#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FLENAME
|
#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME
|
||||||
#define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME
|
#define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME
|
||||||
|
|
||||||
static const char *
|
static const char *
|
||||||
@@ -35,7 +35,7 @@ get_server_path(void) {
|
|||||||
// use scrcpy-server.jar in the same directory as the executable
|
// use scrcpy-server.jar in the same directory as the executable
|
||||||
char *executable_path = get_executable_path();
|
char *executable_path = get_executable_path();
|
||||||
if (!executable_path) {
|
if (!executable_path) {
|
||||||
LOGE("Cannot get executable path, "
|
LOGE("Could not get executable path, "
|
||||||
"using " SERVER_FILENAME " from current directory");
|
"using " SERVER_FILENAME " from current directory");
|
||||||
// not found, use current directory
|
// not found, use current directory
|
||||||
return SERVER_FILENAME;
|
return SERVER_FILENAME;
|
||||||
@@ -47,7 +47,7 @@ get_server_path(void) {
|
|||||||
size_t len = dirlen + 1 + sizeof(SERVER_FILENAME);
|
size_t len = dirlen + 1 + sizeof(SERVER_FILENAME);
|
||||||
char *server_path = SDL_malloc(len);
|
char *server_path = SDL_malloc(len);
|
||||||
if (!server_path) {
|
if (!server_path) {
|
||||||
LOGE("Cannot alloc server path string, "
|
LOGE("Could not alloc server path string, "
|
||||||
"using " SERVER_FILENAME " from current directory");
|
"using " SERVER_FILENAME " from current directory");
|
||||||
SDL_free(executable_path);
|
SDL_free(executable_path);
|
||||||
return SERVER_FILENAME;
|
return SERVER_FILENAME;
|
||||||
@@ -130,7 +130,7 @@ execute_server(struct server *server, const struct server_params *params) {
|
|||||||
bit_rate_string,
|
bit_rate_string,
|
||||||
server->tunnel_forward ? "true" : "false",
|
server->tunnel_forward ? "true" : "false",
|
||||||
params->crop ? params->crop : "-",
|
params->crop ? params->crop : "-",
|
||||||
params->send_frame_meta ? "true" : "false",
|
"true", // always send frame meta (packet boundaries + timestamp)
|
||||||
params->control ? "true" : "false",
|
params->control ? "true" : "false",
|
||||||
};
|
};
|
||||||
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
|
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
|
||||||
@@ -155,6 +155,7 @@ connect_and_read_byte(uint16_t port) {
|
|||||||
// is not listening, so read one byte to detect a working connection
|
// is not listening, so read one byte to detect a working connection
|
||||||
if (net_recv(socket, &byte, 1) != 1) {
|
if (net_recv(socket, &byte, 1) != 1) {
|
||||||
// the server is not listening yet behind the adb tunnel
|
// the server is not listening yet behind the adb tunnel
|
||||||
|
net_close(socket);
|
||||||
return INVALID_SOCKET;
|
return INVALID_SOCKET;
|
||||||
}
|
}
|
||||||
return socket;
|
return socket;
|
||||||
@@ -181,7 +182,7 @@ close_socket(socket_t *socket) {
|
|||||||
SDL_assert(*socket != INVALID_SOCKET);
|
SDL_assert(*socket != INVALID_SOCKET);
|
||||||
net_shutdown(*socket, SHUT_RDWR);
|
net_shutdown(*socket, SHUT_RDWR);
|
||||||
if (!net_close(*socket)) {
|
if (!net_close(*socket)) {
|
||||||
LOGW("Cannot close socket");
|
LOGW("Could not close socket");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
*socket = INVALID_SOCKET;
|
*socket = INVALID_SOCKET;
|
||||||
@@ -305,7 +306,7 @@ server_stop(struct server *server) {
|
|||||||
SDL_assert(server->process != PROCESS_NONE);
|
SDL_assert(server->process != PROCESS_NONE);
|
||||||
|
|
||||||
if (!cmd_terminate(server->process)) {
|
if (!cmd_terminate(server->process)) {
|
||||||
LOGW("Cannot terminate server");
|
LOGW("Could not terminate server");
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_simple_wait(server->process, NULL); // ignore exit code
|
cmd_simple_wait(server->process, NULL); // ignore exit code
|
||||||
|
|||||||
@@ -34,7 +34,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;
|
||||||
bool send_frame_meta;
|
|
||||||
bool control;
|
bool control;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
358
app/src/stream.c
358
app/src/stream.c
@@ -22,54 +22,8 @@
|
|||||||
#define HEADER_SIZE 12
|
#define HEADER_SIZE 12
|
||||||
#define NO_PTS UINT64_C(-1)
|
#define NO_PTS UINT64_C(-1)
|
||||||
|
|
||||||
static struct frame_meta *
|
|
||||||
frame_meta_new(uint64_t pts) {
|
|
||||||
struct frame_meta *meta = SDL_malloc(sizeof(*meta));
|
|
||||||
if (!meta) {
|
|
||||||
return meta;
|
|
||||||
}
|
|
||||||
meta->pts = pts;
|
|
||||||
meta->next = NULL;
|
|
||||||
return meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
frame_meta_delete(struct frame_meta *frame_meta) {
|
|
||||||
SDL_free(frame_meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
receiver_state_push_meta(struct receiver_state *state, uint64_t pts) {
|
stream_recv_packet(struct stream *stream, AVPacket *packet) {
|
||||||
struct frame_meta *frame_meta = frame_meta_new(pts);
|
|
||||||
if (!frame_meta) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// append to the list
|
|
||||||
// (iterate to find the last item, in practice the list should be tiny)
|
|
||||||
struct frame_meta **p = &state->frame_meta_queue;
|
|
||||||
while (*p) {
|
|
||||||
p = &(*p)->next;
|
|
||||||
}
|
|
||||||
*p = frame_meta;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint64_t
|
|
||||||
receiver_state_take_meta(struct receiver_state *state) {
|
|
||||||
struct frame_meta *frame_meta = state->frame_meta_queue; // first item
|
|
||||||
SDL_assert(frame_meta); // must not be empty
|
|
||||||
uint64_t pts = frame_meta->pts;
|
|
||||||
state->frame_meta_queue = frame_meta->next; // remove the item
|
|
||||||
frame_meta_delete(frame_meta);
|
|
||||||
return pts;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) {
|
|
||||||
struct stream *stream = opaque;
|
|
||||||
struct receiver_state *state = &stream->receiver_state;
|
|
||||||
|
|
||||||
// The video stream contains raw packets, without time information. When we
|
// The video stream contains raw packets, without time information. When we
|
||||||
// record, we retrieve the timestamps separately, from a "meta" header
|
// record, we retrieve the timestamps separately, from a "meta" header
|
||||||
// added by the server before each raw packet.
|
// added by the server before each raw packet.
|
||||||
@@ -82,60 +36,30 @@ read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) {
|
|||||||
//
|
//
|
||||||
// It is followed by <packet_size> bytes containing the packet/frame.
|
// It is followed by <packet_size> bytes containing the packet/frame.
|
||||||
|
|
||||||
if (!state->remaining) {
|
uint8_t header[HEADER_SIZE];
|
||||||
#define HEADER_SIZE 12
|
ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE);
|
||||||
uint8_t header[HEADER_SIZE];
|
if (r < HEADER_SIZE) {
|
||||||
ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE);
|
return false;
|
||||||
if (r == -1) {
|
|
||||||
return AVERROR(errno);
|
|
||||||
}
|
|
||||||
if (r == 0) {
|
|
||||||
return AVERROR_EOF;
|
|
||||||
}
|
|
||||||
// no partial read (net_recv_all())
|
|
||||||
SDL_assert_release(r == HEADER_SIZE);
|
|
||||||
|
|
||||||
uint64_t pts = buffer_read64be(header);
|
|
||||||
state->remaining = buffer_read32be(&header[8]);
|
|
||||||
|
|
||||||
if (pts != NO_PTS && !receiver_state_push_meta(state, pts)) {
|
|
||||||
LOGE("Could not store PTS for recording");
|
|
||||||
// we cannot save the PTS, the recording would be broken
|
|
||||||
return AVERROR(ENOMEM);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_assert(state->remaining);
|
uint64_t pts = buffer_read64be(header);
|
||||||
|
uint32_t len = buffer_read32be(&header[8]);
|
||||||
|
SDL_assert(len);
|
||||||
|
|
||||||
if (buf_size > state->remaining) {
|
if (av_new_packet(packet, len)) {
|
||||||
buf_size = state->remaining;
|
LOGE("Could not allocate packet");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ssize_t r = net_recv(stream->socket, buf, buf_size);
|
r = net_recv_all(stream->socket, packet->data, len);
|
||||||
if (r == -1) {
|
if (r < len) {
|
||||||
return errno ? AVERROR(errno) : AVERROR_EOF;
|
av_packet_unref(packet);
|
||||||
}
|
return false;
|
||||||
if (r == 0) {
|
|
||||||
return AVERROR_EOF;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_assert(state->remaining >= r);
|
packet->pts = pts != NO_PTS ? pts : AV_NOPTS_VALUE;
|
||||||
state->remaining -= r;
|
|
||||||
|
|
||||||
return r;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
read_raw_packet(void *opaque, uint8_t *buf, int buf_size) {
|
|
||||||
struct stream *stream = opaque;
|
|
||||||
ssize_t r = net_recv(stream->socket, buf, buf_size);
|
|
||||||
if (r == -1) {
|
|
||||||
return errno ? AVERROR(errno) : AVERROR_EOF;
|
|
||||||
}
|
|
||||||
if (r == 0) {
|
|
||||||
return AVERROR_EOF;
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -145,116 +69,199 @@ notify_stopped(void) {
|
|||||||
SDL_PushEvent(&stop_event);
|
SDL_PushEvent(&stop_event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
process_config_packet(struct stream *stream, AVPacket *packet) {
|
||||||
|
if (stream->recorder && !recorder_push(stream->recorder, packet)) {
|
||||||
|
LOGE("Could not send config packet to recorder");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
process_frame(struct stream *stream, AVPacket *packet) {
|
||||||
|
if (stream->decoder && !decoder_push(stream->decoder, packet)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream->recorder) {
|
||||||
|
packet->dts = packet->pts;
|
||||||
|
|
||||||
|
if (!recorder_push(stream->recorder, packet)) {
|
||||||
|
LOGE("Could not send packet to recorder");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
stream_parse(struct stream *stream, AVPacket *packet) {
|
||||||
|
uint8_t *in_data = packet->data;
|
||||||
|
int in_len = packet->size;
|
||||||
|
uint8_t *out_data = NULL;
|
||||||
|
int out_len = 0;
|
||||||
|
int r = av_parser_parse2(stream->parser, stream->codec_ctx,
|
||||||
|
&out_data, &out_len, in_data, in_len,
|
||||||
|
AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1);
|
||||||
|
|
||||||
|
// PARSER_FLAG_COMPLETE_FRAMES is set
|
||||||
|
SDL_assert(r == in_len);
|
||||||
|
SDL_assert(out_len == in_len);
|
||||||
|
|
||||||
|
if (stream->parser->key_frame == 1) {
|
||||||
|
packet->flags |= AV_PKT_FLAG_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ok = process_frame(stream, packet);
|
||||||
|
if (!ok) {
|
||||||
|
LOGE("Could not process frame");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
stream_push_packet(struct stream *stream, AVPacket *packet) {
|
||||||
|
bool is_config = packet->pts == AV_NOPTS_VALUE;
|
||||||
|
|
||||||
|
// A config packet must not be decoded immetiately (it contains no
|
||||||
|
// frame); instead, it must be concatenated with the future data packet.
|
||||||
|
if (stream->has_pending || is_config) {
|
||||||
|
size_t offset;
|
||||||
|
if (stream->has_pending) {
|
||||||
|
offset = stream->pending.size;
|
||||||
|
if (av_grow_packet(&stream->pending, packet->size)) {
|
||||||
|
LOGE("Could not grow packet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
offset = 0;
|
||||||
|
if (av_new_packet(&stream->pending, packet->size)) {
|
||||||
|
LOGE("Could not create packet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
stream->has_pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(stream->pending.data + offset, packet->data, packet->size);
|
||||||
|
|
||||||
|
if (!is_config) {
|
||||||
|
// prepare the concat packet to send to the decoder
|
||||||
|
stream->pending.pts = packet->pts;
|
||||||
|
stream->pending.dts = packet->dts;
|
||||||
|
stream->pending.flags = packet->flags;
|
||||||
|
packet = &stream->pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_config) {
|
||||||
|
// config packet
|
||||||
|
bool ok = process_config_packet(stream, packet);
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// data packet
|
||||||
|
bool ok = stream_parse(stream, packet);
|
||||||
|
|
||||||
|
if (stream->has_pending) {
|
||||||
|
// the pending packet must be discarded (consumed or error)
|
||||||
|
stream->has_pending = false;
|
||||||
|
av_packet_unref(&stream->pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
run_stream(void *data) {
|
run_stream(void *data) {
|
||||||
struct stream *stream = data;
|
struct stream *stream = data;
|
||||||
|
|
||||||
AVFormatContext *format_ctx = avformat_alloc_context();
|
|
||||||
if (!format_ctx) {
|
|
||||||
LOGC("Could not allocate format context");
|
|
||||||
goto end;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned char *buffer = av_malloc(BUFSIZE);
|
|
||||||
if (!buffer) {
|
|
||||||
LOGC("Could not allocate buffer");
|
|
||||||
goto finally_free_format_ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize the receiver state
|
|
||||||
stream->receiver_state.frame_meta_queue = NULL;
|
|
||||||
stream->receiver_state.remaining = 0;
|
|
||||||
|
|
||||||
// if recording is enabled, a "header" is sent between raw packets
|
|
||||||
int (*read_packet)(void *, uint8_t *, int) =
|
|
||||||
stream->recorder ? read_packet_with_meta : read_raw_packet;
|
|
||||||
AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, stream,
|
|
||||||
read_packet, NULL, NULL);
|
|
||||||
if (!avio_ctx) {
|
|
||||||
LOGC("Could not allocate avio context");
|
|
||||||
// avformat_open_input takes ownership of 'buffer'
|
|
||||||
// so only free the buffer before avformat_open_input()
|
|
||||||
av_free(buffer);
|
|
||||||
goto finally_free_format_ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
format_ctx->pb = avio_ctx;
|
|
||||||
|
|
||||||
if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) {
|
|
||||||
LOGE("Could not open video stream");
|
|
||||||
goto finally_free_avio_ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
|
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
|
||||||
if (!codec) {
|
if (!codec) {
|
||||||
LOGE("H.264 decoder not found");
|
LOGE("H.264 decoder not found");
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stream->codec_ctx = avcodec_alloc_context3(codec);
|
||||||
|
if (!stream->codec_ctx) {
|
||||||
|
LOGC("Could not allocate codec context");
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
if (stream->decoder && !decoder_open(stream->decoder, codec)) {
|
if (stream->decoder && !decoder_open(stream->decoder, codec)) {
|
||||||
LOGE("Could not open decoder");
|
LOGE("Could not open decoder");
|
||||||
goto finally_close_input;
|
goto finally_free_codec_ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stream->recorder && !recorder_open(stream->recorder, codec)) {
|
if (stream->recorder) {
|
||||||
LOGE("Could not open recorder");
|
if (!recorder_open(stream->recorder, codec)) {
|
||||||
goto finally_close_input;
|
LOGE("Could not open recorder");
|
||||||
|
goto finally_close_decoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!recorder_start(stream->recorder)) {
|
||||||
|
LOGE("Could not start recorder");
|
||||||
|
goto finally_close_recorder;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AVPacket packet;
|
stream->parser = av_parser_init(AV_CODEC_ID_H264);
|
||||||
av_init_packet(&packet);
|
if (!stream->parser) {
|
||||||
packet.data = NULL;
|
LOGE("Could not initialize parser");
|
||||||
packet.size = 0;
|
goto finally_stop_and_join_recorder;
|
||||||
|
}
|
||||||
|
|
||||||
while (!av_read_frame(format_ctx, &packet)) {
|
// We must only pass complete frames to av_parser_parse2()!
|
||||||
if (SDL_AtomicGet(&stream->stopped)) {
|
// It's more complicated, but this allows to reduce the latency by 1 frame!
|
||||||
// if the stream is stopped, the socket had been shutdown, so the
|
stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
|
||||||
// last packet is probably corrupted (but not detected as such by
|
|
||||||
// FFmpeg) and will not be decoded correctly
|
for (;;) {
|
||||||
av_packet_unref(&packet);
|
AVPacket packet;
|
||||||
goto quit;
|
bool ok = stream_recv_packet(stream, &packet);
|
||||||
}
|
if (!ok) {
|
||||||
if (stream->decoder && !decoder_push(stream->decoder, &packet)) {
|
// end of stream
|
||||||
av_packet_unref(&packet);
|
break;
|
||||||
goto quit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stream->recorder) {
|
|
||||||
// we retrieve the PTS in order they were received, so they will
|
|
||||||
// be assigned to the correct frame
|
|
||||||
uint64_t pts = receiver_state_take_meta(&stream->receiver_state);
|
|
||||||
packet.pts = pts;
|
|
||||||
packet.dts = pts;
|
|
||||||
|
|
||||||
// no need to rescale with av_packet_rescale_ts(), the timestamps
|
|
||||||
// are in microseconds both in input and output
|
|
||||||
if (!recorder_write(stream->recorder, &packet)) {
|
|
||||||
LOGE("Could not write frame to output file");
|
|
||||||
av_packet_unref(&packet);
|
|
||||||
goto quit;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ok = stream_push_packet(stream, &packet);
|
||||||
av_packet_unref(&packet);
|
av_packet_unref(&packet);
|
||||||
|
if (!ok) {
|
||||||
if (avio_ctx->eof_reached) {
|
// cannot process packet (error already logged)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGD("End of frames");
|
LOGD("End of frames");
|
||||||
|
|
||||||
quit:
|
if (stream->has_pending) {
|
||||||
|
av_packet_unref(&stream->pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
av_parser_close(stream->parser);
|
||||||
|
finally_stop_and_join_recorder:
|
||||||
|
if (stream->recorder) {
|
||||||
|
recorder_stop(stream->recorder);
|
||||||
|
LOGI("Finishing recording...");
|
||||||
|
recorder_join(stream->recorder);
|
||||||
|
}
|
||||||
|
finally_close_recorder:
|
||||||
if (stream->recorder) {
|
if (stream->recorder) {
|
||||||
recorder_close(stream->recorder);
|
recorder_close(stream->recorder);
|
||||||
}
|
}
|
||||||
finally_close_input:
|
finally_close_decoder:
|
||||||
avformat_close_input(&format_ctx);
|
if (stream->decoder) {
|
||||||
finally_free_avio_ctx:
|
decoder_close(stream->decoder);
|
||||||
av_free(avio_ctx->buffer);
|
}
|
||||||
av_free(avio_ctx);
|
finally_free_codec_ctx:
|
||||||
finally_free_format_ctx:
|
avcodec_free_context(&stream->codec_ctx);
|
||||||
avformat_free_context(format_ctx);
|
|
||||||
end:
|
end:
|
||||||
notify_stopped();
|
notify_stopped();
|
||||||
return 0;
|
return 0;
|
||||||
@@ -266,7 +273,7 @@ stream_init(struct stream *stream, socket_t socket,
|
|||||||
stream->socket = socket;
|
stream->socket = socket;
|
||||||
stream->decoder = decoder,
|
stream->decoder = decoder,
|
||||||
stream->recorder = recorder;
|
stream->recorder = recorder;
|
||||||
SDL_AtomicSet(&stream->stopped, 0);
|
stream->has_pending = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
@@ -283,7 +290,6 @@ stream_start(struct stream *stream) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
stream_stop(struct stream *stream) {
|
stream_stop(struct stream *stream) {
|
||||||
SDL_AtomicSet(&stream->stopped, 1);
|
|
||||||
if (stream->decoder) {
|
if (stream->decoder) {
|
||||||
decoder_interrupt(stream->decoder);
|
decoder_interrupt(stream->decoder);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
#include <SDL2/SDL_atomic.h>
|
#include <SDL2/SDL_atomic.h>
|
||||||
#include <SDL2/SDL_thread.h>
|
#include <SDL2/SDL_thread.h>
|
||||||
|
|
||||||
@@ -10,23 +11,18 @@
|
|||||||
|
|
||||||
struct video_buffer;
|
struct video_buffer;
|
||||||
|
|
||||||
struct frame_meta {
|
|
||||||
uint64_t pts;
|
|
||||||
struct frame_meta *next;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct stream {
|
struct stream {
|
||||||
socket_t socket;
|
socket_t socket;
|
||||||
struct video_buffer *video_buffer;
|
struct video_buffer *video_buffer;
|
||||||
SDL_Thread *thread;
|
SDL_Thread *thread;
|
||||||
SDL_atomic_t stopped;
|
|
||||||
struct decoder *decoder;
|
struct decoder *decoder;
|
||||||
struct recorder *recorder;
|
struct recorder *recorder;
|
||||||
struct receiver_state {
|
AVCodecContext *codec_ctx;
|
||||||
// meta (in order) for frames not consumed yet
|
AVCodecParserContext *parser;
|
||||||
struct frame_meta *frame_meta_queue;
|
// successive packets may need to be concatenated, until a non-config
|
||||||
size_t remaining; // remaining bytes to receive for the current frame
|
// packet is available
|
||||||
} receiver_state;
|
bool has_pending;
|
||||||
|
AVPacket pending;
|
||||||
};
|
};
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ cmd_simple_wait(pid_t pid, int *exit_code) {
|
|||||||
int status;
|
int status;
|
||||||
int code;
|
int code;
|
||||||
if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) {
|
if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) {
|
||||||
// cannot wait, or exited unexpectedly, probably by a signal
|
// could not wait, or exited unexpectedly, probably by a signal
|
||||||
code = -1;
|
code = -1;
|
||||||
} else {
|
} else {
|
||||||
code = WEXITSTATUS(status);
|
code = WEXITSTATUS(status);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
|
|||||||
|
|
||||||
wchar_t *wide = utf8_to_wide_char(cmd);
|
wchar_t *wide = utf8_to_wide_char(cmd);
|
||||||
if (!wide) {
|
if (!wide) {
|
||||||
LOGC("Cannot allocate wide char string");
|
LOGC("Could not allocate wide char string");
|
||||||
return PROCESS_ERROR_GENERIC;
|
return PROCESS_ERROR_GENERIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
|
|||||||
DWORD code;
|
DWORD code;
|
||||||
if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0
|
if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0
|
||||||
|| !GetExitCodeProcess(handle, &code)) {
|
|| !GetExitCodeProcess(handle, &code)) {
|
||||||
// cannot wait or retrieve the exit code
|
// could not wait or retrieve the exit code
|
||||||
code = -1; // max value, it's unsigned
|
code = -1; // max value, it's unsigned
|
||||||
}
|
}
|
||||||
if (exit_code) {
|
if (exit_code) {
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ project('scrcpy', 'c',
|
|||||||
meson_version: '>= 0.37',
|
meson_version: '>= 0.37',
|
||||||
default_options: 'c_std=c11')
|
default_options: 'c_std=c11')
|
||||||
|
|
||||||
if get_option('build_app')
|
if get_option('compile_app')
|
||||||
subdir('app')
|
subdir('app')
|
||||||
endif
|
endif
|
||||||
|
|
||||||
if get_option('build_server')
|
if get_option('compile_server')
|
||||||
subdir('server')
|
subdir('server')
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
option('build_app', type: 'boolean', value: true, description: 'Build the client')
|
option('compile_app', type: 'boolean', value: true, description: 'Build the client')
|
||||||
option('build_server', type: 'boolean', value: true, description: 'Build the server')
|
option('compile_server', type: 'boolean', value: true, description: 'Build the server')
|
||||||
option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux')
|
option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux')
|
||||||
option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)')
|
option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)')
|
||||||
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
|
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
|
||||||
option('portable', type: 'boolean', description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable')
|
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable')
|
||||||
option('skip_frames', type: 'boolean', value: true, description: 'Always display the most recent frame')
|
|
||||||
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')
|
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ public final class Server {
|
|||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
@SuppressWarnings("checkstyle:MagicNumber")
|
||||||
private static Options createOptions(String... args) {
|
private static Options createOptions(String... args) {
|
||||||
if (args.length != 6) {
|
if (args.length != 6) {
|
||||||
throw new IllegalArgumentException("Expecting 5 parameters");
|
throw new IllegalArgumentException("Expecting 6 parameters");
|
||||||
}
|
}
|
||||||
|
|
||||||
Options options = new Options();
|
Options options = new Options();
|
||||||
@@ -116,7 +116,7 @@ public final class Server {
|
|||||||
try {
|
try {
|
||||||
new File(SERVER_PATH).delete();
|
new File(SERVER_PATH).delete();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Ln.e("Cannot unlink server", e);
|
Ln.e("Could not unlink server", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class StatusBarManager {
|
|||||||
try {
|
try {
|
||||||
expandNotificationsPanelMethod.invoke(manager);
|
expandNotificationsPanelMethod.invoke(manager);
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||||
Ln.e("Cannot invoke ServiceBarManager.expandNotificationsPanel()", e);
|
Ln.e("Could not invoke ServiceBarManager.expandNotificationsPanel()", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ public class StatusBarManager {
|
|||||||
try {
|
try {
|
||||||
collapsePanelsMethod.invoke(manager);
|
collapsePanelsMethod.invoke(manager);
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||||
Ln.e("Cannot invoke ServiceBarManager.collapsePanels()", e);
|
Ln.e("Could not invoke ServiceBarManager.collapsePanels()", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.genymobile.scrcpy.wrappers;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
|
|
||||||
@@ -77,7 +78,12 @@ public final class SurfaceControl {
|
|||||||
|
|
||||||
public static IBinder getBuiltInDisplay(int builtInDisplayId) {
|
public static IBinder getBuiltInDisplay(int builtInDisplayId) {
|
||||||
try {
|
try {
|
||||||
return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId);
|
// the method signature has changed in Android Q
|
||||||
|
// <https://github.com/Genymobile/scrcpy/issues/586>
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId);
|
||||||
|
}
|
||||||
|
return (IBinder) CLASS.getMethod("getPhysicalDisplayToken", long.class).invoke(null, builtInDisplayId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public class DeviceMessageWriterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerializeClipboard() throws IOException {
|
||||||
|
DeviceMessageWriter writer = new DeviceMessageWriter();
|
||||||
|
|
||||||
|
String text = "aéûoç";
|
||||||
|
byte[] data = text.getBytes(StandardCharsets.UTF_8);
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
|
dos.writeByte(DeviceMessage.TYPE_CLIPBOARD);
|
||||||
|
dos.writeShort(data.length);
|
||||||
|
dos.write(data);
|
||||||
|
|
||||||
|
byte[] expected = bos.toByteArray();
|
||||||
|
|
||||||
|
DeviceMessage msg = DeviceMessage.createClipboard(text);
|
||||||
|
bos = new ByteArrayOutputStream();
|
||||||
|
writer.writeTo(msg, bos);
|
||||||
|
|
||||||
|
byte[] actual = bos.toByteArray();
|
||||||
|
|
||||||
|
Assert.assertArrayEquals(expected, actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user