Compare commits
39 Commits
v1.9
...
buffered_r
Author | SHA1 | Date | |
---|---|---|---|
d113b6ef61 | |||
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
|
||||
|
||||
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
|
||||
the following files to a directory accessible from your `PATH`:
|
||||
@ -40,10 +40,10 @@ Install the required packages from your package manager.
|
||||
|
||||
```bash
|
||||
# runtime dependencies
|
||||
sudo apt install ffmpeg libsdl2-2.0.0
|
||||
sudo apt install ffmpeg libsdl2-2.0-0
|
||||
|
||||
# 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 \
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
- [`scrcpy-server-v1.8.jar`][direct-scrcpy-server]
|
||||
_(SHA-256: 839055ef905903bf98ead1b9b8a127fe402b39ad657a81f9a914b2dbcb2ce5c0)_
|
||||
- [`scrcpy-server-v1.9.jar`][direct-scrcpy-server]
|
||||
_(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
|
||||
configuration:
|
||||
|
1
LICENSE
1
LICENSE
@ -188,6 +188,7 @@
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2019 Romain Vimont
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -44,7 +44,7 @@ clean:
|
||||
build-server:
|
||||
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
|
||||
meson "$(SERVER_BUILD_DIR)" \
|
||||
--buildtype release -Dbuild_app=false )
|
||||
--buildtype release -Dcompile_app=false )
|
||||
ninja -C "$(SERVER_BUILD_DIR)"
|
||||
|
||||
prepare-deps-win32:
|
||||
@ -56,7 +56,7 @@ build-win32: prepare-deps-win32
|
||||
--cross-file cross_win32.txt \
|
||||
--buildtype release --strip -Db_lto=true \
|
||||
-Dcrossbuild_windows=true \
|
||||
-Dbuild_server=false \
|
||||
-Dcompile_server=false \
|
||||
-Dportable=true )
|
||||
ninja -C "$(WIN32_BUILD_DIR)"
|
||||
|
||||
@ -66,7 +66,7 @@ build-win32-noconsole: prepare-deps-win32
|
||||
--cross-file cross_win32.txt \
|
||||
--buildtype release --strip -Db_lto=true \
|
||||
-Dcrossbuild_windows=true \
|
||||
-Dbuild_server=false \
|
||||
-Dcompile_server=false \
|
||||
-Dwindows_noconsole=true \
|
||||
-Dportable=true )
|
||||
ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)"
|
||||
@ -80,7 +80,7 @@ build-win64: prepare-deps-win64
|
||||
--cross-file cross_win64.txt \
|
||||
--buildtype release --strip -Db_lto=true \
|
||||
-Dcrossbuild_windows=true \
|
||||
-Dbuild_server=false \
|
||||
-Dcompile_server=false \
|
||||
-Dportable=true )
|
||||
ninja -C "$(WIN64_BUILD_DIR)"
|
||||
|
||||
@ -90,7 +90,7 @@ build-win64-noconsole: prepare-deps-win64
|
||||
--cross-file cross_win64.txt \
|
||||
--buildtype release --strip -Db_lto=true \
|
||||
-Dcrossbuild_windows=true \
|
||||
-Dbuild_server=false \
|
||||
-Dcompile_server=false \
|
||||
-Dwindows_noconsole=true \
|
||||
-Dportable=true )
|
||||
ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)"
|
||||
|
97
README.md
97
README.md
@ -1,15 +1,15 @@
|
||||
# scrcpy (v1.8)
|
||||
# scrcpy (v1.9)
|
||||
|
||||
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.
|
||||
It works on _GNU/Linux_, _Windows_ and _MacOS_.
|
||||
It works on _GNU/Linux_, _Windows_ and _macOS_.
|
||||
|
||||

|
||||
|
||||
|
||||
## 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).
|
||||
|
||||
@ -29,6 +29,12 @@ control it using keyboard and mouse.
|
||||
On Linux, you typically need to [build the app manually][BUILD]. Don't worry,
|
||||
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].
|
||||
|
||||
[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
|
||||
(including `adb`) are available:
|
||||
|
||||
- [`scrcpy-win32-v1.8.zip`][direct-win32]
|
||||
_(SHA-256: c0c29ed1c66deaa73bdadacd09e598aafb3a117929cf7a314cce1cc45e34de53)_
|
||||
- [`scrcpy-win64-v1.8.zip`][direct-win64]
|
||||
_(SHA-256: 9cc980d07bd8f036ae4e91d0bc6fc3281d7fa8f9752d4913b643c0fb72a19fb7)_
|
||||
- [`scrcpy-win32-v1.9.zip`][direct-win32]
|
||||
_(SHA-256: 3234f7fbcc26b9e399f50b5ca9ed085708954c87fda1b0dd32719d6e7dd861ef)_
|
||||
- [`scrcpy-win64-v1.9.zip`][direct-win64]
|
||||
_(SHA-256: 0088eca1811ea7c7ac350d636c8465b266e6c830bb268770ff88fddbb493077e)_
|
||||
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win32-v1.8.zip
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win64-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.9/scrcpy-win64-v1.9.zip
|
||||
|
||||
You can also [build the app manually][BUILD].
|
||||
|
||||
|
||||
### Mac OS
|
||||
### macOS
|
||||
|
||||
The application is available in [Homebrew]. Just install it:
|
||||
|
||||
@ -95,22 +101,22 @@ scrcpy --help
|
||||
### Reduce size
|
||||
|
||||
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
|
||||
scrcpy --max-size 1024
|
||||
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.
|
||||
|
||||
|
||||
### 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
|
||||
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.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
The target directory can be changed on start:
|
||||
|
||||
```bash
|
||||
scrcpy --push-target /sdcard/foo/bar/
|
||||
```
|
||||
|
||||
### Read-only
|
||||
|
||||
@ -283,42 +294,47 @@ latency), use:
|
||||
scrcpy --render-expired-frames
|
||||
```
|
||||
|
||||
### Custom window title
|
||||
|
||||
By default, the window title is the device model. It can be changed:
|
||||
|
||||
```bash
|
||||
scrcpy --window-title 'My device'
|
||||
```
|
||||
|
||||
|
||||
### Forward audio
|
||||
|
||||
Audio is not forwarded by _scrcpy_.
|
||||
Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only).
|
||||
|
||||
There is a limited solution using [AOA], implemented in the [`audio`] branch. If
|
||||
you are interested, see [issue 14].
|
||||
Also see [issue #14].
|
||||
|
||||
|
||||
[AOA]: https://source.android.com/devices/accessories/aoa2
|
||||
[`audio`]: https://github.com/Genymobile/scrcpy/commits/audio
|
||||
[issue 14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||
[USBaudio]: https://github.com/rom1v/usbaudio
|
||||
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||
|
||||
|
||||
## Shortcuts
|
||||
|
||||
| Action | Shortcut |
|
||||
| -------------------------------------- |:---------------------------- |
|
||||
| switch fullscreen mode | `Ctrl`+`f` |
|
||||
| resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` |
|
||||
| resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ |
|
||||
| click on `HOME` | `Ctrl`+`h` \| _Middle-click_ |
|
||||
| click on `BACK` | `Ctrl`+`b` \| _Right-click²_ |
|
||||
| click on `APP_SWITCH` | `Ctrl`+`s` |
|
||||
| click on `MENU` | `Ctrl`+`m` |
|
||||
| click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ (`Cmd`+`↑` on MacOS) |
|
||||
| click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on MacOS) |
|
||||
| click on `POWER` | `Ctrl`+`p` |
|
||||
| power on | _Right-click²_ |
|
||||
| turn device screen off (keep mirroring)| `Ctrl`+`o` |
|
||||
| expand notification panel | `Ctrl`+`n` |
|
||||
| collapse notification panel | `Ctrl`+`Shift`+`n` |
|
||||
| copy device clipboard to computer | `Ctrl`+`c` |
|
||||
| paste computer clipboard to device | `Ctrl`+`v` |
|
||||
| copy computer clipboard to device | `Ctrl`+`Shift+`v` |
|
||||
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
|
||||
| Switch fullscreen mode | `Ctrl`+`f` |
|
||||
| Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` |
|
||||
| Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ |
|
||||
| Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ |
|
||||
| Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ |
|
||||
| Click on `APP_SWITCH` | `Ctrl`+`s` |
|
||||
| Click on `MENU` | `Ctrl`+`m` |
|
||||
| Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ (`Cmd`+`↑` on macOS) |
|
||||
| Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on macOS) |
|
||||
| Click on `POWER` | `Ctrl`+`p` |
|
||||
| Power on | _Right-click²_ |
|
||||
| Turn device screen off (keep mirroring)| `Ctrl`+`o` |
|
||||
| Expand notification panel | `Ctrl`+`n` |
|
||||
| Collapse notification panel | `Ctrl`+`Shift`+`n` |
|
||||
| Copy device clipboard to computer | `Ctrl`+`c` |
|
||||
| Paste computer clipboard to device | `Ctrl`+`v` |
|
||||
| Copy computer clipboard to device | `Ctrl`+`Shift`+`v` |
|
||||
| Enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
|
||||
|
||||
_¹Double-click on black borders to remove them._
|
||||
_²Right-click turns the screen on if it was off, presses BACK otherwise._
|
||||
@ -369,6 +385,7 @@ Read the [developers page].
|
||||
## Licence
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2019 Romain Vimont
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -1,5 +1,6 @@
|
||||
src = [
|
||||
'src/main.c',
|
||||
'src/buffered_reader.c',
|
||||
'src/command.c',
|
||||
'src/control_msg.c',
|
||||
'src/controller.c',
|
||||
|
72
app/src/buffered_reader.c
Normal file
72
app/src/buffered_reader.c
Normal file
@ -0,0 +1,72 @@
|
||||
#include "buffered_reader.h"
|
||||
|
||||
#include <SDL2/SDL_assert.h>
|
||||
#include "log.h"
|
||||
|
||||
bool
|
||||
buffered_reader_init(struct buffered_reader *reader, socket_t socket,
|
||||
size_t bufsize) {
|
||||
reader->buf = SDL_malloc(bufsize);
|
||||
if (!reader->buf) {
|
||||
LOGC("Could not allocate buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
reader->socket = socket;
|
||||
reader->bufsize = bufsize;
|
||||
reader->offset = 0;
|
||||
reader->len = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
buffered_reader_destroy(struct buffered_reader *reader) {
|
||||
SDL_free(reader->buf);
|
||||
}
|
||||
|
||||
static ssize_t
|
||||
buffered_reader_fill(struct buffered_reader *reader) {
|
||||
SDL_assert(!reader->len);
|
||||
ssize_t r = net_recv(reader->socket, reader->buf, reader->bufsize);
|
||||
if (r > 0) {
|
||||
reader->offset = 0;
|
||||
reader->len = r;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
ssize_t
|
||||
buffered_reader_recv(struct buffered_reader *reader, void *buf, size_t count) {
|
||||
if (!reader->len) {
|
||||
// read from the socket
|
||||
ssize_t r = buffered_reader_fill(reader);
|
||||
if (r <= 0) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
size_t r = count < reader->len ? count : reader->len;
|
||||
memcpy(buf, reader->buf + reader->offset, r);
|
||||
reader->offset += r;
|
||||
reader->len -= r;
|
||||
return r;
|
||||
}
|
||||
|
||||
ssize_t
|
||||
buffered_reader_recv_all(struct buffered_reader *reader, void *buf,
|
||||
size_t count) {
|
||||
size_t done = 0;
|
||||
while (done < count) {
|
||||
ssize_t r = buffered_reader_recv(reader, buf, count - done);
|
||||
if (r <= 0) {
|
||||
// if there was some data, return them immediately
|
||||
return done ? done : r;
|
||||
}
|
||||
|
||||
done += r;
|
||||
buf += r;
|
||||
}
|
||||
|
||||
return done;
|
||||
}
|
29
app/src/buffered_reader.h
Normal file
29
app/src/buffered_reader.h
Normal file
@ -0,0 +1,29 @@
|
||||
#ifndef BUFFERED_READER_H
|
||||
#define BUFFERED_READER_H
|
||||
|
||||
#include "common.h"
|
||||
#include "net.h"
|
||||
|
||||
struct buffered_reader {
|
||||
socket_t socket;
|
||||
void *buf;
|
||||
size_t bufsize;
|
||||
size_t offset;
|
||||
size_t len;
|
||||
};
|
||||
|
||||
bool
|
||||
buffered_reader_init(struct buffered_reader *reader, socket_t socket,
|
||||
size_t bufsize);
|
||||
|
||||
void
|
||||
buffered_reader_destroy(struct buffered_reader *reader);
|
||||
|
||||
ssize_t
|
||||
buffered_reader_recv(struct buffered_reader *reader, void *buf, size_t count);
|
||||
|
||||
ssize_t
|
||||
buffered_reader_recv_all(struct buffered_reader *reader, void *buf,
|
||||
size_t count);
|
||||
|
||||
#endif
|
@ -91,7 +91,7 @@ run_controller(void *data) {
|
||||
bool ok = process_msg(controller, &msg);
|
||||
control_msg_destroy(&msg);
|
||||
if (!ok) {
|
||||
LOGD("Cannot write msg to socket");
|
||||
LOGD("Could not write msg to socket");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@
|
||||
#include "net.h"
|
||||
|
||||
#define DEVICE_NAME_FIELD_LENGTH 64
|
||||
#define DEVICE_SDCARD_PATH "/sdcard/"
|
||||
|
||||
// name must be at least DEVICE_NAME_FIELD_LENGTH bytes
|
||||
bool
|
||||
|
@ -5,17 +5,19 @@
|
||||
|
||||
#include "config.h"
|
||||
#include "command.h"
|
||||
#include "device.h"
|
||||
#include "lock_util.h"
|
||||
#include "log.h"
|
||||
|
||||
#define DEFAULT_PUSH_TARGET "/sdcard/"
|
||||
|
||||
static void
|
||||
file_handler_request_destroy(struct file_handler_request *req) {
|
||||
SDL_free(req->file);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@ -31,7 +33,7 @@ file_handler_init(struct file_handler *file_handler, const char *serial) {
|
||||
if (serial) {
|
||||
file_handler->serial = SDL_strdup(serial);
|
||||
if (!file_handler->serial) {
|
||||
LOGW("Cannot strdup serial");
|
||||
LOGW("Could not strdup serial");
|
||||
SDL_DestroyCond(file_handler->event_cond);
|
||||
SDL_DestroyMutex(file_handler->mutex);
|
||||
return false;
|
||||
@ -46,6 +48,8 @@ file_handler_init(struct file_handler *file_handler, const char *serial) {
|
||||
file_handler->stopped = false;
|
||||
file_handler->current_process = PROCESS_NONE;
|
||||
|
||||
file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -67,8 +71,8 @@ install_apk(const char *serial, const char *file) {
|
||||
}
|
||||
|
||||
static process_t
|
||||
push_file(const char *serial, const char *file) {
|
||||
return adb_push(serial, file, DEVICE_SDCARD_PATH);
|
||||
push_file(const char *serial, const char *file, const char *push_target) {
|
||||
return adb_push(serial, file, push_target);
|
||||
}
|
||||
|
||||
bool
|
||||
@ -124,7 +128,8 @@ run_file_handler(void *data) {
|
||||
process = install_apk(file_handler->serial, req.file);
|
||||
} else {
|
||||
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;
|
||||
mutex_unlock(file_handler->mutex);
|
||||
@ -137,9 +142,11 @@ run_file_handler(void *data) {
|
||||
}
|
||||
} else {
|
||||
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 {
|
||||
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);
|
||||
if (file_handler->current_process != PROCESS_NONE) {
|
||||
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);
|
||||
file_handler->current_process = PROCESS_NONE;
|
||||
|
@ -22,6 +22,7 @@ struct file_handler_request_queue CBUF(struct file_handler_request, 16);
|
||||
|
||||
struct file_handler {
|
||||
char *serial;
|
||||
const char *push_target;
|
||||
SDL_Thread *thread;
|
||||
SDL_mutex *mutex;
|
||||
SDL_cond *event_cond;
|
||||
@ -32,7 +33,8 @@ struct file_handler {
|
||||
};
|
||||
|
||||
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
|
||||
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) {
|
||||
msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN;
|
||||
if (!controller_push_msg(controller, &msg)) {
|
||||
LOGW("Cannot request 'inject %s (DOWN)'", name);
|
||||
LOGW("Could not request 'inject %s (DOWN)'", name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -55,7 +55,7 @@ send_keycode(struct controller *controller, enum android_keycode keycode,
|
||||
if (actions & ACTION_UP) {
|
||||
msg.inject_keycode.action = AKEY_EVENT_ACTION_UP;
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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) {
|
||||
char *text = SDL_GetClipboardText();
|
||||
if (!text) {
|
||||
LOGW("Cannot get clipboard text: %s", SDL_GetError());
|
||||
LOGW("Could not get clipboard text: %s", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
if (!*text) {
|
||||
@ -155,7 +155,7 @@ set_device_clipboard(struct controller *controller) {
|
||||
|
||||
if (!controller_push_msg(controller, &msg)) {
|
||||
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;
|
||||
|
||||
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) {
|
||||
char *text = SDL_GetClipboardText();
|
||||
if (!text) {
|
||||
LOGW("Cannot get clipboard text: %s", SDL_GetError());
|
||||
LOGW("Could not get clipboard text: %s", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
if (!*text) {
|
||||
@ -205,7 +205,7 @@ clipboard_paste(struct controller *controller) {
|
||||
msg.inject_text.text = text;
|
||||
if (!controller_push_msg(controller, &msg)) {
|
||||
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.inject_text.text = SDL_strdup(event->text);
|
||||
if (!msg.inject_text.text) {
|
||||
LOGW("Cannot strdup input text");
|
||||
LOGW("Could not strdup input text");
|
||||
return;
|
||||
}
|
||||
if (!controller_push_msg(input_manager->controller, &msg)) {
|
||||
SDL_free(msg.inject_text.text);
|
||||
LOGW("Cannot request 'inject text'");
|
||||
LOGW("Could not request 'inject text'");
|
||||
}
|
||||
}
|
||||
|
||||
@ -368,7 +368,7 @@ input_manager_process_key(struct input_manager *input_manager,
|
||||
struct control_msg msg;
|
||||
if (input_key_from_sdl_to_android(event, &msg)) {
|
||||
if (!controller_push_msg(controller, &msg)) {
|
||||
LOGW("Cannot request 'inject keycode'");
|
||||
LOGW("Could not request 'inject keycode'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -385,7 +385,7 @@ input_manager_process_mouse_motion(struct input_manager *input_manager,
|
||||
input_manager->screen->frame_size,
|
||||
&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 +431,7 @@ input_manager_process_mouse_button(struct input_manager *input_manager,
|
||||
input_manager->screen->frame_size,
|
||||
&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 +446,7 @@ input_manager_process_mouse_wheel(struct input_manager *input_manager,
|
||||
struct control_msg msg;
|
||||
if (mouse_wheel_from_sdl_to_android(event, position, &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 *crop;
|
||||
const char *record_filename;
|
||||
const char *window_title;
|
||||
const char *push_target;
|
||||
enum recorder_format record_format;
|
||||
bool fullscreen;
|
||||
bool no_control;
|
||||
@ -75,6 +77,11 @@ static void usage(const char *arg0) {
|
||||
" Set the TCP port the client listens on.\n"
|
||||
" Default is %d.\n"
|
||||
"\n"
|
||||
" --push-target path\n"
|
||||
" Set the target directory for pushing files to the device by\n"
|
||||
" drag & drop. It is passed as-is to \"adb push\".\n"
|
||||
" Default is \"/sdcard/\".\n"
|
||||
"\n"
|
||||
" -r, --record file.mp4\n"
|
||||
" Record screen to file.\n"
|
||||
" The format is determined by the -F/--record-format option if\n"
|
||||
@ -86,7 +93,7 @@ static void usage(const char *arg0) {
|
||||
" This flag forces to render all frames, at a cost of a\n"
|
||||
" possible increased latency.\n"
|
||||
"\n"
|
||||
" -s, --serial\n"
|
||||
" -s, --serial serial\n"
|
||||
" The device serial number. Mandatory only if several devices\n"
|
||||
" are connected to adb.\n"
|
||||
"\n"
|
||||
@ -103,6 +110,9 @@ static void usage(const char *arg0) {
|
||||
" -v, --version\n"
|
||||
" Print the version of scrcpy.\n"
|
||||
"\n"
|
||||
" --window-title text\n"
|
||||
" Set a custom window title.\n"
|
||||
"\n"
|
||||
"Shortcuts:\n"
|
||||
"\n"
|
||||
" Ctrl+f\n"
|
||||
@ -295,6 +305,8 @@ guess_record_format(const char *filename) {
|
||||
}
|
||||
|
||||
#define OPT_RENDER_EXPIRED_FRAMES 1000
|
||||
#define OPT_WINDOW_TITLE 1001
|
||||
#define OPT_PUSH_TARGET 1002
|
||||
|
||||
static bool
|
||||
parse_args(struct args *args, int argc, char *argv[]) {
|
||||
@ -308,6 +320,8 @@ parse_args(struct args *args, int argc, char *argv[]) {
|
||||
{"no-control", no_argument, NULL, 'n'},
|
||||
{"no-display", no_argument, NULL, 'N'},
|
||||
{"port", required_argument, NULL, 'p'},
|
||||
{"push-target", required_argument, NULL,
|
||||
OPT_PUSH_TARGET},
|
||||
{"record", required_argument, NULL, 'r'},
|
||||
{"record-format", required_argument, NULL, 'f'},
|
||||
{"render-expired-frames", no_argument, NULL,
|
||||
@ -316,6 +330,8 @@ parse_args(struct args *args, int argc, char *argv[]) {
|
||||
{"show-touches", no_argument, NULL, 't'},
|
||||
{"turn-screen-off", no_argument, NULL, 'S'},
|
||||
{"version", no_argument, NULL, 'v'},
|
||||
{"window-title", required_argument, NULL,
|
||||
OPT_WINDOW_TITLE},
|
||||
{NULL, 0, NULL, 0 },
|
||||
};
|
||||
int c;
|
||||
@ -378,6 +394,12 @@ parse_args(struct args *args, int argc, char *argv[]) {
|
||||
case OPT_RENDER_EXPIRED_FRAMES:
|
||||
args->render_expired_frames = true;
|
||||
break;
|
||||
case OPT_WINDOW_TITLE:
|
||||
args->window_title = optarg;
|
||||
break;
|
||||
case OPT_PUSH_TARGET:
|
||||
args->push_target = optarg;
|
||||
break;
|
||||
default:
|
||||
// getopt prints the error message on stderr
|
||||
return false;
|
||||
@ -414,6 +436,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;
|
||||
}
|
||||
|
||||
@ -429,6 +456,8 @@ main(int argc, char *argv[]) {
|
||||
.serial = NULL,
|
||||
.crop = NULL,
|
||||
.record_filename = NULL,
|
||||
.window_title = NULL,
|
||||
.push_target = NULL,
|
||||
.record_format = 0,
|
||||
.help = false,
|
||||
.version = false,
|
||||
@ -456,6 +485,8 @@ main(int argc, char *argv[]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOGI("scrcpy " SCRCPY_VERSION " <https://github.com/Genymobile/scrcpy>");
|
||||
|
||||
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
|
||||
av_register_all();
|
||||
#endif
|
||||
@ -473,6 +504,8 @@ main(int argc, char *argv[]) {
|
||||
.crop = args.crop,
|
||||
.port = args.port,
|
||||
.record_filename = args.record_filename,
|
||||
.window_title = args.window_title,
|
||||
.push_target = args.push_target,
|
||||
.record_format = args.record_format,
|
||||
.max_size = args.max_size,
|
||||
.bit_rate = args.bit_rate,
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
#include "compat.h"
|
||||
#include "config.h"
|
||||
#include "lock_util.h"
|
||||
#include "log.h"
|
||||
|
||||
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
|
||||
@ -26,6 +27,82 @@ find_muxer(const char *name) {
|
||||
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
|
||||
recorder_init(struct recorder *recorder,
|
||||
const char *filename,
|
||||
@ -33,10 +110,28 @@ recorder_init(struct recorder *recorder,
|
||||
struct size declared_frame_size) {
|
||||
recorder->filename = SDL_strdup(filename);
|
||||
if (!recorder->filename) {
|
||||
LOGE("Cannot strdup filename");
|
||||
LOGE("Could not strdup filename");
|
||||
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->declared_frame_size = declared_frame_size;
|
||||
recorder->header_written = false;
|
||||
@ -46,6 +141,8 @@ recorder_init(struct recorder *recorder,
|
||||
|
||||
void
|
||||
recorder_destroy(struct recorder *recorder) {
|
||||
SDL_DestroyCond(recorder->queue_cond);
|
||||
SDL_DestroyMutex(recorder->mutex);
|
||||
SDL_free(recorder->filename);
|
||||
}
|
||||
|
||||
@ -119,12 +216,17 @@ recorder_close(struct recorder *recorder) {
|
||||
int ret = av_write_trailer(recorder->ctx);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to write trailer to %s", recorder->filename);
|
||||
recorder->failed = true;
|
||||
}
|
||||
avio_close(recorder->ctx->pb);
|
||||
avformat_free_context(recorder->ctx);
|
||||
|
||||
const char *format_name = recorder_get_format_name(recorder->format);
|
||||
LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
|
||||
if (recorder->failed) {
|
||||
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
|
||||
@ -133,7 +235,7 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
|
||||
|
||||
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
|
||||
if (!extradata) {
|
||||
LOGC("Cannot allocate extradata");
|
||||
LOGC("Could not allocate extradata");
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -151,9 +253,6 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
|
||||
int ret = avformat_write_header(recorder->ctx, NULL);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to write header to %s", recorder->filename);
|
||||
SDL_free(extradata);
|
||||
avio_closep(&recorder->ctx->pb);
|
||||
avformat_free_context(recorder->ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -169,13 +268,110 @@ recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) {
|
||||
bool
|
||||
recorder_write(struct recorder *recorder, AVPacket *packet) {
|
||||
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);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
recorder->header_written = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (packet->pts == AV_NOPTS_VALUE) {
|
||||
// ignore config packets
|
||||
return true;
|
||||
}
|
||||
|
||||
recorder_rescale_packet(recorder, packet);
|
||||
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 <libavformat/avformat.h>
|
||||
#include <SDL2/SDL_mutex.h>
|
||||
#include <SDL2/SDL_thread.h>
|
||||
|
||||
#include "common.h"
|
||||
|
||||
@ -11,12 +13,29 @@ enum recorder_format {
|
||||
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 {
|
||||
char *filename;
|
||||
enum recorder_format format;
|
||||
AVFormatContext *ctx;
|
||||
struct size declared_frame_size;
|
||||
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
|
||||
@ -33,6 +52,15 @@ void
|
||||
recorder_close(struct recorder *recorder);
|
||||
|
||||
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
|
||||
|
@ -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);
|
||||
if (!local_fmt) {
|
||||
LOGC("Cannot allocate string");
|
||||
LOGC("Could not allocate string");
|
||||
return;
|
||||
}
|
||||
// strcpy is safe here, the destination is large enough
|
||||
@ -277,7 +277,6 @@ scrcpy(const struct scrcpy_options *options) {
|
||||
.local_port = options->port,
|
||||
.max_size = options->max_size,
|
||||
.bit_rate = options->bit_rate,
|
||||
.send_frame_meta = record,
|
||||
.control = options->control,
|
||||
};
|
||||
if (!server_start(&server, options->serial, ¶ms)) {
|
||||
@ -298,6 +297,7 @@ scrcpy(const struct scrcpy_options *options) {
|
||||
bool video_buffer_initialized = false;
|
||||
bool file_handler_initialized = false;
|
||||
bool recorder_initialized = false;
|
||||
bool stream_initialized = false;
|
||||
bool stream_started = false;
|
||||
bool controller_initialized = false;
|
||||
bool controller_started = false;
|
||||
@ -334,7 +334,8 @@ scrcpy(const struct scrcpy_options *options) {
|
||||
video_buffer_initialized = true;
|
||||
|
||||
if (options->control) {
|
||||
if (!file_handler_init(&file_handler, server.serial)) {
|
||||
if (!file_handler_init(&file_handler, server.serial,
|
||||
options->push_target)) {
|
||||
goto end;
|
||||
}
|
||||
file_handler_initialized = true;
|
||||
@ -358,7 +359,10 @@ scrcpy(const struct scrcpy_options *options) {
|
||||
|
||||
av_log_set_callback(av_log_callback);
|
||||
|
||||
stream_init(&stream, server.video_socket, dec, rec);
|
||||
if (!stream_init(&stream, server.video_socket, dec, rec)) {
|
||||
goto end;
|
||||
}
|
||||
stream_initialized = true;
|
||||
|
||||
// now we consumed the header values, the socket receives the video stream
|
||||
// start the stream
|
||||
@ -380,7 +384,10 @@ scrcpy(const struct scrcpy_options *options) {
|
||||
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)) {
|
||||
goto end;
|
||||
}
|
||||
@ -391,7 +398,7 @@ scrcpy(const struct scrcpy_options *options) {
|
||||
msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF;
|
||||
|
||||
if (!controller_push_msg(&controller, &msg)) {
|
||||
LOGW("Cannot request 'set screen power mode'");
|
||||
LOGW("Could not request 'set screen power mode'");
|
||||
}
|
||||
}
|
||||
|
||||
@ -434,6 +441,9 @@ end:
|
||||
if (stream_started) {
|
||||
stream_join(&stream);
|
||||
}
|
||||
if (stream_initialized) {
|
||||
stream_destroy(&stream);
|
||||
}
|
||||
if (controller_started) {
|
||||
controller_join(&controller);
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ struct scrcpy_options {
|
||||
const char *serial;
|
||||
const char *crop;
|
||||
const char *record_filename;
|
||||
const char *window_title;
|
||||
const char *push_target;
|
||||
enum recorder_format record_format;
|
||||
uint16_t port;
|
||||
uint16_t max_size;
|
||||
|
@ -85,7 +85,7 @@ get_optimal_size(struct size current_size, struct size frame_size) {
|
||||
uint32_t h;
|
||||
|
||||
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;
|
||||
h = current_size.height;
|
||||
} else {
|
||||
@ -134,7 +134,7 @@ create_texture(SDL_Renderer *renderer, struct size frame_size) {
|
||||
}
|
||||
|
||||
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) {
|
||||
screen->frame_size = frame_size;
|
||||
|
||||
@ -152,7 +152,7 @@ screen_init_rendering(struct screen *screen, const char *device_name,
|
||||
#endif
|
||||
}
|
||||
|
||||
screen->window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED,
|
||||
screen->window = SDL_CreateWindow(window_title, SDL_WINDOWPOS_UNDEFINED,
|
||||
SDL_WINDOWPOS_UNDEFINED,
|
||||
window_size.width, window_size.height,
|
||||
window_flags);
|
||||
|
@ -44,7 +44,7 @@ screen_init(struct screen *screen);
|
||||
|
||||
// initialize screen, create window, renderer and texture (window is hidden)
|
||||
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);
|
||||
|
||||
// show the window
|
||||
|
@ -15,7 +15,7 @@
|
||||
#define SOCKET_NAME "scrcpy"
|
||||
#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
|
||||
|
||||
static const char *
|
||||
@ -35,7 +35,7 @@ get_server_path(void) {
|
||||
// use scrcpy-server.jar in the same directory as the executable
|
||||
char *executable_path = get_executable_path();
|
||||
if (!executable_path) {
|
||||
LOGE("Cannot get executable path, "
|
||||
LOGE("Could not get executable path, "
|
||||
"using " SERVER_FILENAME " from current directory");
|
||||
// not found, use current directory
|
||||
return SERVER_FILENAME;
|
||||
@ -47,7 +47,7 @@ get_server_path(void) {
|
||||
size_t len = dirlen + 1 + sizeof(SERVER_FILENAME);
|
||||
char *server_path = SDL_malloc(len);
|
||||
if (!server_path) {
|
||||
LOGE("Cannot alloc server path string, "
|
||||
LOGE("Could not alloc server path string, "
|
||||
"using " SERVER_FILENAME " from current directory");
|
||||
SDL_free(executable_path);
|
||||
return SERVER_FILENAME;
|
||||
@ -130,7 +130,7 @@ execute_server(struct server *server, const struct server_params *params) {
|
||||
bit_rate_string,
|
||||
server->tunnel_forward ? "true" : "false",
|
||||
params->crop ? params->crop : "-",
|
||||
params->send_frame_meta ? "true" : "false",
|
||||
"true", // always send frame meta (packet boundaries + timestamp)
|
||||
params->control ? "true" : "false",
|
||||
};
|
||||
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
|
||||
if (net_recv(socket, &byte, 1) != 1) {
|
||||
// the server is not listening yet behind the adb tunnel
|
||||
net_close(socket);
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
return socket;
|
||||
@ -181,7 +182,7 @@ close_socket(socket_t *socket) {
|
||||
SDL_assert(*socket != INVALID_SOCKET);
|
||||
net_shutdown(*socket, SHUT_RDWR);
|
||||
if (!net_close(*socket)) {
|
||||
LOGW("Cannot close socket");
|
||||
LOGW("Could not close socket");
|
||||
return;
|
||||
}
|
||||
*socket = INVALID_SOCKET;
|
||||
@ -305,7 +306,7 @@ server_stop(struct server *server) {
|
||||
SDL_assert(server->process != PROCESS_NONE);
|
||||
|
||||
if (!cmd_terminate(server->process)) {
|
||||
LOGW("Cannot terminate server");
|
||||
LOGW("Could not terminate server");
|
||||
}
|
||||
|
||||
cmd_simple_wait(server->process, NULL); // ignore exit code
|
||||
|
@ -34,7 +34,6 @@ struct server_params {
|
||||
uint16_t local_port;
|
||||
uint16_t max_size;
|
||||
uint32_t bit_rate;
|
||||
bool send_frame_meta;
|
||||
bool control;
|
||||
};
|
||||
|
||||
|
373
app/src/stream.c
373
app/src/stream.c
@ -17,59 +17,13 @@
|
||||
#include "log.h"
|
||||
#include "recorder.h"
|
||||
|
||||
#define BUFSIZE 0x10000
|
||||
#define STREAM_BUFSIZE 0x10000
|
||||
|
||||
#define HEADER_SIZE 12
|
||||
#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
|
||||
receiver_state_push_meta(struct receiver_state *state, uint64_t pts) {
|
||||
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;
|
||||
|
||||
stream_recv_packet(struct stream *stream, AVPacket *packet) {
|
||||
// The video stream contains raw packets, without time information. When we
|
||||
// record, we retrieve the timestamps separately, from a "meta" header
|
||||
// added by the server before each raw packet.
|
||||
@ -82,60 +36,31 @@ read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) {
|
||||
//
|
||||
// It is followed by <packet_size> bytes containing the packet/frame.
|
||||
|
||||
if (!state->remaining) {
|
||||
#define HEADER_SIZE 12
|
||||
uint8_t header[HEADER_SIZE];
|
||||
ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE);
|
||||
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);
|
||||
}
|
||||
uint8_t header[HEADER_SIZE];
|
||||
ssize_t r =
|
||||
buffered_reader_recv_all(&stream->buffered_reader, header, HEADER_SIZE);
|
||||
if (r < HEADER_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
buf_size = state->remaining;
|
||||
if (av_new_packet(packet, len)) {
|
||||
LOGE("Could not allocate packet");
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
r = buffered_reader_recv_all(&stream->buffered_reader, packet->data, len);
|
||||
if (r < len) {
|
||||
av_packet_unref(packet);
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_assert(state->remaining >= r);
|
||||
state->remaining -= r;
|
||||
packet->pts = pts != NO_PTS ? pts : AV_NOPTS_VALUE;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
@ -145,128 +70,221 @@ notify_stopped(void) {
|
||||
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
|
||||
run_stream(void *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);
|
||||
if (!codec) {
|
||||
LOGE("H.264 decoder not found");
|
||||
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)) {
|
||||
LOGE("Could not open decoder");
|
||||
goto finally_close_input;
|
||||
goto finally_free_codec_ctx;
|
||||
}
|
||||
|
||||
if (stream->recorder && !recorder_open(stream->recorder, codec)) {
|
||||
LOGE("Could not open recorder");
|
||||
goto finally_close_input;
|
||||
if (stream->recorder) {
|
||||
if (!recorder_open(stream->recorder, codec)) {
|
||||
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;
|
||||
av_init_packet(&packet);
|
||||
packet.data = NULL;
|
||||
packet.size = 0;
|
||||
stream->parser = av_parser_init(AV_CODEC_ID_H264);
|
||||
if (!stream->parser) {
|
||||
LOGE("Could not initialize parser");
|
||||
goto finally_stop_and_join_recorder;
|
||||
}
|
||||
|
||||
while (!av_read_frame(format_ctx, &packet)) {
|
||||
if (SDL_AtomicGet(&stream->stopped)) {
|
||||
// if the stream is stopped, the socket had been shutdown, so the
|
||||
// last packet is probably corrupted (but not detected as such by
|
||||
// FFmpeg) and will not be decoded correctly
|
||||
av_packet_unref(&packet);
|
||||
goto quit;
|
||||
}
|
||||
if (stream->decoder && !decoder_push(stream->decoder, &packet)) {
|
||||
av_packet_unref(&packet);
|
||||
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;
|
||||
}
|
||||
// We must only pass complete frames to av_parser_parse2()!
|
||||
// It's more complicated, but this allows to reduce the latency by 1 frame!
|
||||
stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
|
||||
|
||||
for (;;) {
|
||||
AVPacket packet;
|
||||
bool ok = stream_recv_packet(stream, &packet);
|
||||
if (!ok) {
|
||||
// end of stream
|
||||
break;
|
||||
}
|
||||
|
||||
ok = stream_push_packet(stream, &packet);
|
||||
av_packet_unref(&packet);
|
||||
|
||||
if (avio_ctx->eof_reached) {
|
||||
if (!ok) {
|
||||
// cannot process packet (error already logged)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
recorder_close(stream->recorder);
|
||||
}
|
||||
finally_close_input:
|
||||
avformat_close_input(&format_ctx);
|
||||
finally_free_avio_ctx:
|
||||
av_free(avio_ctx->buffer);
|
||||
av_free(avio_ctx);
|
||||
finally_free_format_ctx:
|
||||
avformat_free_context(format_ctx);
|
||||
finally_close_decoder:
|
||||
if (stream->decoder) {
|
||||
decoder_close(stream->decoder);
|
||||
}
|
||||
finally_free_codec_ctx:
|
||||
avcodec_free_context(&stream->codec_ctx);
|
||||
end:
|
||||
notify_stopped();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
bool
|
||||
stream_init(struct stream *stream, socket_t socket,
|
||||
struct decoder *decoder, struct recorder *recorder) {
|
||||
if (!buffered_reader_init(&stream->buffered_reader, socket,
|
||||
STREAM_BUFSIZE)) {
|
||||
return false;
|
||||
}
|
||||
stream->socket = socket;
|
||||
stream->decoder = decoder,
|
||||
stream->recorder = recorder;
|
||||
SDL_AtomicSet(&stream->stopped, 0);
|
||||
stream->has_pending = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
stream_destroy(struct stream *stream) {
|
||||
buffered_reader_destroy(&stream->buffered_reader);
|
||||
}
|
||||
|
||||
bool
|
||||
@ -283,7 +301,6 @@ stream_start(struct stream *stream) {
|
||||
|
||||
void
|
||||
stream_stop(struct stream *stream) {
|
||||
SDL_AtomicSet(&stream->stopped, 1);
|
||||
if (stream->decoder) {
|
||||
decoder_interrupt(stream->decoder);
|
||||
}
|
||||
|
@ -3,36 +3,37 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <SDL2/SDL_atomic.h>
|
||||
#include <SDL2/SDL_thread.h>
|
||||
|
||||
#include "buffered_reader.h"
|
||||
#include "net.h"
|
||||
|
||||
struct video_buffer;
|
||||
|
||||
struct frame_meta {
|
||||
uint64_t pts;
|
||||
struct frame_meta *next;
|
||||
};
|
||||
|
||||
struct stream {
|
||||
socket_t socket;
|
||||
struct buffered_reader buffered_reader;
|
||||
struct video_buffer *video_buffer;
|
||||
SDL_Thread *thread;
|
||||
SDL_atomic_t stopped;
|
||||
struct decoder *decoder;
|
||||
struct recorder *recorder;
|
||||
struct receiver_state {
|
||||
// meta (in order) for frames not consumed yet
|
||||
struct frame_meta *frame_meta_queue;
|
||||
size_t remaining; // remaining bytes to receive for the current frame
|
||||
} receiver_state;
|
||||
AVCodecContext *codec_ctx;
|
||||
AVCodecParserContext *parser;
|
||||
// successive packets may need to be concatenated, until a non-config
|
||||
// packet is available
|
||||
bool has_pending;
|
||||
AVPacket pending;
|
||||
};
|
||||
|
||||
void
|
||||
bool
|
||||
stream_init(struct stream *stream, socket_t socket,
|
||||
struct decoder *decoder, struct recorder *recorder);
|
||||
|
||||
void
|
||||
stream_destroy(struct stream *stream);
|
||||
|
||||
bool
|
||||
stream_start(struct stream *stream);
|
||||
|
||||
|
@ -94,7 +94,7 @@ cmd_simple_wait(pid_t pid, int *exit_code) {
|
||||
int status;
|
||||
int code;
|
||||
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;
|
||||
} else {
|
||||
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);
|
||||
if (!wide) {
|
||||
LOGC("Cannot allocate wide char string");
|
||||
LOGC("Could not allocate wide char string");
|
||||
return PROCESS_ERROR_GENERIC;
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
|
||||
DWORD code;
|
||||
if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0
|
||||
|| !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
|
||||
}
|
||||
if (exit_code) {
|
||||
|
@ -3,11 +3,11 @@ project('scrcpy', 'c',
|
||||
meson_version: '>= 0.37',
|
||||
default_options: 'c_std=c11')
|
||||
|
||||
if get_option('build_app')
|
||||
if get_option('compile_app')
|
||||
subdir('app')
|
||||
endif
|
||||
|
||||
if get_option('build_server')
|
||||
if get_option('compile_server')
|
||||
subdir('server')
|
||||
endif
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
option('build_app', type: 'boolean', value: true, description: 'Build the client')
|
||||
option('build_server', type: 'boolean', value: true, description: 'Build the server')
|
||||
option('compile_app', type: 'boolean', value: true, description: 'Build the client')
|
||||
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('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('portable', type: 'boolean', 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('portable', type: 'boolean', value: false, description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable')
|
||||
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')
|
||||
|
@ -68,7 +68,7 @@ public final class Server {
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
private static Options createOptions(String... args) {
|
||||
if (args.length != 6) {
|
||||
throw new IllegalArgumentException("Expecting 5 parameters");
|
||||
throw new IllegalArgumentException("Expecting 6 parameters");
|
||||
}
|
||||
|
||||
Options options = new Options();
|
||||
@ -116,7 +116,7 @@ public final class Server {
|
||||
try {
|
||||
new File(SERVER_PATH).delete();
|
||||
} catch (Exception e) {
|
||||
Ln.e("Cannot unlink server", e);
|
||||
Ln.e("Could not unlink server", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ public class StatusBarManager {
|
||||
try {
|
||||
expandNotificationsPanelMethod.invoke(manager);
|
||||
} 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 {
|
||||
collapsePanelsMethod.invoke(manager);
|
||||
} 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.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.view.Surface;
|
||||
|
||||
@ -77,7 +78,12 @@ public final class SurfaceControl {
|
||||
|
||||
public static IBinder getBuiltInDisplay(int builtInDisplayId) {
|
||||
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) {
|
||||
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