mirror of
https://github.com/Genymobile/scrcpy.git
synced 2026-03-12 07:04:28 +01:00
Compare commits
88 Commits
gamepad.dr
...
virtual_di
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a2b929aac | ||
|
|
9434718970 | ||
|
|
6ddcc98663 | ||
|
|
19178e0df9 | ||
|
|
064670ab4c | ||
|
|
ff9fb5994d | ||
|
|
a36de26969 | ||
|
|
281fcc7052 | ||
|
|
65fc53eace | ||
|
|
a6f74d72f5 | ||
|
|
e724ff4349 | ||
|
|
79014143b9 | ||
|
|
c0a6432967 | ||
|
|
ec602a0334 | ||
|
|
7a9ea5c66f | ||
|
|
d92b7a6024 | ||
|
|
0bb3955b95 | ||
|
|
62776fb261 | ||
|
|
10f60054ac | ||
|
|
42fb947780 | ||
|
|
2e7a15a998 | ||
|
|
a7e61fb871 | ||
|
|
0cc6f6aa09 | ||
|
|
f69ac40534 | ||
|
|
665ccb32f5 | ||
|
|
292adf294d | ||
|
|
f9f3bfabe3 | ||
|
|
6d23a389ca | ||
|
|
337901368e | ||
|
|
4cc4abdcc8 | ||
|
|
befc0fac5b | ||
|
|
f01a622ead | ||
|
|
0ba430a462 | ||
|
|
91d40c7548 | ||
|
|
9f3d51106d | ||
|
|
bf2b679e70 | ||
|
|
7f250dd669 | ||
|
|
68e27c7357 | ||
|
|
c4febd55eb | ||
|
|
f9d1a333a0 | ||
|
|
64a25f6e9d | ||
|
|
5fe884276b | ||
|
|
3e68244dd3 | ||
|
|
a34a62ca4b | ||
|
|
a59c6df4b7 | ||
|
|
f4d1e49ad9 | ||
|
|
4565f36ee6 | ||
|
|
c8479fe8bf | ||
|
|
de8455400c | ||
|
|
1f5be743b4 | ||
|
|
222916eebe | ||
|
|
6c707ad8a3 | ||
|
|
d748ac75e6 | ||
|
|
6f0c9eba9b | ||
|
|
f6219d2640 | ||
|
|
6e9b0d7d4c | ||
|
|
3e9c89c535 | ||
|
|
9af3bacdd6 | ||
|
|
2dd02ebb80 | ||
|
|
dad04bf138 | ||
|
|
1afc8ca368 | ||
|
|
785099b74d | ||
|
|
08da2e068e | ||
|
|
49c8ca34fd | ||
|
|
a84b0dfd0c | ||
|
|
cbf5db85c1 | ||
|
|
72ee195693 | ||
|
|
8620d06741 | ||
|
|
e9240f6804 | ||
|
|
e9b32d8a52 | ||
|
|
ce4e1fc420 | ||
|
|
e8f02685e9 | ||
|
|
4a6b335f7d | ||
|
|
90ee0062cb | ||
|
|
e03888d587 | ||
|
|
8453e3ba7d | ||
|
|
145a9468fd | ||
|
|
1d713d7598 | ||
|
|
265a15e0b1 | ||
|
|
6451ad271a | ||
|
|
bec3321fff | ||
|
|
dea1fe3386 | ||
|
|
a7cae59578 | ||
|
|
f089ea67e1 | ||
|
|
63ced79842 | ||
|
|
33a8c39beb | ||
|
|
903a5aaaf5 | ||
|
|
21e2e2606e |
11
README.md
11
README.md
@@ -2,7 +2,7 @@
|
||||
source for the project. Do not download releases from random websites, even if
|
||||
their name contains `scrcpy`.**
|
||||
|
||||
# scrcpy (v2.6.1)
|
||||
# scrcpy (v2.7)
|
||||
|
||||
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||
|
||||
@@ -37,6 +37,7 @@ Its features include:
|
||||
- [camera mirroring](doc/camera.md) (Android 12+)
|
||||
- [mirroring as a webcam (V4L2)](doc/v4l2.md) (Linux-only)
|
||||
- physical [keyboard][hid-keyboard] and [mouse][hid-mouse] simulation (HID)
|
||||
- [gamepad](doc/gamepad.md) support
|
||||
- [OTG mode](doc/otg.md)
|
||||
- and more…
|
||||
|
||||
@@ -111,6 +112,13 @@ Here are just some common examples.
|
||||
scrcpy --otg
|
||||
```
|
||||
|
||||
- Control the device using gamepad controllers plugged into the computer:
|
||||
|
||||
```bash
|
||||
scrcpy --gamepad=uhid
|
||||
scrcpy -G # short version
|
||||
```
|
||||
|
||||
## User documentation
|
||||
|
||||
The application provides a lot of features and configuration options. They are
|
||||
@@ -122,6 +130,7 @@ documented in the following pages:
|
||||
- [Control](doc/control.md)
|
||||
- [Keyboard](doc/keyboard.md)
|
||||
- [Mouse](doc/mouse.md)
|
||||
- [Gamepad](doc/gamepad.md)
|
||||
- [Device](doc/device.md)
|
||||
- [Window](doc/window.md)
|
||||
- [Recording](doc/recording.md)
|
||||
|
||||
@@ -26,6 +26,8 @@ _scrcpy() {
|
||||
-e --select-tcpip
|
||||
-f --fullscreen
|
||||
--force-adb-forward
|
||||
-G
|
||||
--gamepad=
|
||||
-h --help
|
||||
-K
|
||||
--keyboard=
|
||||
@@ -127,6 +129,10 @@ _scrcpy() {
|
||||
COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--gamepad)
|
||||
COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--orientation|--display-orientation)
|
||||
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
|
||||
return
|
||||
|
||||
@@ -33,8 +33,10 @@ arguments=(
|
||||
{-e,--select-tcpip}'[Use TCP/IP device]'
|
||||
{-f,--fullscreen}'[Start in fullscreen]'
|
||||
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
|
||||
'-G[Use UHID/AOA gamepad (same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode)]'
|
||||
'--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)'
|
||||
{-h,--help}'[Print the help]'
|
||||
'-K[Use UHID keyboard (same as --keyboard=uhid)]'
|
||||
'-K[Use UHID/AOA keyboard (same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode)]'
|
||||
'--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)'
|
||||
'--kill-adb-on-close[Kill adb when scrcpy terminates]'
|
||||
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
|
||||
@@ -44,7 +46,7 @@ arguments=(
|
||||
'--list-encoders[List video and audio encoders available on the device]'
|
||||
'--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)'
|
||||
{-m,--max-size=}'[Limit both the width and height of the video to value]'
|
||||
'-M[Use UHID mouse (same as --mouse=uhid)]'
|
||||
'-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]'
|
||||
'--max-fps=[Limit the frame rate of screen capture]'
|
||||
'--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)'
|
||||
'--mouse-bind=[Configure bindings of secondary clicks]'
|
||||
|
||||
@@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
|
||||
VERSION=7.0.1
|
||||
VERSION=7.0.2
|
||||
FILENAME=ffmpeg-$VERSION.tar.xz
|
||||
PROJECT_DIR=ffmpeg-$VERSION
|
||||
SHA256SUM=bce9eeb0f17ef8982390b1f37711a61b4290dc8c2a0c1a37b5857e85bfb0e4ff
|
||||
SHA256SUM=8646515b638a3ad303e23af6a3587734447cb8fc0a0c064ecdb8e95c4fd8b389
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
|
||||
VERSION=2.30.5
|
||||
VERSION=2.30.7
|
||||
FILENAME=SDL-$VERSION.tar.gz
|
||||
PROJECT_DIR=SDL-release-$VERSION
|
||||
SHA256SUM=be3ca88f8c362704627a0bc5406edb2cd6cc6ba463596d81ebb7c2f18763d3bf
|
||||
SHA256SUM=1578c96f62c9ae36b64e431b2aa0e0b0fd07c275dedbc694afc38e19056688f5
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ src = [
|
||||
'src/adb/adb_parser.c',
|
||||
'src/adb/adb_tunnel.c',
|
||||
'src/audio_player.c',
|
||||
'src/audio_regulator.c',
|
||||
'src/cli.c',
|
||||
'src/clock.c',
|
||||
'src/compat.c',
|
||||
@@ -22,6 +23,7 @@ src = [
|
||||
'src/frame_buffer.c',
|
||||
'src/input_manager.c',
|
||||
'src/keyboard_sdk.c',
|
||||
'src/mouse_capture.c',
|
||||
'src/mouse_sdk.c',
|
||||
'src/opengl.c',
|
||||
'src/options.c',
|
||||
|
||||
@@ -13,7 +13,7 @@ BEGIN
|
||||
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
||||
VALUE "OriginalFilename", "scrcpy.exe"
|
||||
VALUE "ProductName", "scrcpy"
|
||||
VALUE "ProductVersion", "2.6.1"
|
||||
VALUE "ProductVersion", "2.7"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
12
app/scrcpy.1
12
app/scrcpy.1
@@ -185,9 +185,9 @@ Select how to send gamepad inputs to the device.
|
||||
|
||||
Possible values are "disabled", "uhid" and "aoa":
|
||||
|
||||
- "disabled" does not send keyboard inputs to the device.
|
||||
- "uhid" simulates a physical HID gamepad using the Linux HID kernel module on the device.
|
||||
- "aoa" simulates a physical HID gamepad using the AOAv2 protocol. It may only work over USB.
|
||||
- "disabled" does not send gamepad inputs to the device.
|
||||
- "uhid" simulates physical HID gamepads using the Linux HID kernel module on the device.
|
||||
- "aoa" simulates physical HID gamepads using the AOAv2 protocol. It may only work over USB.
|
||||
|
||||
Also see \fB\-\-keyboard\f and R\fB\-\-mouse\fR.
|
||||
.TP
|
||||
@@ -727,7 +727,11 @@ Pinch-to-zoom and rotate from the center of the screen
|
||||
|
||||
.TP
|
||||
.B Shift+click-and-move
|
||||
Tilt (slide vertically with two fingers)
|
||||
Tilt vertically (slide with 2 fingers)
|
||||
|
||||
.TP
|
||||
.B Ctrl+Shift+click-and-move
|
||||
Tilt horizontally (slide with 2 fingers)
|
||||
|
||||
.TP
|
||||
.B Drag & drop APK file
|
||||
|
||||
@@ -1,138 +1,23 @@
|
||||
#include "audio_player.h"
|
||||
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/opt.h>
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
#define SC_AUDIO_PLAYER_NDEBUG // comment to debug
|
||||
|
||||
/**
|
||||
* Real-time audio player with configurable latency
|
||||
*
|
||||
* As input, the player regularly receives AVFrames of decoded audio samples.
|
||||
* As output, an SDL callback regularly requests audio samples to be played.
|
||||
* In the middle, an audio buffer stores the samples produced but not consumed
|
||||
* yet.
|
||||
*
|
||||
* The goal of the player is to feed the audio output with a latency as low as
|
||||
* possible while avoiding buffer underrun (i.e. not being able to provide
|
||||
* samples when requested).
|
||||
*
|
||||
* The player aims to feed the audio output with as little latency as possible
|
||||
* while avoiding buffer underrun. To achieve this, it attempts to maintain the
|
||||
* average buffering (the number of samples present in the buffer) around a
|
||||
* target value. If this target buffering is too low, then buffer underrun will
|
||||
* occur frequently. If it is too high, then latency will become unacceptable.
|
||||
* This target value is configured using the scrcpy option --audio-buffer.
|
||||
*
|
||||
* The player cannot adjust the sample input rate (it receives samples produced
|
||||
* in real-time) or the sample output rate (it must provide samples as
|
||||
* requested by the audio output callback). Therefore, it may only apply
|
||||
* compensation by resampling (converting _m_ input samples to _n_ output
|
||||
* samples).
|
||||
*
|
||||
* The compensation itself is applied by libswresample (FFmpeg). It is
|
||||
* configured using swr_set_compensation(). An important work for the player
|
||||
* is to estimate the compensation value regularly and apply it.
|
||||
*
|
||||
* The estimated buffering level is the result of averaging the "natural"
|
||||
* buffering (samples are produced and consumed by blocks, so it must be
|
||||
* smoothed), and making instant adjustments resulting of its own actions
|
||||
* (explicit compensation and silence insertion on underflow), which are not
|
||||
* smoothed.
|
||||
*
|
||||
* Buffer underflow events can occur when packets arrive too late. In that case,
|
||||
* the player inserts silence. Once the packets finally arrive (late), one
|
||||
* strategy could be to drop the samples that were replaced by silence, in
|
||||
* order to keep a minimal latency. However, dropping samples in case of buffer
|
||||
* underflow is inadvisable, as it would temporarily increase the underflow
|
||||
* even more and cause very noticeable audio glitches.
|
||||
*
|
||||
* Therefore, the player doesn't drop any sample on underflow. The compensation
|
||||
* mechanism will absorb the delay introduced by the inserted silence.
|
||||
*/
|
||||
|
||||
/** Downcast frame_sink to sc_audio_player */
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink)
|
||||
|
||||
#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT
|
||||
#define SC_SDL_SAMPLE_FMT AUDIO_F32
|
||||
|
||||
#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ap->buf, (SAMPLES))
|
||||
#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ap->buf, (BYTES))
|
||||
|
||||
static void SDLCALL
|
||||
sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
|
||||
struct sc_audio_player *ap = userdata;
|
||||
|
||||
// This callback is called with the lock used by SDL_LockAudioDevice()
|
||||
|
||||
assert(len_int > 0);
|
||||
size_t len = len_int;
|
||||
uint32_t count = TO_SAMPLES(len);
|
||||
|
||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||
LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count);
|
||||
#endif
|
||||
assert(len % ap->audioreg.sample_size == 0);
|
||||
uint32_t out_samples = len / ap->audioreg.sample_size;
|
||||
|
||||
bool played = atomic_load_explicit(&ap->played, memory_order_relaxed);
|
||||
if (!played) {
|
||||
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf);
|
||||
// Wait until the buffer is filled up to at least target_buffering
|
||||
// before playing
|
||||
if (buffered_samples < ap->target_buffering) {
|
||||
LOGV("[Audio] Inserting initial buffering silence: %" PRIu32
|
||||
" samples", count);
|
||||
// Delay playback starting to reach the target buffering. Fill the
|
||||
// whole buffer with silence (len is small compared to the
|
||||
// arbitrary margin value).
|
||||
memset(stream, 0, len);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t read = sc_audiobuf_read(&ap->buf, stream, count);
|
||||
|
||||
if (read < count) {
|
||||
uint32_t silence = count - read;
|
||||
// Insert silence. In theory, the inserted silent samples replace the
|
||||
// missing real samples, which will arrive later, so they should be
|
||||
// dropped to keep the latency minimal. However, this would cause very
|
||||
// audible glitches, so let the clock compensation restore the target
|
||||
// latency.
|
||||
LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples",
|
||||
silence);
|
||||
memset(stream + TO_BYTES(read), 0, TO_BYTES(silence));
|
||||
|
||||
bool received = atomic_load_explicit(&ap->received,
|
||||
memory_order_relaxed);
|
||||
if (received) {
|
||||
// Inserting additional samples immediately increases buffering
|
||||
atomic_fetch_add_explicit(&ap->underflow, silence,
|
||||
memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
atomic_store_explicit(&ap->played, true, memory_order_relaxed);
|
||||
}
|
||||
|
||||
static uint8_t *
|
||||
sc_audio_player_get_swr_buf(struct sc_audio_player *ap, uint32_t min_samples) {
|
||||
size_t min_buf_size = TO_BYTES(min_samples);
|
||||
if (min_buf_size > ap->swr_buf_alloc_size) {
|
||||
size_t new_size = min_buf_size + 4096;
|
||||
uint8_t *buf = realloc(ap->swr_buf, new_size);
|
||||
if (!buf) {
|
||||
LOG_OOM();
|
||||
// Could not realloc to the requested size
|
||||
return NULL;
|
||||
}
|
||||
ap->swr_buf = buf;
|
||||
ap->swr_buf_alloc_size = new_size;
|
||||
}
|
||||
|
||||
return ap->swr_buf;
|
||||
sc_audio_regulator_pull(&ap->audioreg, stream, out_samples);
|
||||
}
|
||||
|
||||
static bool
|
||||
@@ -140,209 +25,21 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
||||
const AVFrame *frame) {
|
||||
struct sc_audio_player *ap = DOWNCAST(sink);
|
||||
|
||||
SwrContext *swr_ctx = ap->swr_ctx;
|
||||
|
||||
int64_t swr_delay = swr_get_delay(swr_ctx, ap->sample_rate);
|
||||
// No need to av_rescale_rnd(), input and output sample rates are the same.
|
||||
// Add more space (256) for clock compensation.
|
||||
int dst_nb_samples = swr_delay + frame->nb_samples + 256;
|
||||
|
||||
uint8_t *swr_buf = sc_audio_player_get_swr_buf(ap, dst_nb_samples);
|
||||
if (!swr_buf) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples,
|
||||
(const uint8_t **) frame->data, frame->nb_samples);
|
||||
if (ret < 0) {
|
||||
LOGE("Resampling failed: %d", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
// swr_convert() returns the number of samples which would have been
|
||||
// written if the buffer was big enough.
|
||||
uint32_t samples = MIN(ret, dst_nb_samples);
|
||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||
LOGD("[Audio] %" PRIu32 " samples written to buffer", samples);
|
||||
#endif
|
||||
|
||||
uint32_t cap = sc_audiobuf_capacity(&ap->buf);
|
||||
if (samples > cap) {
|
||||
// Very very unlikely: a single resampled frame should never
|
||||
// exceed the audio buffer size (or something is very wrong).
|
||||
// Ignore the first bytes in swr_buf to avoid memory corruption anyway.
|
||||
swr_buf += TO_BYTES(samples - cap);
|
||||
samples = cap;
|
||||
}
|
||||
|
||||
uint32_t skipped_samples = 0;
|
||||
|
||||
uint32_t written = sc_audiobuf_write(&ap->buf, swr_buf, samples);
|
||||
if (written < samples) {
|
||||
uint32_t remaining = samples - written;
|
||||
|
||||
// All samples that could be written without locking have been written,
|
||||
// now we need to lock to drop/consume old samples
|
||||
SDL_LockAudioDevice(ap->device);
|
||||
|
||||
// Retry with the lock
|
||||
written += sc_audiobuf_write(&ap->buf,
|
||||
swr_buf + TO_BYTES(written),
|
||||
remaining);
|
||||
if (written < samples) {
|
||||
remaining = samples - written;
|
||||
// Still insufficient, drop old samples to make space
|
||||
skipped_samples = sc_audiobuf_read(&ap->buf, NULL, remaining);
|
||||
assert(skipped_samples == remaining);
|
||||
}
|
||||
|
||||
SDL_UnlockAudioDevice(ap->device);
|
||||
|
||||
if (written < samples) {
|
||||
// Now there is enough space
|
||||
uint32_t w = sc_audiobuf_write(&ap->buf,
|
||||
swr_buf + TO_BYTES(written),
|
||||
remaining);
|
||||
assert(w == remaining);
|
||||
(void) w;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t underflow = 0;
|
||||
uint32_t max_buffered_samples;
|
||||
bool played = atomic_load_explicit(&ap->played, memory_order_relaxed);
|
||||
if (played) {
|
||||
underflow = atomic_exchange_explicit(&ap->underflow, 0,
|
||||
memory_order_relaxed);
|
||||
|
||||
max_buffered_samples = ap->target_buffering
|
||||
+ 12 * ap->output_buffer
|
||||
+ ap->target_buffering / 10;
|
||||
} else {
|
||||
// SDL playback not started yet, do not accumulate more than
|
||||
// max_initial_buffering samples, this would cause unnecessary delay
|
||||
// (and glitches to compensate) on start.
|
||||
max_buffered_samples = ap->target_buffering + 2 * ap->output_buffer;
|
||||
}
|
||||
|
||||
uint32_t can_read = sc_audiobuf_can_read(&ap->buf);
|
||||
if (can_read > max_buffered_samples) {
|
||||
uint32_t skip_samples = 0;
|
||||
|
||||
SDL_LockAudioDevice(ap->device);
|
||||
can_read = sc_audiobuf_can_read(&ap->buf);
|
||||
if (can_read > max_buffered_samples) {
|
||||
skip_samples = can_read - max_buffered_samples;
|
||||
uint32_t r = sc_audiobuf_read(&ap->buf, NULL, skip_samples);
|
||||
assert(r == skip_samples);
|
||||
(void) r;
|
||||
skipped_samples += skip_samples;
|
||||
}
|
||||
SDL_UnlockAudioDevice(ap->device);
|
||||
|
||||
if (skip_samples) {
|
||||
if (played) {
|
||||
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
|
||||
" samples", skip_samples);
|
||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||
} else {
|
||||
LOGD("[Audio] Playback not started, skipping %" PRIu32
|
||||
" samples", skip_samples);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
atomic_store_explicit(&ap->received, true, memory_order_relaxed);
|
||||
if (!played) {
|
||||
// Nothing more to do
|
||||
return true;
|
||||
}
|
||||
|
||||
// Number of samples added (or removed, if negative) for compensation
|
||||
int32_t instant_compensation = (int32_t) written - frame->nb_samples;
|
||||
// Inserting silence instantly increases buffering
|
||||
int32_t inserted_silence = (int32_t) underflow;
|
||||
// Dropping input samples instantly decreases buffering
|
||||
int32_t dropped = (int32_t) skipped_samples;
|
||||
|
||||
// The compensation must apply instantly, it must not be smoothed
|
||||
ap->avg_buffering.avg += instant_compensation + inserted_silence - dropped;
|
||||
if (ap->avg_buffering.avg < 0) {
|
||||
// Since dropping samples instantly reduces buffering, the difference
|
||||
// is applied immediately to the average value, assuming that the delay
|
||||
// between the producer and the consumer will be caught up.
|
||||
//
|
||||
// However, when this assumption is not valid, the average buffering
|
||||
// may decrease indefinitely. Prevent it to become negative to limit
|
||||
// the consequences.
|
||||
ap->avg_buffering.avg = 0;
|
||||
}
|
||||
|
||||
// However, the buffering level must be smoothed
|
||||
sc_average_push(&ap->avg_buffering, can_read);
|
||||
|
||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||
LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f",
|
||||
can_read, sc_average_get(&ap->avg_buffering));
|
||||
#endif
|
||||
|
||||
ap->samples_since_resync += written;
|
||||
if (ap->samples_since_resync >= ap->sample_rate) {
|
||||
// Recompute compensation every second
|
||||
ap->samples_since_resync = 0;
|
||||
|
||||
float avg = sc_average_get(&ap->avg_buffering);
|
||||
int diff = ap->target_buffering - avg;
|
||||
|
||||
// Enable compensation when the difference exceeds +/- 4ms.
|
||||
// Disable compensation when the difference is lower than +/- 1ms.
|
||||
int threshold = ap->compensation != 0
|
||||
? ap->sample_rate / 1000 /* 1ms */
|
||||
: ap->sample_rate * 4 / 1000; /* 4ms */
|
||||
|
||||
if (abs(diff) < threshold) {
|
||||
// Do not compensate for small values, the error is just noise
|
||||
diff = 0;
|
||||
} else if (diff < 0 && can_read < ap->target_buffering) {
|
||||
// Do not accelerate if the instant buffering level is below the
|
||||
// target, this would increase underflow
|
||||
diff = 0;
|
||||
}
|
||||
// Compensate the diff over 4 seconds (but will be recomputed after 1
|
||||
// second)
|
||||
int distance = 4 * ap->sample_rate;
|
||||
// Limit compensation rate to 2%
|
||||
int abs_max_diff = distance / 50;
|
||||
diff = CLAMP(diff, -abs_max_diff, abs_max_diff);
|
||||
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
|
||||
" compensation=%d", ap->target_buffering, avg, can_read, diff);
|
||||
|
||||
if (diff != ap->compensation) {
|
||||
int ret = swr_set_compensation(swr_ctx, diff, distance);
|
||||
if (ret < 0) {
|
||||
LOGW("Resampling compensation failed: %d", ret);
|
||||
// not fatal
|
||||
} else {
|
||||
ap->compensation = diff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return sc_audio_regulator_push(&ap->audioreg, frame);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
const AVCodecContext *ctx) {
|
||||
struct sc_audio_player *ap = DOWNCAST(sink);
|
||||
|
||||
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||
assert(ctx->ch_layout.nb_channels > 0);
|
||||
unsigned nb_channels = ctx->ch_layout.nb_channels;
|
||||
assert(ctx->ch_layout.nb_channels > 0 && ctx->ch_layout.nb_channels < 256);
|
||||
uint8_t nb_channels = ctx->ch_layout.nb_channels;
|
||||
#else
|
||||
int tmp = av_get_channel_layout_nb_channels(ctx->channel_layout);
|
||||
assert(tmp > 0);
|
||||
unsigned nb_channels = tmp;
|
||||
assert(tmp > 0 && tmp < 256);
|
||||
uint8_t nb_channels = tmp;
|
||||
#endif
|
||||
|
||||
assert(ctx->sample_rate > 0);
|
||||
@@ -350,17 +47,19 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT);
|
||||
assert(out_bytes_per_sample > 0);
|
||||
|
||||
ap->sample_rate = ctx->sample_rate;
|
||||
ap->nb_channels = nb_channels;
|
||||
ap->out_bytes_per_sample = out_bytes_per_sample;
|
||||
uint32_t target_buffering_samples =
|
||||
ap->target_buffering_delay * ctx->sample_rate / SC_TICK_FREQ;
|
||||
|
||||
ap->target_buffering = ap->target_buffering_delay * ap->sample_rate
|
||||
/ SC_TICK_FREQ;
|
||||
size_t sample_size = nb_channels * out_bytes_per_sample;
|
||||
bool ok = sc_audio_regulator_init(&ap->audioreg, sample_size, ctx,
|
||||
target_buffering_samples);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t aout_samples = ap->output_buffer_duration * ap->sample_rate
|
||||
uint64_t aout_samples = ap->output_buffer_duration * ctx->sample_rate
|
||||
/ SC_TICK_FREQ;
|
||||
assert(aout_samples <= 0xFFFF);
|
||||
ap->output_buffer = (uint16_t) aout_samples;
|
||||
|
||||
SDL_AudioSpec desired = {
|
||||
.freq = ctx->sample_rate,
|
||||
@@ -375,69 +74,10 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
ap->device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0);
|
||||
if (!ap->device) {
|
||||
LOGE("Could not open audio device: %s", SDL_GetError());
|
||||
sc_audio_regulator_destroy(&ap->audioreg);
|
||||
return false;
|
||||
}
|
||||
|
||||
SwrContext *swr_ctx = swr_alloc();
|
||||
if (!swr_ctx) {
|
||||
LOG_OOM();
|
||||
goto error_close_audio_device;
|
||||
}
|
||||
ap->swr_ctx = swr_ctx;
|
||||
|
||||
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||
av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0);
|
||||
av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0);
|
||||
#else
|
||||
av_opt_set_channel_layout(swr_ctx, "in_channel_layout",
|
||||
ctx->channel_layout, 0);
|
||||
av_opt_set_channel_layout(swr_ctx, "out_channel_layout",
|
||||
ctx->channel_layout, 0);
|
||||
#endif
|
||||
|
||||
av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0);
|
||||
av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0);
|
||||
|
||||
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0);
|
||||
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0);
|
||||
|
||||
int ret = swr_init(swr_ctx);
|
||||
if (ret) {
|
||||
LOGE("Failed to initialize the resampling context");
|
||||
goto error_free_swr_ctx;
|
||||
}
|
||||
|
||||
// Use a ring-buffer of the target buffering size plus 1 second between the
|
||||
// producer and the consumer. It's too big on purpose, to guarantee that
|
||||
// the producer and the consumer will be able to access it in parallel
|
||||
// without locking.
|
||||
uint32_t audiobuf_samples = ap->target_buffering + ap->sample_rate;
|
||||
|
||||
size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample;
|
||||
bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples);
|
||||
if (!ok) {
|
||||
goto error_free_swr_ctx;
|
||||
}
|
||||
|
||||
size_t initial_swr_buf_size = TO_BYTES(4096);
|
||||
ap->swr_buf = malloc(initial_swr_buf_size);
|
||||
if (!ap->swr_buf) {
|
||||
LOG_OOM();
|
||||
goto error_destroy_audiobuf;
|
||||
}
|
||||
ap->swr_buf_alloc_size = initial_swr_buf_size;
|
||||
|
||||
// Samples are produced and consumed by blocks, so the buffering must be
|
||||
// smoothed to get a relatively stable value.
|
||||
sc_average_init(&ap->avg_buffering, 128);
|
||||
ap->samples_since_resync = 0;
|
||||
|
||||
ap->received = false;
|
||||
atomic_init(&ap->played, false);
|
||||
atomic_init(&ap->received, false);
|
||||
atomic_init(&ap->underflow, 0);
|
||||
ap->compensation = 0;
|
||||
|
||||
// The thread calling open() is the thread calling push(), which fills the
|
||||
// audio buffer consumed by the SDL audio thread.
|
||||
ok = sc_thread_set_priority(SC_THREAD_PRIORITY_TIME_CRITICAL);
|
||||
@@ -449,15 +89,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
SDL_PauseAudioDevice(ap->device, 0);
|
||||
|
||||
return true;
|
||||
|
||||
error_destroy_audiobuf:
|
||||
sc_audiobuf_destroy(&ap->buf);
|
||||
error_free_swr_ctx:
|
||||
swr_free(&ap->swr_ctx);
|
||||
error_close_audio_device:
|
||||
SDL_CloseAudioDevice(ap->device);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -468,9 +99,7 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) {
|
||||
SDL_PauseAudioDevice(ap->device, 1);
|
||||
SDL_CloseAudioDevice(ap->device);
|
||||
|
||||
free(ap->swr_buf);
|
||||
sc_audiobuf_destroy(&ap->buf);
|
||||
swr_free(&ap->swr_ctx);
|
||||
sc_audio_regulator_destroy(&ap->audioreg);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -5,76 +5,27 @@
|
||||
|
||||
#include <stdatomic.h>
|
||||
#include <stdbool.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libswresample/swresample.h>
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include "audio_regulator.h"
|
||||
#include "trait/frame_sink.h"
|
||||
#include "util/audiobuf.h"
|
||||
#include "util/average.h"
|
||||
#include "util/thread.h"
|
||||
#include "util/tick.h"
|
||||
|
||||
struct sc_audio_player {
|
||||
struct sc_frame_sink frame_sink;
|
||||
|
||||
SDL_AudioDeviceID device;
|
||||
|
||||
// The target buffering between the producer and the consumer. This value
|
||||
// is directly use for compensation.
|
||||
// Since audio capture and/or encoding on the device typically produce
|
||||
// blocks of 960 samples (20ms) or 1024 samples (~21.3ms), this target
|
||||
// value should be higher.
|
||||
sc_tick target_buffering_delay;
|
||||
uint32_t target_buffering; // in samples
|
||||
|
||||
// SDL audio output buffer size.
|
||||
// SDL audio output buffer size
|
||||
sc_tick output_buffer_duration;
|
||||
uint16_t output_buffer;
|
||||
|
||||
// Audio buffer to communicate between the receiver and the SDL audio
|
||||
// callback
|
||||
struct sc_audiobuf buf;
|
||||
|
||||
// Resampler (only used from the receiver thread)
|
||||
struct SwrContext *swr_ctx;
|
||||
|
||||
// The sample rate is the same for input and output
|
||||
unsigned sample_rate;
|
||||
// The number of channels is the same for input and output
|
||||
unsigned nb_channels;
|
||||
// The number of bytes per sample for a single channel
|
||||
size_t out_bytes_per_sample;
|
||||
|
||||
// Target buffer for resampling (only used by the receiver thread)
|
||||
uint8_t *swr_buf;
|
||||
size_t swr_buf_alloc_size;
|
||||
|
||||
// Number of buffered samples (may be negative on underflow) (only used by
|
||||
// the receiver thread)
|
||||
struct sc_average avg_buffering;
|
||||
// Count the number of samples to trigger a compensation update regularly
|
||||
// (only used by the receiver thread)
|
||||
uint32_t samples_since_resync;
|
||||
|
||||
// Number of silence samples inserted since the last received packet
|
||||
atomic_uint_least32_t underflow;
|
||||
|
||||
// Current applied compensation value (only used by the receiver thread)
|
||||
int compensation;
|
||||
|
||||
// Set to true the first time a sample is received
|
||||
atomic_bool received;
|
||||
|
||||
// Set to true the first time the SDL callback is called
|
||||
atomic_bool played;
|
||||
|
||||
const struct sc_audio_player_callbacks *cbs;
|
||||
void *cbs_userdata;
|
||||
};
|
||||
|
||||
struct sc_audio_player_callbacks {
|
||||
void (*on_ended)(struct sc_audio_player *ap, bool success, void *userdata);
|
||||
SDL_AudioDeviceID device;
|
||||
struct sc_audio_regulator audioreg;
|
||||
};
|
||||
|
||||
void
|
||||
|
||||
415
app/src/audio_regulator.c
Normal file
415
app/src/audio_regulator.c
Normal file
@@ -0,0 +1,415 @@
|
||||
#include "audio_regulator.h"
|
||||
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/opt.h>
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
//#define SC_AUDIO_REGULATOR_DEBUG // uncomment to debug
|
||||
|
||||
/**
|
||||
* Real-time audio regulator with configurable latency
|
||||
*
|
||||
* As input, the regulator regularly receives AVFrames of decoded audio samples.
|
||||
* As output, the audio player regularly requests audio samples to be played.
|
||||
* In the middle, an audio buffer stores the samples produced but not consumed
|
||||
* yet.
|
||||
*
|
||||
* The goal of the regulator is to feed the audio player with a latency as low
|
||||
* as possible while avoiding buffer underrun (i.e. not being able to provide
|
||||
* samples when requested).
|
||||
*
|
||||
* To achieve this, it attempts to maintain the average buffering (the number
|
||||
* of samples present in the buffer) around a target value. If this target
|
||||
* buffering is too low, then buffer underrun will occur frequently. If it is
|
||||
* too high, then latency will become unacceptable. This target value is
|
||||
* configured using the scrcpy option --audio-buffer.
|
||||
*
|
||||
* The regulator cannot adjust the sample input rate (it receives samples
|
||||
* produced in real-time) or the sample output rate (it must provide samples as
|
||||
* requested by the audio player). Therefore, it may only apply compensation by
|
||||
* resampling (converting _m_ input samples to _n_ output samples).
|
||||
*
|
||||
* The compensation itself is applied by libswresample (FFmpeg). It is
|
||||
* configured using swr_set_compensation(). An important work for the regulator
|
||||
* is to estimate the compensation value regularly and apply it.
|
||||
*
|
||||
* The estimated buffering level is the result of averaging the "natural"
|
||||
* buffering (samples are produced and consumed by blocks, so it must be
|
||||
* smoothed), and making instant adjustments resulting of its own actions
|
||||
* (explicit compensation and silence insertion on underflow), which are not
|
||||
* smoothed.
|
||||
*
|
||||
* Buffer underflow events can occur when packets arrive too late. In that case,
|
||||
* the regulator inserts silence. Once the packets finally arrive (late), one
|
||||
* strategy could be to drop the samples that were replaced by silence, in
|
||||
* order to keep a minimal latency. However, dropping samples in case of buffer
|
||||
* underflow is inadvisable, as it would temporarily increase the underflow
|
||||
* even more and cause very noticeable audio glitches.
|
||||
*
|
||||
* Therefore, the regulator doesn't drop any sample on underflow. The
|
||||
* compensation mechanism will absorb the delay introduced by the inserted
|
||||
* silence.
|
||||
*/
|
||||
|
||||
#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ar->buf, (SAMPLES))
|
||||
#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ar->buf, (BYTES))
|
||||
|
||||
void
|
||||
sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out,
|
||||
uint32_t out_samples) {
|
||||
#ifdef SC_AUDIO_REGULATOR_DEBUG
|
||||
LOGD("[Audio] Audio regulator pulls %" PRIu32 " samples", out_samples);
|
||||
#endif
|
||||
|
||||
// A lock is necessary in the rare case where the producer needs to drop
|
||||
// samples already pushed (when the buffer is full)
|
||||
sc_mutex_lock(&ar->mutex);
|
||||
|
||||
bool played = atomic_load_explicit(&ar->played, memory_order_relaxed);
|
||||
if (!played) {
|
||||
uint32_t buffered_samples = sc_audiobuf_can_read(&ar->buf);
|
||||
// Wait until the buffer is filled up to at least target_buffering
|
||||
// before playing
|
||||
if (buffered_samples < ar->target_buffering) {
|
||||
LOGV("[Audio] Inserting initial buffering silence: %" PRIu32
|
||||
" samples", out_samples);
|
||||
// Delay playback starting to reach the target buffering. Fill the
|
||||
// whole buffer with silence (len is small compared to the
|
||||
// arbitrary margin value).
|
||||
memset(out, 0, out_samples * ar->sample_size);
|
||||
sc_mutex_unlock(&ar->mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t read = sc_audiobuf_read(&ar->buf, out, out_samples);
|
||||
|
||||
sc_mutex_unlock(&ar->mutex);
|
||||
|
||||
if (read < out_samples) {
|
||||
uint32_t silence = out_samples - read;
|
||||
// Insert silence. In theory, the inserted silent samples replace the
|
||||
// missing real samples, which will arrive later, so they should be
|
||||
// dropped to keep the latency minimal. However, this would cause very
|
||||
// audible glitches, so let the clock compensation restore the target
|
||||
// latency.
|
||||
LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples",
|
||||
silence);
|
||||
memset(out + TO_BYTES(read), 0, TO_BYTES(silence));
|
||||
|
||||
bool received = atomic_load_explicit(&ar->received,
|
||||
memory_order_relaxed);
|
||||
if (received) {
|
||||
// Inserting additional samples immediately increases buffering
|
||||
atomic_fetch_add_explicit(&ar->underflow, silence,
|
||||
memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
atomic_store_explicit(&ar->played, true, memory_order_relaxed);
|
||||
}
|
||||
|
||||
static uint8_t *
|
||||
sc_audio_regulator_get_swr_buf(struct sc_audio_regulator *ar,
|
||||
uint32_t min_samples) {
|
||||
size_t min_buf_size = TO_BYTES(min_samples);
|
||||
if (min_buf_size > ar->swr_buf_alloc_size) {
|
||||
size_t new_size = min_buf_size + 4096;
|
||||
uint8_t *buf = realloc(ar->swr_buf, new_size);
|
||||
if (!buf) {
|
||||
LOG_OOM();
|
||||
// Could not realloc to the requested size
|
||||
return NULL;
|
||||
}
|
||||
ar->swr_buf = buf;
|
||||
ar->swr_buf_alloc_size = new_size;
|
||||
}
|
||||
|
||||
return ar->swr_buf;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
|
||||
SwrContext *swr_ctx = ar->swr_ctx;
|
||||
|
||||
int64_t swr_delay = swr_get_delay(swr_ctx, ar->sample_rate);
|
||||
// No need to av_rescale_rnd(), input and output sample rates are the same.
|
||||
// Add more space (256) for clock compensation.
|
||||
int dst_nb_samples = swr_delay + frame->nb_samples + 256;
|
||||
|
||||
uint8_t *swr_buf = sc_audio_regulator_get_swr_buf(ar, dst_nb_samples);
|
||||
if (!swr_buf) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples,
|
||||
(const uint8_t **) frame->data, frame->nb_samples);
|
||||
if (ret < 0) {
|
||||
LOGE("Resampling failed: %d", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
// swr_convert() returns the number of samples which would have been
|
||||
// written if the buffer was big enough.
|
||||
uint32_t samples = MIN(ret, dst_nb_samples);
|
||||
#ifdef SC_AUDIO_REGULATOR_DEBUG
|
||||
LOGD("[Audio] %" PRIu32 " samples written to buffer", samples);
|
||||
#endif
|
||||
|
||||
uint32_t cap = sc_audiobuf_capacity(&ar->buf);
|
||||
if (samples > cap) {
|
||||
// Very very unlikely: a single resampled frame should never
|
||||
// exceed the audio buffer size (or something is very wrong).
|
||||
// Ignore the first bytes in swr_buf to avoid memory corruption anyway.
|
||||
swr_buf += TO_BYTES(samples - cap);
|
||||
samples = cap;
|
||||
}
|
||||
|
||||
uint32_t skipped_samples = 0;
|
||||
|
||||
uint32_t written = sc_audiobuf_write(&ar->buf, swr_buf, samples);
|
||||
if (written < samples) {
|
||||
uint32_t remaining = samples - written;
|
||||
|
||||
// All samples that could be written without locking have been written,
|
||||
// now we need to lock to drop/consume old samples
|
||||
sc_mutex_lock(&ar->mutex);
|
||||
|
||||
// Retry with the lock
|
||||
written += sc_audiobuf_write(&ar->buf,
|
||||
swr_buf + TO_BYTES(written),
|
||||
remaining);
|
||||
if (written < samples) {
|
||||
remaining = samples - written;
|
||||
// Still insufficient, drop old samples to make space
|
||||
skipped_samples = sc_audiobuf_read(&ar->buf, NULL, remaining);
|
||||
assert(skipped_samples == remaining);
|
||||
}
|
||||
|
||||
sc_mutex_unlock(&ar->mutex);
|
||||
|
||||
if (written < samples) {
|
||||
// Now there is enough space
|
||||
uint32_t w = sc_audiobuf_write(&ar->buf,
|
||||
swr_buf + TO_BYTES(written),
|
||||
remaining);
|
||||
assert(w == remaining);
|
||||
(void) w;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t underflow = 0;
|
||||
uint32_t max_buffered_samples;
|
||||
bool played = atomic_load_explicit(&ar->played, memory_order_relaxed);
|
||||
if (played) {
|
||||
underflow = atomic_exchange_explicit(&ar->underflow, 0,
|
||||
memory_order_relaxed);
|
||||
|
||||
max_buffered_samples = ar->target_buffering * 11 / 10
|
||||
+ 60 * ar->sample_rate / 1000 /* 60 ms */;
|
||||
} else {
|
||||
// Playback not started yet, do not accumulate more than
|
||||
// max_initial_buffering samples, this would cause unnecessary delay
|
||||
// (and glitches to compensate) on start.
|
||||
max_buffered_samples = ar->target_buffering
|
||||
+ 10 * ar->sample_rate / 1000 /* 10 ms */;
|
||||
}
|
||||
|
||||
uint32_t can_read = sc_audiobuf_can_read(&ar->buf);
|
||||
if (can_read > max_buffered_samples) {
|
||||
uint32_t skip_samples = 0;
|
||||
|
||||
sc_mutex_lock(&ar->mutex);
|
||||
can_read = sc_audiobuf_can_read(&ar->buf);
|
||||
if (can_read > max_buffered_samples) {
|
||||
skip_samples = can_read - max_buffered_samples;
|
||||
uint32_t r = sc_audiobuf_read(&ar->buf, NULL, skip_samples);
|
||||
assert(r == skip_samples);
|
||||
(void) r;
|
||||
skipped_samples += skip_samples;
|
||||
}
|
||||
sc_mutex_unlock(&ar->mutex);
|
||||
|
||||
if (skip_samples) {
|
||||
if (played) {
|
||||
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
|
||||
" samples", skip_samples);
|
||||
#ifdef SC_AUDIO_REGULATOR_DEBUG
|
||||
} else {
|
||||
LOGD("[Audio] Playback not started, skipping %" PRIu32
|
||||
" samples", skip_samples);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
atomic_store_explicit(&ar->received, true, memory_order_relaxed);
|
||||
if (!played) {
|
||||
// Nothing more to do
|
||||
return true;
|
||||
}
|
||||
|
||||
// Number of samples added (or removed, if negative) for compensation
|
||||
int32_t instant_compensation = (int32_t) written - frame->nb_samples;
|
||||
// Inserting silence instantly increases buffering
|
||||
int32_t inserted_silence = (int32_t) underflow;
|
||||
// Dropping input samples instantly decreases buffering
|
||||
int32_t dropped = (int32_t) skipped_samples;
|
||||
|
||||
// The compensation must apply instantly, it must not be smoothed
|
||||
ar->avg_buffering.avg += instant_compensation + inserted_silence - dropped;
|
||||
if (ar->avg_buffering.avg < 0) {
|
||||
// Since dropping samples instantly reduces buffering, the difference
|
||||
// is applied immediately to the average value, assuming that the delay
|
||||
// between the producer and the consumer will be caught up.
|
||||
//
|
||||
// However, when this assumption is not valid, the average buffering
|
||||
// may decrease indefinitely. Prevent it to become negative to limit
|
||||
// the consequences.
|
||||
ar->avg_buffering.avg = 0;
|
||||
}
|
||||
|
||||
// However, the buffering level must be smoothed
|
||||
sc_average_push(&ar->avg_buffering, can_read);
|
||||
|
||||
#ifdef SC_AUDIO_REGULATOR_DEBUG
|
||||
LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f",
|
||||
can_read, sc_average_get(&ar->avg_buffering));
|
||||
#endif
|
||||
|
||||
ar->samples_since_resync += written;
|
||||
if (ar->samples_since_resync >= ar->sample_rate) {
|
||||
// Recompute compensation every second
|
||||
ar->samples_since_resync = 0;
|
||||
|
||||
float avg = sc_average_get(&ar->avg_buffering);
|
||||
int diff = ar->target_buffering - avg;
|
||||
|
||||
// Enable compensation when the difference exceeds +/- 4ms.
|
||||
// Disable compensation when the difference is lower than +/- 1ms.
|
||||
int threshold = ar->compensation != 0
|
||||
? ar->sample_rate / 1000 /* 1ms */
|
||||
: ar->sample_rate * 4 / 1000; /* 4ms */
|
||||
|
||||
if (abs(diff) < threshold) {
|
||||
// Do not compensate for small values, the error is just noise
|
||||
diff = 0;
|
||||
} else if (diff < 0 && can_read < ar->target_buffering) {
|
||||
// Do not accelerate if the instant buffering level is below the
|
||||
// target, this would increase underflow
|
||||
diff = 0;
|
||||
}
|
||||
// Compensate the diff over 4 seconds (but will be recomputed after 1
|
||||
// second)
|
||||
int distance = 4 * ar->sample_rate;
|
||||
// Limit compensation rate to 2%
|
||||
int abs_max_diff = distance / 50;
|
||||
diff = CLAMP(diff, -abs_max_diff, abs_max_diff);
|
||||
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
|
||||
" compensation=%d", ar->target_buffering, avg, can_read, diff);
|
||||
|
||||
if (diff != ar->compensation) {
|
||||
int ret = swr_set_compensation(swr_ctx, diff, distance);
|
||||
if (ret < 0) {
|
||||
LOGW("Resampling compensation failed: %d", ret);
|
||||
// not fatal
|
||||
} else {
|
||||
ar->compensation = diff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size,
|
||||
const AVCodecContext *ctx, uint32_t target_buffering) {
|
||||
SwrContext *swr_ctx = swr_alloc();
|
||||
if (!swr_ctx) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
ar->swr_ctx = swr_ctx;
|
||||
|
||||
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||
av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0);
|
||||
av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0);
|
||||
#else
|
||||
av_opt_set_channel_layout(swr_ctx, "in_channel_layout",
|
||||
ctx->channel_layout, 0);
|
||||
av_opt_set_channel_layout(swr_ctx, "out_channel_layout",
|
||||
ctx->channel_layout, 0);
|
||||
#endif
|
||||
|
||||
av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0);
|
||||
av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0);
|
||||
|
||||
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0);
|
||||
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0);
|
||||
|
||||
int ret = swr_init(swr_ctx);
|
||||
if (ret) {
|
||||
LOGE("Failed to initialize the resampling context");
|
||||
goto error_free_swr_ctx;
|
||||
}
|
||||
|
||||
bool ok = sc_mutex_init(&ar->mutex);
|
||||
if (!ok) {
|
||||
goto error_free_swr_ctx;
|
||||
}
|
||||
|
||||
ar->target_buffering = target_buffering;
|
||||
ar->sample_size = sample_size;
|
||||
ar->sample_rate = ctx->sample_rate;
|
||||
|
||||
// Use a ring-buffer of the target buffering size plus 1 second between the
|
||||
// producer and the consumer. It's too big on purpose, to guarantee that
|
||||
// the producer and the consumer will be able to access it in parallel
|
||||
// without locking.
|
||||
uint32_t audiobuf_samples = target_buffering + ar->sample_rate;
|
||||
|
||||
ok = sc_audiobuf_init(&ar->buf, sample_size, audiobuf_samples);
|
||||
if (!ok) {
|
||||
goto error_destroy_mutex;
|
||||
}
|
||||
|
||||
size_t initial_swr_buf_size = TO_BYTES(4096);
|
||||
ar->swr_buf = malloc(initial_swr_buf_size);
|
||||
if (!ar->swr_buf) {
|
||||
LOG_OOM();
|
||||
goto error_destroy_audiobuf;
|
||||
}
|
||||
ar->swr_buf_alloc_size = initial_swr_buf_size;
|
||||
|
||||
// Samples are produced and consumed by blocks, so the buffering must be
|
||||
// smoothed to get a relatively stable value.
|
||||
sc_average_init(&ar->avg_buffering, 128);
|
||||
ar->samples_since_resync = 0;
|
||||
|
||||
ar->received = false;
|
||||
atomic_init(&ar->played, false);
|
||||
atomic_init(&ar->received, false);
|
||||
atomic_init(&ar->underflow, 0);
|
||||
ar->compensation = 0;
|
||||
|
||||
return true;
|
||||
|
||||
error_destroy_audiobuf:
|
||||
sc_audiobuf_destroy(&ar->buf);
|
||||
error_destroy_mutex:
|
||||
sc_mutex_destroy(&ar->mutex);
|
||||
error_free_swr_ctx:
|
||||
swr_free(&ar->swr_ctx);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
sc_audio_regulator_destroy(struct sc_audio_regulator *ar) {
|
||||
free(ar->swr_buf);
|
||||
sc_audiobuf_destroy(&ar->buf);
|
||||
sc_mutex_destroy(&ar->mutex);
|
||||
swr_free(&ar->swr_ctx);
|
||||
}
|
||||
71
app/src/audio_regulator.h
Normal file
71
app/src/audio_regulator.h
Normal file
@@ -0,0 +1,71 @@
|
||||
#ifndef SC_AUDIO_REGULATOR_H
|
||||
#define SC_AUDIO_REGULATOR_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdatomic.h>
|
||||
#include <stdbool.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libswresample/swresample.h>
|
||||
#include "util/audiobuf.h"
|
||||
#include "util/average.h"
|
||||
#include "util/thread.h"
|
||||
|
||||
#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT
|
||||
|
||||
struct sc_audio_regulator {
|
||||
sc_mutex mutex;
|
||||
|
||||
// Target buffering between the producer and the consumer (in samples)
|
||||
uint32_t target_buffering;
|
||||
|
||||
// Audio buffer to communicate between the receiver and the player
|
||||
struct sc_audiobuf buf;
|
||||
|
||||
// Resampler (only used from the receiver thread)
|
||||
struct SwrContext *swr_ctx;
|
||||
|
||||
// The sample rate is the same for input and output
|
||||
uint32_t sample_rate;
|
||||
// The number of bytes per sample (for all channels)
|
||||
size_t sample_size;
|
||||
|
||||
// Target buffer for resampling (only used by the receiver thread)
|
||||
uint8_t *swr_buf;
|
||||
size_t swr_buf_alloc_size;
|
||||
|
||||
// Number of buffered samples (may be negative on underflow) (only used by
|
||||
// the receiver thread)
|
||||
struct sc_average avg_buffering;
|
||||
// Count the number of samples to trigger a compensation update regularly
|
||||
// (only used by the receiver thread)
|
||||
uint32_t samples_since_resync;
|
||||
|
||||
// Number of silence samples inserted since the last received packet
|
||||
atomic_uint_least32_t underflow;
|
||||
|
||||
// Current applied compensation value (only used by the receiver thread)
|
||||
int compensation;
|
||||
|
||||
// Set to true the first time a sample is received
|
||||
atomic_bool received;
|
||||
|
||||
// Set to true the first time samples are pulled by the player
|
||||
atomic_bool played;
|
||||
};
|
||||
|
||||
bool
|
||||
sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size,
|
||||
const AVCodecContext *ctx, uint32_t target_buffering);
|
||||
|
||||
void
|
||||
sc_audio_regulator_destroy(struct sc_audio_regulator *ar);
|
||||
|
||||
bool
|
||||
sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame);
|
||||
|
||||
void
|
||||
sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out,
|
||||
uint32_t samples);
|
||||
|
||||
#endif
|
||||
@@ -384,10 +384,10 @@ static const struct sc_option options[] = {
|
||||
.text = "Select how to send gamepad inputs to the device.\n"
|
||||
"Possible values are \"disabled\", \"uhid\" and \"aoa\".\n"
|
||||
"\"disabled\" does not send gamepad inputs to the device.\n"
|
||||
"\"uhid\" simulates a physical HID gamepad using the Linux "
|
||||
"UHID kernel module on the device.\n"
|
||||
"\"aoa\" simulates a physical gamepad using the AOAv2 "
|
||||
"protocol. It may only work over USB.\n"
|
||||
"\"uhid\" simulates physical HID gamepads using the Linux UHID "
|
||||
"kernel module on the device.\n"
|
||||
"\"aoa\" simulates physical gamepads using the AOAv2 protocol."
|
||||
"It may only work over USB.\n"
|
||||
"Also see --keyboard and --mouse.",
|
||||
},
|
||||
{
|
||||
@@ -1072,7 +1072,11 @@ static const struct sc_shortcut shortcuts[] = {
|
||||
},
|
||||
{
|
||||
.shortcuts = { "Shift+click-and-move" },
|
||||
.text = "Tilt (slide vertically with two fingers)",
|
||||
.text = "Tilt vertically (slide with 2 fingers)",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "Ctrl+Shift+click-and-move" },
|
||||
.text = "Tilt horizontally (slide with 2 fingers)",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "Drag & drop APK file" },
|
||||
@@ -1491,18 +1495,6 @@ parse_max_size(const char *s, uint16_t *max_size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_max_fps(const char *s, uint16_t *max_fps) {
|
||||
long value;
|
||||
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max fps");
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*max_fps = (uint16_t) value;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_buffering_time(const char *s, sc_tick *tick) {
|
||||
long value;
|
||||
@@ -2276,9 +2268,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
"--keyboard=uhid instead.");
|
||||
return false;
|
||||
case OPT_MAX_FPS:
|
||||
if (!parse_max_fps(optarg, &opts->max_fps)) {
|
||||
return false;
|
||||
}
|
||||
opts->max_fps = optarg;
|
||||
break;
|
||||
case 'm':
|
||||
if (!parse_max_size(optarg, &opts->max_size)) {
|
||||
@@ -2812,20 +2802,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
} else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID_OR_AOA) {
|
||||
opts->mouse_input_mode = otg ? SC_MOUSE_INPUT_MODE_AOA
|
||||
: SC_MOUSE_INPUT_MODE_SDK;
|
||||
: SC_MOUSE_INPUT_MODE_UHID;
|
||||
} else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK
|
||||
&& !opts->video_playback) {
|
||||
LOGE("SDK mouse mode requires video playback. Try --mouse=uhid.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opts->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AUTO) {
|
||||
// UHID does not work on all devices (with old Android
|
||||
// versions), so it cannot be enabled by default
|
||||
opts->gamepad_input_mode = otg ? SC_GAMEPAD_INPUT_MODE_AOA
|
||||
: SC_GAMEPAD_INPUT_MODE_DISABLED;
|
||||
} else if (opts->gamepad_input_mode
|
||||
== SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA) {
|
||||
if (opts->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA) {
|
||||
opts->gamepad_input_mode = otg ? SC_GAMEPAD_INPUT_MODE_AOA
|
||||
: SC_GAMEPAD_INPUT_MODE_UHID;
|
||||
}
|
||||
@@ -2885,9 +2868,17 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
return false;
|
||||
}
|
||||
|
||||
enum sc_gamepad_input_mode gmode = opts->gamepad_input_mode;
|
||||
if (gmode != SC_GAMEPAD_INPUT_MODE_AOA
|
||||
&& gmode != SC_GAMEPAD_INPUT_MODE_DISABLED) {
|
||||
LOGE("In OTG mode, --gamepad only supports aoa or disabled.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (kmode == SC_KEYBOARD_INPUT_MODE_DISABLED
|
||||
&& mmode == SC_MOUSE_INPUT_MODE_DISABLED) {
|
||||
LOGE("Could not disable both keyboard and mouse in OTG mode.");
|
||||
&& mmode == SC_MOUSE_INPUT_MODE_DISABLED
|
||||
&& gmode == SC_GAMEPAD_INPUT_MODE_DISABLED) {
|
||||
LOGE("Cannot not disable all inputs in OTG mode.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2928,18 +2919,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
|
||||
if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) {
|
||||
LOGE("Could not specify both --camera-id and --camera-facing");
|
||||
LOGE("Cannot specify both --camera-id and --camera-facing");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opts->camera_size) {
|
||||
if (opts->max_size) {
|
||||
LOGE("Could not specify both --camera-size and -m/--max-size");
|
||||
LOGE("Cannot specify both --camera-size and -m/--max-size");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opts->camera_ar) {
|
||||
LOGE("Could not specify both --camera-size and --camera-ar");
|
||||
LOGE("Cannot specify both --camera-size and --camera-ar");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -3080,19 +3071,19 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
|
||||
if (!opts->control) {
|
||||
if (opts->turn_screen_off) {
|
||||
LOGE("Could not request to turn screen off if control is disabled");
|
||||
LOGE("Cannot request to turn screen off if control is disabled");
|
||||
return false;
|
||||
}
|
||||
if (opts->stay_awake) {
|
||||
LOGE("Could not request to stay awake if control is disabled");
|
||||
LOGE("Cannot request to stay awake if control is disabled");
|
||||
return false;
|
||||
}
|
||||
if (opts->show_touches) {
|
||||
LOGE("Could not request to show touches if control is disabled");
|
||||
LOGE("Cannot request to show touches if control is disabled");
|
||||
return false;
|
||||
}
|
||||
if (opts->power_off_on_close) {
|
||||
LOGE("Could not request power off on close if control is disabled");
|
||||
LOGE("Cannot request power off on close if control is disabled");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -3117,7 +3108,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
// OTG mode is compatible with only very few options.
|
||||
// Only report obvious errors.
|
||||
if (opts->record_filename) {
|
||||
LOGE("OTG mode: could not record");
|
||||
LOGE("OTG mode: cannot record");
|
||||
return false;
|
||||
}
|
||||
if (opts->turn_screen_off) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
#define SC_CLOCK_NDEBUG // comment to debug
|
||||
//#define SC_CLOCK_DEBUG // uncomment to debug
|
||||
|
||||
#define SC_CLOCK_RANGE 32
|
||||
|
||||
@@ -21,10 +21,12 @@ sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
|
||||
}
|
||||
|
||||
sc_tick offset = system - stream;
|
||||
clock->offset = ((clock->range - 1) * clock->offset + offset)
|
||||
/ clock->range;
|
||||
unsigned clock_weight = clock->range - 1;
|
||||
unsigned value_weight = SC_CLOCK_RANGE - clock->range + 1;
|
||||
clock->offset = (clock->offset * clock_weight + offset * value_weight)
|
||||
/ SC_CLOCK_RANGE;
|
||||
|
||||
#ifndef SC_CLOCK_NDEBUG
|
||||
#ifdef SC_CLOCK_DEBUG
|
||||
LOGD("Clock estimation: pts + %" PRItick, clock->offset);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <libavutil/version.h>
|
||||
#include <SDL2/SDL_version.h>
|
||||
|
||||
#ifndef __WIN32
|
||||
#ifndef _WIN32
|
||||
# define PRIu64_ PRIu64
|
||||
# define SC_PRIsizet "zu"
|
||||
#else
|
||||
|
||||
@@ -83,15 +83,34 @@ write_position(uint8_t *buf, const struct sc_position *position) {
|
||||
sc_write16be(&buf[10], position->screen_size.height);
|
||||
}
|
||||
|
||||
// write length (4 bytes) + string (non null-terminated)
|
||||
// Write truncated string, and return the size
|
||||
static size_t
|
||||
write_string(const char *utf8, size_t max_len, uint8_t *buf) {
|
||||
write_string_payload(uint8_t *payload, const char *utf8, size_t max_len) {
|
||||
if (!utf8) {
|
||||
return 0;
|
||||
}
|
||||
size_t len = sc_str_utf8_truncation_index(utf8, max_len);
|
||||
memcpy(payload, utf8, len);
|
||||
return len;
|
||||
}
|
||||
|
||||
// Write length (4 bytes) + string (non null-terminated)
|
||||
static size_t
|
||||
write_string(uint8_t *buf, const char *utf8, size_t max_len) {
|
||||
size_t len = write_string_payload(buf + 4, utf8, max_len);
|
||||
sc_write32be(buf, len);
|
||||
memcpy(&buf[4], utf8, len);
|
||||
return 4 + len;
|
||||
}
|
||||
|
||||
// Write length (1 byte) + string (non null-terminated)
|
||||
static size_t
|
||||
write_string_tiny(uint8_t *buf, const char *utf8, size_t max_len) {
|
||||
assert(max_len <= 0xFF);
|
||||
size_t len = write_string_payload(buf + 1, utf8, max_len);
|
||||
buf[0] = len;
|
||||
return 1 + len;
|
||||
}
|
||||
|
||||
size_t
|
||||
sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
|
||||
buf[0] = msg->type;
|
||||
@@ -103,9 +122,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
|
||||
sc_write32be(&buf[10], msg->inject_keycode.metastate);
|
||||
return 14;
|
||||
case SC_CONTROL_MSG_TYPE_INJECT_TEXT: {
|
||||
size_t len =
|
||||
write_string(msg->inject_text.text,
|
||||
SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]);
|
||||
size_t len = write_string(&buf[1], msg->inject_text.text,
|
||||
SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
|
||||
return 1 + len;
|
||||
}
|
||||
case SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT:
|
||||
@@ -137,19 +155,26 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
|
||||
case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD:
|
||||
sc_write64be(&buf[1], msg->set_clipboard.sequence);
|
||||
buf[9] = !!msg->set_clipboard.paste;
|
||||
size_t len = write_string(msg->set_clipboard.text,
|
||||
SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH,
|
||||
&buf[10]);
|
||||
size_t len = write_string(&buf[10], msg->set_clipboard.text,
|
||||
SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH);
|
||||
return 10 + len;
|
||||
case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
|
||||
buf[1] = msg->set_screen_power_mode.mode;
|
||||
return 2;
|
||||
case SC_CONTROL_MSG_TYPE_UHID_CREATE:
|
||||
sc_write16be(&buf[1], msg->uhid_create.id);
|
||||
sc_write16be(&buf[3], msg->uhid_create.report_desc_size);
|
||||
memcpy(&buf[5], msg->uhid_create.report_desc,
|
||||
msg->uhid_create.report_desc_size);
|
||||
return 5 + msg->uhid_create.report_desc_size;
|
||||
|
||||
size_t index = 3;
|
||||
index += write_string_tiny(&buf[index], msg->uhid_create.name, 127);
|
||||
|
||||
sc_write16be(&buf[index], msg->uhid_create.report_desc_size);
|
||||
index += 2;
|
||||
|
||||
memcpy(&buf[index], msg->uhid_create.report_desc,
|
||||
msg->uhid_create.report_desc_size);
|
||||
index += msg->uhid_create.report_desc_size;
|
||||
|
||||
return index;
|
||||
case SC_CONTROL_MSG_TYPE_UHID_INPUT:
|
||||
sc_write16be(&buf[1], msg->uhid_input.id);
|
||||
sc_write16be(&buf[3], msg->uhid_input.size);
|
||||
@@ -255,10 +280,15 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
|
||||
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
|
||||
LOG_CMSG("rotate device");
|
||||
break;
|
||||
case SC_CONTROL_MSG_TYPE_UHID_CREATE:
|
||||
LOG_CMSG("UHID create [%" PRIu16 "] report_desc_size=%" PRIu16,
|
||||
msg->uhid_create.id, msg->uhid_create.report_desc_size);
|
||||
case SC_CONTROL_MSG_TYPE_UHID_CREATE: {
|
||||
// Quote only if name is not null
|
||||
const char *name = msg->uhid_create.name;
|
||||
const char *quote = name ? "\"" : "";
|
||||
LOG_CMSG("UHID create [%" PRIu16 "] name=%s%s%s "
|
||||
"report_desc_size=%" PRIu16, msg->uhid_create.id,
|
||||
quote, name, quote, msg->uhid_create.report_desc_size);
|
||||
break;
|
||||
}
|
||||
case SC_CONTROL_MSG_TYPE_UHID_INPUT: {
|
||||
char *hex = sc_str_to_hex_string(msg->uhid_input.data,
|
||||
msg->uhid_input.size);
|
||||
|
||||
@@ -98,6 +98,7 @@ struct sc_control_msg {
|
||||
} set_screen_power_mode;
|
||||
struct {
|
||||
uint16_t id;
|
||||
const char *name; // pointer to static data
|
||||
uint16_t report_desc_size;
|
||||
const uint8_t *report_desc; // pointer to static data
|
||||
} uhid_create;
|
||||
|
||||
@@ -116,7 +116,7 @@ sc_controller_push_msg(struct sc_controller *controller,
|
||||
LOG_OOM();
|
||||
}
|
||||
}
|
||||
// Otherwise (if the queue is full), the msg is discarded
|
||||
// Otherwise, the msg is discarded
|
||||
|
||||
sc_mutex_unlock(&controller->mutex);
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
#define SC_BUFFERING_NDEBUG // comment to debug
|
||||
|
||||
/** Downcast frame_sink to sc_delay_buffer */
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_delay_buffer, frame_sink)
|
||||
|
||||
@@ -80,7 +78,7 @@ run_buffering(void *data) {
|
||||
goto stopped;
|
||||
}
|
||||
|
||||
#ifndef SC_BUFFERING_NDEBUG
|
||||
#ifdef SC_BUFFERING_DEBUG
|
||||
LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick,
|
||||
pts, dframe.push_date, sc_tick_now());
|
||||
#endif
|
||||
@@ -134,6 +132,7 @@ sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink,
|
||||
|
||||
sc_clock_init(&db->clock);
|
||||
sc_vecdeque_init(&db->queue);
|
||||
db->stopped = false;
|
||||
|
||||
if (!sc_frame_source_sinks_open(&db->frame_source, ctx)) {
|
||||
goto error_destroy_wait_cond;
|
||||
@@ -206,7 +205,7 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink,
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifndef SC_BUFFERING_NDEBUG
|
||||
#ifdef SC_BUFFERING_DEBUG
|
||||
dframe.push_date = sc_tick_now();
|
||||
#endif
|
||||
|
||||
|
||||
@@ -12,12 +12,14 @@
|
||||
#include "util/tick.h"
|
||||
#include "util/vecdeque.h"
|
||||
|
||||
//#define SC_BUFFERING_DEBUG // uncomment to debug
|
||||
|
||||
// forward declarations
|
||||
typedef struct AVFrame AVFrame;
|
||||
|
||||
struct sc_delayed_frame {
|
||||
AVFrame *frame;
|
||||
#ifndef NDEBUG
|
||||
#ifdef SC_BUFFERING_DEBUG
|
||||
sc_tick push_date;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ sc_post_to_main_thread(sc_runnable_fn run, void *userdata) {
|
||||
LOGD("Could not post runnable to main thread (filtered)");
|
||||
} else {
|
||||
assert(ret < 0);
|
||||
LOGW("Coud not post to main thread: %s", SDL_GetError());
|
||||
LOGW("Could not post runnable to main thread: %s", SDL_GetError());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ struct sc_hid_input {
|
||||
|
||||
struct sc_hid_open {
|
||||
uint16_t hid_id;
|
||||
const char *name; // pointer to static memory
|
||||
const uint8_t *report_desc; // pointer to static memory
|
||||
size_t report_desc_size;
|
||||
};
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
#include "util/binary.h"
|
||||
#include "util/log.h"
|
||||
|
||||
// 2 bytes for left stick (X, Y)
|
||||
// 2 bytes for right stick (Z, Rz)
|
||||
// 2 bytes for L2/R2 triggers
|
||||
// 2x2 bytes for left stick (X, Y)
|
||||
// 2x2 bytes for right stick (Z, Rz)
|
||||
// 2x2 bytes for L2/R2 triggers
|
||||
// 2 bytes for buttons + padding,
|
||||
// 1 byte for hat switch (dpad) + padding
|
||||
#define SC_HID_GAMEPAD_EVENT_SIZE 15
|
||||
|
||||
// The ->buttons field stores the state for all buttons, but only some of them
|
||||
// (the 16 LSB) must be transmitted "as is". The DPAD (hat switch) are stored
|
||||
// locally in the MSB, but not transmitted as is: they are transformed to
|
||||
// generate another specific byte.
|
||||
// (the 16 LSB) must be transmitted "as is". The DPAD (hat switch) buttons are
|
||||
// stored locally in the MSB of this field, but not transmitted as is: they are
|
||||
// transformed to generate another specific byte.
|
||||
#define SC_HID_BUTTONS_MASK 0xFFFF
|
||||
|
||||
// outside SC_HID_BUTTONS_MASK
|
||||
@@ -26,7 +26,7 @@
|
||||
#define SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT UINT32_C(0x80000)
|
||||
|
||||
/**
|
||||
* Gamepad descriptor manually crafted to transmit the events.
|
||||
* Gamepad descriptor manually crafted to transmit the input reports.
|
||||
*
|
||||
* The HID specification is available here:
|
||||
* <https://www.usb.org/document-library/device-class-definition-hid-111>
|
||||
@@ -90,7 +90,7 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
|
||||
// Usage Minimum (1)
|
||||
0x19, 0x01,
|
||||
// Usage Maximum (16)
|
||||
0x29, 0x0F,
|
||||
0x29, 0x10,
|
||||
// Logical Minimum (0)
|
||||
0x15, 0x00,
|
||||
// Logical Maximum (1)
|
||||
@@ -117,7 +117,6 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
|
||||
// Input (Data, Variable, Null State): 4-bit value
|
||||
0x81, 0x42,
|
||||
|
||||
|
||||
// End Collection
|
||||
0xC0,
|
||||
|
||||
@@ -126,7 +125,7 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A gamepad HID event is 15 bytes long:
|
||||
* A gamepad HID input report is 15 bytes long:
|
||||
* - bytes 0-3: left stick state
|
||||
* - bytes 4-7: right stick state
|
||||
* - bytes 8-11: L2/R2 triggers state
|
||||
@@ -135,28 +134,28 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
|
||||
*
|
||||
* +---------------+
|
||||
* byte 0: |. . . . . . . .|
|
||||
* | | left stick x state (0-65535)
|
||||
* | | left stick x (0-65535, little-endian)
|
||||
* byte 1: |. . . . . . . .|
|
||||
* +---------------+
|
||||
* byte 2: |. . . . . . . .|
|
||||
* | | left stick y state (0-65535)
|
||||
* | | left stick y (0-65535, little-endian)
|
||||
* byte 3: |. . . . . . . .|
|
||||
* +---------------+
|
||||
* byte 4: |. . . . . . . .|
|
||||
* | | right stick x state (0-65535)
|
||||
* | | right stick x (0-65535, little-endian)
|
||||
* byte 5: |. . . . . . . .|
|
||||
* +---------------+
|
||||
* byte 6: |. . . . . . . .|
|
||||
* | | right stick y state (0-65535)
|
||||
* | | right stick y (0-65535, little-endian)
|
||||
* byte 7: |. . . . . . . .|
|
||||
* +---------------+
|
||||
* byte 8: |. . . . . . . .|
|
||||
* | | L2 trigger state (0-65535)
|
||||
* byte 9: |. . . . . . . .|
|
||||
* | | L2 trigger (0-32767, little-endian)
|
||||
* byte 9: |0 . . . . . . .|
|
||||
* +---------------+
|
||||
* byte 10: |. . . . . . . .|
|
||||
* | | R2 trigger state (0-65535)
|
||||
* byte 11: |. . . . . . . .|
|
||||
* | | R2 trigger (0-32767, little-endian)
|
||||
* byte 11: |0 . . . . . . .|
|
||||
* +---------------+
|
||||
*
|
||||
* ,--------------- SC_GAMEPAD_BUTTON_RIGHT_SHOULDER
|
||||
@@ -244,8 +243,14 @@ sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid,
|
||||
|
||||
sc_hid_gamepad_slot_init(&hid->slots[slot_idx], gamepad_id);
|
||||
|
||||
SDL_GameController* game_controller =
|
||||
SDL_GameControllerFromInstanceID(gamepad_id);
|
||||
assert(game_controller);
|
||||
const char *name = SDL_GameControllerName(game_controller);
|
||||
|
||||
uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx);
|
||||
hid_open->hid_id = hid_id;
|
||||
hid_open->name = name;
|
||||
hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC;
|
||||
hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "input_events.h"
|
||||
|
||||
#define SC_MAX_GAMEPADS 8
|
||||
#define SC_HID_ID_GAMEPAD_FIRST 2
|
||||
#define SC_HID_ID_GAMEPAD_FIRST 3
|
||||
#define SC_HID_ID_GAMEPAD_LAST (SC_HID_ID_GAMEPAD_FIRST + SC_MAX_GAMEPADS - 1)
|
||||
|
||||
struct sc_hid_gamepad_slot {
|
||||
|
||||
@@ -125,7 +125,7 @@ static const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A keyboard HID event is 8 bytes long:
|
||||
* A keyboard HID input report is 8 bytes long:
|
||||
*
|
||||
* - byte 0: modifiers (1 flag per modifier key, 8 possible modifier keys)
|
||||
* - byte 1: reserved (always 0)
|
||||
@@ -335,6 +335,7 @@ sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input,
|
||||
|
||||
void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) {
|
||||
hid_open->hid_id = SC_HID_ID_KEYBOARD;
|
||||
hid_open->name = NULL; // No name specified after "scrcpy"
|
||||
hid_open->report_desc = SC_HID_KEYBOARD_REPORT_DESC;
|
||||
hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* <https://www.usb.org/document-library/hid-usage-tables-15>
|
||||
* §4 Generic Desktop Page (0x01) (p32)
|
||||
*/
|
||||
const uint8_t SC_HID_MOUSE_REPORT_DESC[] = {
|
||||
static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = {
|
||||
// Usage Page (Generic Desktop)
|
||||
0x05, 0x01,
|
||||
// Usage (Mouse)
|
||||
@@ -81,7 +81,7 @@ const uint8_t SC_HID_MOUSE_REPORT_DESC[] = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A mouse HID event is 4 bytes long:
|
||||
* A mouse HID input report is 4 bytes long:
|
||||
*
|
||||
* - byte 0: buttons state
|
||||
* - byte 1: relative x motion (signed byte from -127 to 127)
|
||||
@@ -190,6 +190,7 @@ sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
|
||||
|
||||
void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) {
|
||||
hid_open->hid_id = SC_HID_ID_MOUSE;
|
||||
hid_open->name = NULL; // No name specified after "scrcpy"
|
||||
hid_open->report_desc = SC_HID_MOUSE_REPORT_DESC;
|
||||
hid_open->report_desc_size = sizeof(SC_HID_MOUSE_REPORT_DESC);
|
||||
}
|
||||
|
||||
@@ -516,7 +516,7 @@ sc_gamepad_device_event_type_from_sdl_type(uint32_t type) {
|
||||
static inline enum sc_gamepad_axis
|
||||
sc_gamepad_axis_from_sdl(uint8_t axis) {
|
||||
if (axis <= SDL_CONTROLLER_AXIS_TRIGGERRIGHT) {
|
||||
// SDL_GAMEPAD_AXIS_* constants are initialized from
|
||||
// SC_GAMEPAD_AXIS_* constants are initialized from
|
||||
// SDL_CONTROLLER_AXIS_*
|
||||
return axis;
|
||||
}
|
||||
|
||||
@@ -5,53 +5,9 @@
|
||||
|
||||
#include "input_events.h"
|
||||
#include "screen.h"
|
||||
#include "shortcut_mod.h"
|
||||
#include "util/log.h"
|
||||
|
||||
#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI)
|
||||
|
||||
static inline uint16_t
|
||||
to_sdl_mod(uint8_t shortcut_mod) {
|
||||
uint16_t sdl_mod = 0;
|
||||
if (shortcut_mod & SC_SHORTCUT_MOD_LCTRL) {
|
||||
sdl_mod |= KMOD_LCTRL;
|
||||
}
|
||||
if (shortcut_mod & SC_SHORTCUT_MOD_RCTRL) {
|
||||
sdl_mod |= KMOD_RCTRL;
|
||||
}
|
||||
if (shortcut_mod & SC_SHORTCUT_MOD_LALT) {
|
||||
sdl_mod |= KMOD_LALT;
|
||||
}
|
||||
if (shortcut_mod & SC_SHORTCUT_MOD_RALT) {
|
||||
sdl_mod |= KMOD_RALT;
|
||||
}
|
||||
if (shortcut_mod & SC_SHORTCUT_MOD_LSUPER) {
|
||||
sdl_mod |= KMOD_LGUI;
|
||||
}
|
||||
if (shortcut_mod & SC_SHORTCUT_MOD_RSUPER) {
|
||||
sdl_mod |= KMOD_RGUI;
|
||||
}
|
||||
return sdl_mod;
|
||||
}
|
||||
|
||||
static bool
|
||||
is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) {
|
||||
// keep only the relevant modifier keys
|
||||
sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK;
|
||||
|
||||
// at least one shortcut mod pressed?
|
||||
return sdl_mod & im->sdl_shortcut_mods;
|
||||
}
|
||||
|
||||
static bool
|
||||
is_shortcut_key(struct sc_input_manager *im, SDL_Keycode keycode) {
|
||||
return (im->sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL)
|
||||
|| (im->sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL)
|
||||
|| (im->sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT)
|
||||
|| (im->sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT)
|
||||
|| (im->sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI)
|
||||
|| (im->sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI);
|
||||
}
|
||||
|
||||
void
|
||||
sc_input_manager_init(struct sc_input_manager *im,
|
||||
const struct sc_input_manager_params *params) {
|
||||
@@ -73,7 +29,7 @@ sc_input_manager_init(struct sc_input_manager *im,
|
||||
im->legacy_paste = params->legacy_paste;
|
||||
im->clipboard_autosync = params->clipboard_autosync;
|
||||
|
||||
im->sdl_shortcut_mods = to_sdl_mod(params->shortcut_mods);
|
||||
im->sdl_shortcut_mods = sc_shortcut_mods_to_sdl(params->shortcut_mods);
|
||||
|
||||
im->vfinger_down = false;
|
||||
im->vfinger_invert_x = false;
|
||||
@@ -346,7 +302,8 @@ sc_input_manager_process_text_input(struct sc_input_manager *im,
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_shortcut_mod(im, SDL_GetModState())) {
|
||||
if (sc_shortcut_mods_is_shortcut_mod(im->sdl_shortcut_mods,
|
||||
SDL_GetModState())) {
|
||||
// A shortcut must never generate text events
|
||||
return;
|
||||
}
|
||||
@@ -402,7 +359,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
bool paused = im->screen->paused;
|
||||
bool video = im->screen->video;
|
||||
|
||||
SDL_Keycode keycode = event->keysym.sym;
|
||||
SDL_Keycode sdl_keycode = event->keysym.sym;
|
||||
uint16_t mod = event->keysym.mod;
|
||||
bool down = event->type == SDL_KEYDOWN;
|
||||
bool ctrl = event->keysym.mod & KMOD_CTRL;
|
||||
@@ -413,22 +370,23 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
// press/release is a modifier key.
|
||||
// The second condition is necessary to ignore the release of the modifier
|
||||
// key (because in this case mod is 0).
|
||||
bool is_shortcut = is_shortcut_mod(im, mod)
|
||||
|| is_shortcut_key(im, keycode);
|
||||
uint16_t mods = im->sdl_shortcut_mods;
|
||||
bool is_shortcut = sc_shortcut_mods_is_shortcut_mod(mods, mod)
|
||||
|| sc_shortcut_mods_is_shortcut_key(mods, sdl_keycode);
|
||||
|
||||
if (down && !repeat) {
|
||||
if (keycode == im->last_keycode && mod == im->last_mod) {
|
||||
if (sdl_keycode == im->last_keycode && mod == im->last_mod) {
|
||||
++im->key_repeat;
|
||||
} else {
|
||||
im->key_repeat = 0;
|
||||
im->last_keycode = keycode;
|
||||
im->last_keycode = sdl_keycode;
|
||||
im->last_mod = mod;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_shortcut) {
|
||||
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
|
||||
switch (keycode) {
|
||||
switch (sdl_keycode) {
|
||||
case SDLK_h:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_home(im, action);
|
||||
@@ -536,7 +494,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
return;
|
||||
case SDLK_f:
|
||||
if (video && !shift && !repeat && down) {
|
||||
sc_screen_switch_fullscreen(im->screen);
|
||||
sc_screen_toggle_fullscreen(im->screen);
|
||||
}
|
||||
return;
|
||||
case SDLK_w:
|
||||
@@ -587,7 +545,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
}
|
||||
|
||||
uint64_t ack_to_wait = SC_SEQUENCE_INVALID;
|
||||
bool is_ctrl_v = ctrl && !shift && keycode == SDLK_v && down && !repeat;
|
||||
bool is_ctrl_v = ctrl && !shift && sdl_keycode == SDLK_v && down && !repeat;
|
||||
if (im->clipboard_autosync && is_ctrl_v) {
|
||||
if (im->legacy_paste) {
|
||||
// inject the text as input events
|
||||
@@ -615,10 +573,20 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
}
|
||||
}
|
||||
|
||||
enum sc_keycode keycode = sc_keycode_from_sdl(sdl_keycode);
|
||||
if (keycode == SC_KEYCODE_UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
enum sc_scancode scancode = sc_scancode_from_sdl(event->keysym.scancode);
|
||||
if (scancode == SC_SCANCODE_UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct sc_key_event evt = {
|
||||
.action = sc_action_from_sdl_keyboard_type(event->type),
|
||||
.keycode = sc_keycode_from_sdl(event->keysym.sym),
|
||||
.scancode = sc_scancode_from_sdl(event->keysym.scancode),
|
||||
.keycode = keycode,
|
||||
.scancode = scancode,
|
||||
.repeat = event->repeat,
|
||||
.mods_state = sc_mods_state_from_sdl(event->keysym.mod),
|
||||
};
|
||||
@@ -741,6 +709,10 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
||||
bool down = event->type == SDL_MOUSEBUTTONDOWN;
|
||||
|
||||
enum sc_mouse_button button = sc_mouse_button_from_sdl(event->button);
|
||||
if (button == SC_MOUSE_BUTTON_UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!down) {
|
||||
// Mark the button as released
|
||||
im->mouse_buttons_state &= ~button;
|
||||
@@ -822,14 +794,14 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
||||
}
|
||||
|
||||
bool change_vfinger = event->button == SDL_BUTTON_LEFT &&
|
||||
((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) ||
|
||||
((down && !im->vfinger_down && (ctrl_pressed || shift_pressed)) ||
|
||||
(!down && im->vfinger_down));
|
||||
bool use_finger = im->vfinger_down || change_vfinger;
|
||||
|
||||
struct sc_mouse_click_event evt = {
|
||||
.position = sc_input_manager_get_position(im, event->x, event->y),
|
||||
.action = sc_action_from_sdl_mousebutton_type(event->type),
|
||||
.button = sc_mouse_button_from_sdl(event->button),
|
||||
.button = button,
|
||||
.pointer_id = use_finger ? SC_POINTER_ID_GENERIC_FINGER
|
||||
: SC_POINTER_ID_MOUSE,
|
||||
.buttons_state = im->mouse_buttons_state,
|
||||
@@ -854,16 +826,28 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
||||
// In other words, the center of the rotation/scaling is the center of the
|
||||
// screen.
|
||||
//
|
||||
// To simulate a tilt gesture (a vertical slide with two fingers), Shift
|
||||
// can be used instead of Ctrl. The "virtual finger" has a position
|
||||
// To simulate a vertical tilt gesture (a vertical slide with two fingers),
|
||||
// Shift can be used instead of Ctrl. The "virtual finger" has a position
|
||||
// inverted with respect to the vertical axis of symmetry in the middle of
|
||||
// the screen.
|
||||
//
|
||||
// To simulate a horizontal tilt gesture (a horizontal slide with two
|
||||
// fingers), Ctrl+Shift can be used. The "virtual finger" has a position
|
||||
// inverted with respect to the horizontal axis of symmetry in the middle
|
||||
// of the screen. It is expected to be less frequently used, that's why the
|
||||
// one-mod shortcuts are assigned to rotation and vertical tilt.
|
||||
if (change_vfinger) {
|
||||
struct sc_point mouse =
|
||||
sc_screen_convert_window_to_frame_coords(im->screen, event->x,
|
||||
event->y);
|
||||
if (down) {
|
||||
im->vfinger_invert_x = ctrl_pressed || shift_pressed;
|
||||
// Ctrl Shift invert_x invert_y
|
||||
// ---- ----- ==> -------- --------
|
||||
// 0 0 0 0 -
|
||||
// 0 1 1 0 vertical tilt
|
||||
// 1 0 1 1 rotate
|
||||
// 1 1 0 1 horizontal tilt
|
||||
im->vfinger_invert_x = ctrl_pressed ^ shift_pressed;
|
||||
im->vfinger_invert_y = ctrl_pressed;
|
||||
}
|
||||
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size,
|
||||
@@ -950,10 +934,15 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
|
||||
|
||||
static void
|
||||
sc_input_manager_process_gamepad_axis(struct sc_input_manager *im,
|
||||
const SDL_ControllerAxisEvent *event) {
|
||||
const SDL_ControllerAxisEvent *event) {
|
||||
enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis);
|
||||
if (axis == SC_GAMEPAD_AXIS_UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct sc_gamepad_axis_event evt = {
|
||||
.gamepad_id = event->which,
|
||||
.axis = sc_gamepad_axis_from_sdl(event->axis),
|
||||
.axis = axis,
|
||||
.value = event->value,
|
||||
};
|
||||
im->gp->ops->process_gamepad_axis(im->gp, &evt);
|
||||
@@ -962,10 +951,15 @@ sc_input_manager_process_gamepad_axis(struct sc_input_manager *im,
|
||||
static void
|
||||
sc_input_manager_process_gamepad_button(struct sc_input_manager *im,
|
||||
const SDL_ControllerButtonEvent *event) {
|
||||
enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button);
|
||||
if (button == SC_GAMEPAD_BUTTON_UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct sc_gamepad_button_event evt = {
|
||||
.gamepad_id = event->which,
|
||||
.action = sc_action_from_sdl_controllerbutton_type(event->type),
|
||||
.button = sc_gamepad_button_from_sdl(event->button),
|
||||
.button = button,
|
||||
};
|
||||
im->gp->ops->process_gamepad_button(im->gp, &evt);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,10 @@ convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod,
|
||||
{SC_KEYCODE_RCTRL, AKEYCODE_CTRL_RIGHT},
|
||||
{SC_KEYCODE_LSHIFT, AKEYCODE_SHIFT_LEFT},
|
||||
{SC_KEYCODE_RSHIFT, AKEYCODE_SHIFT_RIGHT},
|
||||
{SC_KEYCODE_LALT, AKEYCODE_ALT_LEFT},
|
||||
{SC_KEYCODE_RALT, AKEYCODE_ALT_RIGHT},
|
||||
{SC_KEYCODE_LGUI, AKEYCODE_META_LEFT},
|
||||
{SC_KEYCODE_RGUI, AKEYCODE_META_RIGHT},
|
||||
};
|
||||
|
||||
// Numpad navigation keys.
|
||||
@@ -166,11 +170,7 @@ convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mod & (SC_MOD_LALT | SC_MOD_RALT | SC_MOD_LGUI | SC_MOD_RGUI)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if ALT and META are not pressed, also handle letters and space
|
||||
// Handle letters and space
|
||||
entry = SC_INTMAP_FIND_ENTRY(alphaspace_keys, from);
|
||||
if (entry) {
|
||||
*to = entry->value;
|
||||
|
||||
123
app/src/mouse_capture.c
Normal file
123
app/src/mouse_capture.c
Normal file
@@ -0,0 +1,123 @@
|
||||
#include "mouse_capture.h"
|
||||
|
||||
#include "shortcut_mod.h"
|
||||
#include "util/log.h"
|
||||
|
||||
void
|
||||
sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window,
|
||||
uint8_t shortcut_mods) {
|
||||
mc->window = window;
|
||||
mc->sdl_mouse_capture_keys = sc_shortcut_mods_to_sdl(shortcut_mods);
|
||||
mc->mouse_capture_key_pressed = SDLK_UNKNOWN;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
sc_mouse_capture_is_capture_key(struct sc_mouse_capture *mc, SDL_Keycode key) {
|
||||
return sc_shortcut_mods_is_shortcut_key(mc->sdl_mouse_capture_keys, key);
|
||||
}
|
||||
|
||||
bool
|
||||
sc_mouse_capture_handle_event(struct sc_mouse_capture *mc,
|
||||
const SDL_Event *event) {
|
||||
switch (event->type) {
|
||||
case SDL_WINDOWEVENT:
|
||||
if (event->window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
|
||||
sc_mouse_capture_set_active(mc, false);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case SDL_KEYDOWN: {
|
||||
SDL_Keycode key = event->key.keysym.sym;
|
||||
if (sc_mouse_capture_is_capture_key(mc, key)) {
|
||||
if (!mc->mouse_capture_key_pressed) {
|
||||
mc->mouse_capture_key_pressed = key;
|
||||
} else {
|
||||
// Another mouse capture key has been pressed, cancel
|
||||
// mouse (un)capture
|
||||
mc->mouse_capture_key_pressed = 0;
|
||||
}
|
||||
// Mouse capture keys are never forwarded to the device
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_KEYUP: {
|
||||
SDL_Keycode key = event->key.keysym.sym;
|
||||
SDL_Keycode cap = mc->mouse_capture_key_pressed;
|
||||
mc->mouse_capture_key_pressed = 0;
|
||||
if (sc_mouse_capture_is_capture_key(mc, key)) {
|
||||
if (key == cap) {
|
||||
// A mouse capture key has been pressed then released:
|
||||
// toggle the capture mouse mode
|
||||
sc_mouse_capture_toggle(mc);
|
||||
}
|
||||
// Mouse capture keys are never forwarded to the device
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_MOUSEWHEEL:
|
||||
case SDL_MOUSEMOTION:
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
if (!sc_mouse_capture_is_active(mc)) {
|
||||
// The mouse will be captured on SDL_MOUSEBUTTONUP, so consume
|
||||
// the event
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
if (!sc_mouse_capture_is_active(mc)) {
|
||||
sc_mouse_capture_set_active(mc, true);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case SDL_FINGERMOTION:
|
||||
case SDL_FINGERDOWN:
|
||||
case SDL_FINGERUP:
|
||||
// Touch events are not compatible with relative mode
|
||||
// (coordinates are not relative), so consume the event
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture) {
|
||||
#ifdef __APPLE__
|
||||
// Workaround for SDL bug on macOS:
|
||||
// <https://github.com/libsdl-org/SDL/issues/5340>
|
||||
if (capture) {
|
||||
int mouse_x, mouse_y;
|
||||
SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
|
||||
|
||||
int x, y, w, h;
|
||||
SDL_GetWindowPosition(window, &x, &y);
|
||||
SDL_GetWindowSize(window, &w, &h);
|
||||
|
||||
bool outside_window = mouse_x < x || mouse_x >= x + w
|
||||
|| mouse_y < y || mouse_y >= y + h;
|
||||
if (outside_window) {
|
||||
SDL_WarpMouseInWindow(mc->window, w / 2, h / 2);
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void) mc;
|
||||
#endif
|
||||
if (SDL_SetRelativeMouseMode(capture)) {
|
||||
LOGE("Could not set relative mouse mode to %s: %s",
|
||||
capture ? "true" : "false", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
sc_mouse_capture_is_active(struct sc_mouse_capture *mc) {
|
||||
(void) mc;
|
||||
return SDL_GetRelativeMouseMode();
|
||||
}
|
||||
|
||||
void
|
||||
sc_mouse_capture_toggle(struct sc_mouse_capture *mc) {
|
||||
bool new_value = !sc_mouse_capture_is_active(mc);
|
||||
sc_mouse_capture_set_active(mc, new_value);
|
||||
}
|
||||
38
app/src/mouse_capture.h
Normal file
38
app/src/mouse_capture.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef SC_MOUSE_CAPTURE_H
|
||||
#define SC_MOUSE_CAPTURE_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
struct sc_mouse_capture {
|
||||
SDL_Window *window;
|
||||
uint16_t sdl_mouse_capture_keys;
|
||||
|
||||
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
|
||||
// RGUI) must be pressed. This variable tracks the pressed capture key.
|
||||
SDL_Keycode mouse_capture_key_pressed;
|
||||
|
||||
};
|
||||
|
||||
void
|
||||
sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window,
|
||||
uint8_t shortcut_mods);
|
||||
|
||||
void
|
||||
sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture);
|
||||
|
||||
bool
|
||||
sc_mouse_capture_is_active(struct sc_mouse_capture *mc);
|
||||
|
||||
void
|
||||
sc_mouse_capture_toggle(struct sc_mouse_capture *mc);
|
||||
|
||||
// Return true if it consumed the event
|
||||
bool
|
||||
sc_mouse_capture_handle_event(struct sc_mouse_capture *mc,
|
||||
const SDL_Event *event);
|
||||
|
||||
#endif
|
||||
@@ -23,7 +23,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.record_format = SC_RECORD_FORMAT_AUTO,
|
||||
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AUTO,
|
||||
.mouse_input_mode = SC_MOUSE_INPUT_MODE_AUTO,
|
||||
.gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_AUTO,
|
||||
.gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_DISABLED,
|
||||
.mouse_bindings = {
|
||||
.pri = {
|
||||
.right_click = SC_MOUSE_BINDING_AUTO,
|
||||
@@ -49,7 +49,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.max_size = 0,
|
||||
.video_bit_rate = 0,
|
||||
.audio_bit_rate = 0,
|
||||
.max_fps = 0,
|
||||
.max_fps = NULL,
|
||||
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
|
||||
.display_orientation = SC_ORIENTATION_0,
|
||||
.record_orientation = SC_ORIENTATION_0,
|
||||
|
||||
@@ -159,9 +159,8 @@ enum sc_mouse_input_mode {
|
||||
};
|
||||
|
||||
enum sc_gamepad_input_mode {
|
||||
SC_GAMEPAD_INPUT_MODE_AUTO,
|
||||
SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode
|
||||
SC_GAMEPAD_INPUT_MODE_DISABLED,
|
||||
SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode
|
||||
SC_GAMEPAD_INPUT_MODE_UHID,
|
||||
SC_GAMEPAD_INPUT_MODE_AOA,
|
||||
};
|
||||
@@ -251,7 +250,7 @@ struct scrcpy_options {
|
||||
uint16_t max_size;
|
||||
uint32_t video_bit_rate;
|
||||
uint32_t audio_bit_rate;
|
||||
uint16_t max_fps;
|
||||
const char *max_fps; // float to be parsed by the server
|
||||
enum sc_lock_video_orientation lock_video_orientation;
|
||||
enum sc_orientation display_orientation;
|
||||
enum sc_orientation record_orientation;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "events.h"
|
||||
#include "util/log.h"
|
||||
#include "util/str.h"
|
||||
#include "util/thread.h"
|
||||
|
||||
struct sc_uhid_output_task_data {
|
||||
struct sc_uhid_devices *uhid_devices;
|
||||
@@ -43,6 +44,8 @@ sc_receiver_destroy(struct sc_receiver *receiver) {
|
||||
|
||||
static void
|
||||
task_set_clipboard(void *userdata) {
|
||||
assert(sc_thread_get_id() == SC_MAIN_THREAD_ID);
|
||||
|
||||
char *text = userdata;
|
||||
|
||||
char *current = SDL_GetClipboardText();
|
||||
@@ -50,17 +53,18 @@ task_set_clipboard(void *userdata) {
|
||||
SDL_free(current);
|
||||
if (same) {
|
||||
LOGD("Computer clipboard unchanged");
|
||||
free(text);
|
||||
return;
|
||||
} else {
|
||||
LOGI("Device clipboard copied");
|
||||
SDL_SetClipboardText(text);
|
||||
}
|
||||
|
||||
LOGI("Device clipboard copied");
|
||||
SDL_SetClipboardText(text);
|
||||
free(text);
|
||||
}
|
||||
|
||||
static void
|
||||
task_uhid_output(void *userdata) {
|
||||
assert(sc_thread_get_id() == SC_MAIN_THREAD_ID);
|
||||
|
||||
struct sc_uhid_output_task_data *data = userdata;
|
||||
|
||||
sc_uhid_devices_process_hid_output(data->uhid_devices, data->id, data->data,
|
||||
@@ -117,11 +121,6 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) {
|
||||
}
|
||||
}
|
||||
|
||||
// This is a programming error to receive this message if there is
|
||||
// no uhid_devices instance
|
||||
assert(receiver->uhid_devices);
|
||||
|
||||
// Also check at runtime (do not trust the server)
|
||||
if (!receiver->uhid_devices) {
|
||||
LOGE("Received unexpected HID output message");
|
||||
sc_device_msg_destroy(msg);
|
||||
|
||||
@@ -65,8 +65,8 @@ struct scrcpy {
|
||||
struct sc_aoa aoa;
|
||||
// sequence/ack helper to synchronize clipboard and Ctrl+v via HID
|
||||
struct sc_acksync acksync;
|
||||
struct sc_uhid_devices uhid_devices;
|
||||
#endif
|
||||
struct sc_uhid_devices uhid_devices;
|
||||
union {
|
||||
struct sc_keyboard_sdk keyboard_sdk;
|
||||
struct sc_keyboard_uhid keyboard_uhid;
|
||||
@@ -136,6 +136,10 @@ sdl_set_hints(const char *render_driver) {
|
||||
if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) {
|
||||
LOGW("Could not disable minimize on focus loss");
|
||||
}
|
||||
|
||||
if (!SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1")) {
|
||||
LOGW("Could not allow joystick background events");
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -185,11 +189,12 @@ event_loop(struct scrcpy *s) {
|
||||
case SDL_QUIT:
|
||||
LOGD("User requested to quit");
|
||||
return SCRCPY_EXIT_SUCCESS;
|
||||
case SC_EVENT_RUN_ON_MAIN_THREAD:
|
||||
case SC_EVENT_RUN_ON_MAIN_THREAD: {
|
||||
sc_runnable_fn run = event.user.data1;
|
||||
void *userdata = event.user.data2;
|
||||
run(userdata);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (!sc_screen_handle_event(&s->screen, &event)) {
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
@@ -724,7 +729,6 @@ aoa_complete:
|
||||
#endif
|
||||
|
||||
struct sc_keyboard_uhid *uhid_keyboard = NULL;
|
||||
struct sc_gamepad_uhid *uhid_gamepad = NULL;
|
||||
|
||||
if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_SDK) {
|
||||
sc_keyboard_sdk_init(&s->keyboard_sdk, &s->controller,
|
||||
@@ -759,8 +763,8 @@ aoa_complete:
|
||||
}
|
||||
|
||||
struct sc_uhid_devices *uhid_devices = NULL;
|
||||
if (uhid_keyboard || uhid_gamepad) {
|
||||
sc_uhid_devices_init(&s->uhid_devices, uhid_keyboard, uhid_gamepad);
|
||||
if (uhid_keyboard) {
|
||||
sc_uhid_devices_init(&s->uhid_devices, uhid_keyboard);
|
||||
uhid_devices = &s->uhid_devices;
|
||||
}
|
||||
|
||||
@@ -897,8 +901,8 @@ aoa_complete:
|
||||
timeout_started = true;
|
||||
}
|
||||
|
||||
bool use_gamepads = true;
|
||||
if (use_gamepads) {
|
||||
if (options->control
|
||||
&& options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) {
|
||||
init_sdl_gamepads();
|
||||
}
|
||||
|
||||
|
||||
135
app/src/screen.c
135
app/src/screen.c
@@ -162,47 +162,6 @@ sc_screen_is_relative_mode(struct sc_screen *screen) {
|
||||
return screen->im.mp && screen->im.mp->relative_mode;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_screen_set_mouse_capture(struct sc_screen *screen, bool capture) {
|
||||
#ifdef __APPLE__
|
||||
// Workaround for SDL bug on macOS:
|
||||
// <https://github.com/libsdl-org/SDL/issues/5340>
|
||||
if (capture) {
|
||||
int mouse_x, mouse_y;
|
||||
SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
|
||||
|
||||
int x, y, w, h;
|
||||
SDL_GetWindowPosition(screen->window, &x, &y);
|
||||
SDL_GetWindowSize(screen->window, &w, &h);
|
||||
|
||||
bool outside_window = mouse_x < x || mouse_x >= x + w
|
||||
|| mouse_y < y || mouse_y >= y + h;
|
||||
if (outside_window) {
|
||||
SDL_WarpMouseInWindow(screen->window, w / 2, h / 2);
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void) screen;
|
||||
#endif
|
||||
if (SDL_SetRelativeMouseMode(capture)) {
|
||||
LOGE("Could not set relative mouse mode to %s: %s",
|
||||
capture ? "true" : "false", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
static inline bool
|
||||
sc_screen_get_mouse_capture(struct sc_screen *screen) {
|
||||
(void) screen;
|
||||
return SDL_GetRelativeMouseMode();
|
||||
}
|
||||
|
||||
static inline void
|
||||
sc_screen_toggle_mouse_capture(struct sc_screen *screen) {
|
||||
(void) screen;
|
||||
bool new_value = !sc_screen_get_mouse_capture(screen);
|
||||
sc_screen_set_mouse_capture(screen, new_value);
|
||||
}
|
||||
|
||||
static void
|
||||
sc_screen_update_content_rect(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
@@ -299,6 +258,12 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink,
|
||||
|
||||
struct sc_screen *screen = DOWNCAST(sink);
|
||||
|
||||
if (ctx->width <= 0 || ctx->width > 0xFFFF
|
||||
|| ctx->height <= 0 || ctx->height > 0xFFFF) {
|
||||
LOGE("Invalid video size: %dx%d", ctx->width, ctx->height);
|
||||
return false;
|
||||
}
|
||||
|
||||
assert(ctx->width > 0 && ctx->width <= 0xFFFF);
|
||||
assert(ctx->height > 0 && ctx->height <= 0xFFFF);
|
||||
// screen->frame_size is never used before the event is pushed, and the
|
||||
@@ -309,7 +274,6 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink,
|
||||
// Post the event on the UI thread (the texture must be created from there)
|
||||
bool ok = sc_push_event(SC_EVENT_SCREEN_INIT_SIZE);
|
||||
if (!ok) {
|
||||
LOGW("Could not post init size event: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -351,7 +315,6 @@ sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
|
||||
// Post the event on the UI thread
|
||||
bool ok = sc_push_event(SC_EVENT_NEW_FRAME);
|
||||
if (!ok) {
|
||||
LOGW("Could not post new frame event: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -367,7 +330,6 @@ sc_screen_init(struct sc_screen *screen,
|
||||
screen->fullscreen = false;
|
||||
screen->maximized = false;
|
||||
screen->minimized = false;
|
||||
screen->mouse_capture_key_pressed = 0;
|
||||
screen->paused = false;
|
||||
screen->resume_frame = NULL;
|
||||
screen->orientation = SC_ORIENTATION_0;
|
||||
@@ -482,6 +444,9 @@ sc_screen_init(struct sc_screen *screen,
|
||||
|
||||
sc_input_manager_init(&screen->im, &im_params);
|
||||
|
||||
// Initialize even if not used for simplicity
|
||||
sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods);
|
||||
|
||||
#ifdef CONTINUOUS_RESIZING_WORKAROUND
|
||||
if (screen->video) {
|
||||
SDL_AddEventWatch(event_watcher, screen);
|
||||
@@ -502,7 +467,7 @@ sc_screen_init(struct sc_screen *screen,
|
||||
|
||||
if (!screen->video && sc_screen_is_relative_mode(screen)) {
|
||||
// Capture mouse immediately if video mirroring is disabled
|
||||
sc_screen_set_mouse_capture(screen, true);
|
||||
sc_mouse_capture_set_active(&screen->mc, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -534,7 +499,7 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
|
||||
SDL_SetWindowPosition(screen->window, x, y);
|
||||
|
||||
if (screen->req.fullscreen) {
|
||||
sc_screen_switch_fullscreen(screen);
|
||||
sc_screen_toggle_fullscreen(screen);
|
||||
}
|
||||
|
||||
if (screen->req.start_fps_counter) {
|
||||
@@ -709,7 +674,7 @@ sc_screen_apply_frame(struct sc_screen *screen) {
|
||||
|
||||
if (sc_screen_is_relative_mode(screen)) {
|
||||
// Capture mouse on start
|
||||
sc_screen_set_mouse_capture(screen, true);
|
||||
sc_mouse_capture_set_active(&screen->mc, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,7 +735,7 @@ sc_screen_set_paused(struct sc_screen *screen, bool paused) {
|
||||
}
|
||||
|
||||
void
|
||||
sc_screen_switch_fullscreen(struct sc_screen *screen) {
|
||||
sc_screen_toggle_fullscreen(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
@@ -833,15 +798,8 @@ sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
|
||||
content_size.height);
|
||||
}
|
||||
|
||||
static inline bool
|
||||
sc_screen_is_mouse_capture_key(SDL_Keycode key) {
|
||||
return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
|
||||
bool relative_mode = sc_screen_is_relative_mode(screen);
|
||||
|
||||
switch (event->type) {
|
||||
case SC_EVENT_SCREEN_INIT_SIZE: {
|
||||
// The initial size is passed via screen->frame_size
|
||||
@@ -899,69 +857,14 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
|
||||
apply_pending_resize(screen);
|
||||
sc_screen_render(screen, true);
|
||||
break;
|
||||
case SDL_WINDOWEVENT_FOCUS_LOST:
|
||||
if (relative_mode) {
|
||||
sc_screen_set_mouse_capture(screen, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
case SDL_KEYDOWN:
|
||||
if (relative_mode) {
|
||||
SDL_Keycode key = event->key.keysym.sym;
|
||||
if (sc_screen_is_mouse_capture_key(key)) {
|
||||
if (!screen->mouse_capture_key_pressed) {
|
||||
screen->mouse_capture_key_pressed = key;
|
||||
} else {
|
||||
// Another mouse capture key has been pressed, cancel
|
||||
// mouse (un)capture
|
||||
screen->mouse_capture_key_pressed = 0;
|
||||
}
|
||||
// Mouse capture keys are never forwarded to the device
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SDL_KEYUP:
|
||||
if (relative_mode) {
|
||||
SDL_Keycode key = event->key.keysym.sym;
|
||||
SDL_Keycode cap = screen->mouse_capture_key_pressed;
|
||||
screen->mouse_capture_key_pressed = 0;
|
||||
if (sc_screen_is_mouse_capture_key(key)) {
|
||||
if (key == cap) {
|
||||
// A mouse capture key has been pressed then released:
|
||||
// toggle the capture mouse mode
|
||||
sc_screen_toggle_mouse_capture(screen);
|
||||
}
|
||||
// Mouse capture keys are never forwarded to the device
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SDL_MOUSEWHEEL:
|
||||
case SDL_MOUSEMOTION:
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
if (relative_mode && !sc_screen_get_mouse_capture(screen)) {
|
||||
// Do not forward to input manager, the mouse will be captured
|
||||
// on SDL_MOUSEBUTTONUP
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case SDL_FINGERMOTION:
|
||||
case SDL_FINGERDOWN:
|
||||
case SDL_FINGERUP:
|
||||
if (relative_mode) {
|
||||
// Touch events are not compatible with relative mode
|
||||
// (coordinates are not relative)
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
if (relative_mode && !sc_screen_get_mouse_capture(screen)) {
|
||||
sc_screen_set_mouse_capture(screen, true);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (sc_screen_is_relative_mode(screen)
|
||||
&& sc_mouse_capture_handle_event(&screen->mc, event)) {
|
||||
// The mouse capture handler consumed the event
|
||||
return true;
|
||||
}
|
||||
|
||||
sc_input_manager_handle_event(&screen->im, event);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "fps_counter.h"
|
||||
#include "frame_buffer.h"
|
||||
#include "input_manager.h"
|
||||
#include "mouse_capture.h"
|
||||
#include "opengl.h"
|
||||
#include "options.h"
|
||||
#include "trait/key_processor.h"
|
||||
@@ -30,6 +31,7 @@ struct sc_screen {
|
||||
|
||||
struct sc_display display;
|
||||
struct sc_input_manager im;
|
||||
struct sc_mouse_capture mc; // only used in mouse relative mode
|
||||
struct sc_frame_buffer fb;
|
||||
struct sc_fps_counter fps_counter;
|
||||
|
||||
@@ -61,10 +63,6 @@ struct sc_screen {
|
||||
bool maximized;
|
||||
bool minimized;
|
||||
|
||||
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
|
||||
// RGUI) must be pressed. This variable tracks the pressed capture key.
|
||||
SDL_Keycode mouse_capture_key_pressed;
|
||||
|
||||
AVFrame *frame;
|
||||
|
||||
bool paused;
|
||||
@@ -126,9 +124,9 @@ sc_screen_destroy(struct sc_screen *screen);
|
||||
void
|
||||
sc_screen_hide_window(struct sc_screen *screen);
|
||||
|
||||
// switch the fullscreen mode
|
||||
// toggle the fullscreen mode
|
||||
void
|
||||
sc_screen_switch_fullscreen(struct sc_screen *screen);
|
||||
sc_screen_toggle_fullscreen(struct sc_screen *screen);
|
||||
|
||||
// resize window to optimal size (remove black borders)
|
||||
void
|
||||
|
||||
@@ -218,6 +218,21 @@ sc_server_get_audio_source_name(enum sc_audio_source audio_source) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
validate_string(const char *s) {
|
||||
// The parameters values are passed as command line arguments to adb, so
|
||||
// they must either be properly escaped, or they must not contain any
|
||||
// special shell characters.
|
||||
// Since they are not properly escaped on Windows anyway (see
|
||||
// sys/win/process.c), just forbid special shell characters.
|
||||
if (strpbrk(s, " ;'\"*$?&`#\\|<>[]{}()!~\r\n")) {
|
||||
LOGE("Invalid server param: [%s]", s);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static sc_pid
|
||||
execute_server(struct sc_server *server,
|
||||
const struct sc_server_params *params) {
|
||||
@@ -260,6 +275,11 @@ execute_server(struct sc_server *server,
|
||||
} \
|
||||
cmd[count++] = p; \
|
||||
} while(0)
|
||||
#define VALIDATE_STRING(s) do { \
|
||||
if (!validate_string(s)) { \
|
||||
goto end; \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
ADD_PARAM("scid=%08x", params->scid);
|
||||
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
|
||||
@@ -301,7 +321,8 @@ execute_server(struct sc_server *server,
|
||||
ADD_PARAM("max_size=%" PRIu16, params->max_size);
|
||||
}
|
||||
if (params->max_fps) {
|
||||
ADD_PARAM("max_fps=%" PRIu16, params->max_fps);
|
||||
VALIDATE_STRING(params->max_fps);
|
||||
ADD_PARAM("max_fps=%s", params->max_fps);
|
||||
}
|
||||
if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
|
||||
ADD_PARAM("lock_video_orientation=%" PRIi8,
|
||||
@@ -311,6 +332,7 @@ execute_server(struct sc_server *server,
|
||||
ADD_PARAM("tunnel_forward=true");
|
||||
}
|
||||
if (params->crop) {
|
||||
VALIDATE_STRING(params->crop);
|
||||
ADD_PARAM("crop=%s", params->crop);
|
||||
}
|
||||
if (!params->control) {
|
||||
@@ -321,9 +343,11 @@ execute_server(struct sc_server *server,
|
||||
ADD_PARAM("display_id=%" PRIu32, params->display_id);
|
||||
}
|
||||
if (params->camera_id) {
|
||||
VALIDATE_STRING(params->camera_id);
|
||||
ADD_PARAM("camera_id=%s", params->camera_id);
|
||||
}
|
||||
if (params->camera_size) {
|
||||
VALIDATE_STRING(params->camera_size);
|
||||
ADD_PARAM("camera_size=%s", params->camera_size);
|
||||
}
|
||||
if (params->camera_facing != SC_CAMERA_FACING_ANY) {
|
||||
@@ -331,6 +355,7 @@ execute_server(struct sc_server *server,
|
||||
sc_server_get_camera_facing_name(params->camera_facing));
|
||||
}
|
||||
if (params->camera_ar) {
|
||||
VALIDATE_STRING(params->camera_ar);
|
||||
ADD_PARAM("camera_ar=%s", params->camera_ar);
|
||||
}
|
||||
if (params->camera_fps) {
|
||||
@@ -346,15 +371,19 @@ execute_server(struct sc_server *server,
|
||||
ADD_PARAM("stay_awake=true");
|
||||
}
|
||||
if (params->video_codec_options) {
|
||||
VALIDATE_STRING(params->video_codec_options);
|
||||
ADD_PARAM("video_codec_options=%s", params->video_codec_options);
|
||||
}
|
||||
if (params->audio_codec_options) {
|
||||
VALIDATE_STRING(params->audio_codec_options);
|
||||
ADD_PARAM("audio_codec_options=%s", params->audio_codec_options);
|
||||
}
|
||||
if (params->video_encoder) {
|
||||
VALIDATE_STRING(params->video_encoder);
|
||||
ADD_PARAM("video_encoder=%s", params->video_encoder);
|
||||
}
|
||||
if (params->audio_encoder) {
|
||||
VALIDATE_STRING(params->audio_encoder);
|
||||
ADD_PARAM("audio_encoder=%s", params->audio_encoder);
|
||||
}
|
||||
if (params->power_off_on_close) {
|
||||
@@ -630,6 +659,14 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
||||
}
|
||||
}
|
||||
|
||||
if (control_socket != SC_SOCKET_NONE) {
|
||||
// Disable Nagle's algorithm for the control socket
|
||||
// (it only impacts the sending side, so it is useless to set it
|
||||
// for the other sockets)
|
||||
bool ok = net_set_tcp_nodelay(control_socket, true);
|
||||
(void) ok; // error already logged
|
||||
}
|
||||
|
||||
// we don't need the adb tunnel anymore
|
||||
sc_adb_tunnel_close(tunnel, &server->intr, serial,
|
||||
server->device_socket_name);
|
||||
|
||||
@@ -44,7 +44,7 @@ struct sc_server_params {
|
||||
uint16_t max_size;
|
||||
uint32_t video_bit_rate;
|
||||
uint32_t audio_bit_rate;
|
||||
uint16_t max_fps;
|
||||
const char *max_fps; // float to be parsed by the server
|
||||
int8_t lock_video_orientation;
|
||||
bool control;
|
||||
uint32_t display_id;
|
||||
|
||||
60
app/src/shortcut_mod.h
Normal file
60
app/src/shortcut_mod.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#ifndef SC_SHORTCUT_MOD_H
|
||||
#define SC_SHORTCUT_MOD_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <SDL2/SDL_keycode.h>
|
||||
|
||||
#include "options.h"
|
||||
|
||||
#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI)
|
||||
|
||||
// input: OR of enum sc_shortcut_mod
|
||||
// output: OR of SDL_Keymod
|
||||
static inline uint16_t
|
||||
sc_shortcut_mods_to_sdl(uint8_t shortcut_mods) {
|
||||
uint16_t sdl_mod = 0;
|
||||
if (shortcut_mods & SC_SHORTCUT_MOD_LCTRL) {
|
||||
sdl_mod |= KMOD_LCTRL;
|
||||
}
|
||||
if (shortcut_mods & SC_SHORTCUT_MOD_RCTRL) {
|
||||
sdl_mod |= KMOD_RCTRL;
|
||||
}
|
||||
if (shortcut_mods & SC_SHORTCUT_MOD_LALT) {
|
||||
sdl_mod |= KMOD_LALT;
|
||||
}
|
||||
if (shortcut_mods & SC_SHORTCUT_MOD_RALT) {
|
||||
sdl_mod |= KMOD_RALT;
|
||||
}
|
||||
if (shortcut_mods & SC_SHORTCUT_MOD_LSUPER) {
|
||||
sdl_mod |= KMOD_LGUI;
|
||||
}
|
||||
if (shortcut_mods & SC_SHORTCUT_MOD_RSUPER) {
|
||||
sdl_mod |= KMOD_RGUI;
|
||||
}
|
||||
return sdl_mod;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
sc_shortcut_mods_is_shortcut_mod(uint16_t sdl_shortcut_mods, uint16_t sdl_mod) {
|
||||
// sdl_shortcut_mods must be within the mask
|
||||
assert(!(sdl_shortcut_mods & ~SC_SDL_SHORTCUT_MODS_MASK));
|
||||
|
||||
// at least one shortcut mod pressed?
|
||||
return sdl_mod & sdl_shortcut_mods;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
sc_shortcut_mods_is_shortcut_key(uint16_t sdl_shortcut_mods,
|
||||
SDL_Keycode keycode) {
|
||||
return (sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL)
|
||||
|| (sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL)
|
||||
|| (sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT)
|
||||
|| (sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT)
|
||||
|| (sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI)
|
||||
|| (sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "hid/hid_gamepad.h"
|
||||
#include "input_events.h"
|
||||
#include "util/log.h"
|
||||
#include "util/str.h"
|
||||
|
||||
/** Downcast gamepad processor to sc_gamepad_uhid */
|
||||
#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_uhid, gamepad_processor)
|
||||
@@ -31,6 +30,7 @@ sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad,
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
|
||||
msg.uhid_create.id = hid_open->hid_id;
|
||||
msg.uhid_create.name = hid_open->name;
|
||||
msg.uhid_create.report_desc = hid_open->report_desc;
|
||||
msg.uhid_create.report_desc_size = hid_open->report_desc_size;
|
||||
|
||||
@@ -106,22 +106,6 @@ sc_gamepad_processor_process_gamepad_button(struct sc_gamepad_processor *gp,
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
sc_gamepad_uhid_process_hid_output(struct sc_gamepad_uhid *gamepad,
|
||||
uint16_t hid_id, const uint8_t *data,
|
||||
size_t size) {
|
||||
(void) gamepad;
|
||||
char *hex = sc_str_to_hex_string(data, size);
|
||||
if (hex) {
|
||||
LOGI("==== HID output [%" PRIu16 "] %s", hid_id, hex);
|
||||
free(hex);
|
||||
} else {
|
||||
LOGI("==== HID output [%" PRIu16 "]", hid_id);
|
||||
}
|
||||
|
||||
// TODO
|
||||
}
|
||||
|
||||
void
|
||||
sc_gamepad_uhid_init(struct sc_gamepad_uhid *gamepad,
|
||||
struct sc_controller *controller) {
|
||||
|
||||
@@ -20,9 +20,4 @@ void
|
||||
sc_gamepad_uhid_init(struct sc_gamepad_uhid *mouse,
|
||||
struct sc_controller *controller);
|
||||
|
||||
void
|
||||
sc_gamepad_uhid_process_hid_output(struct sc_gamepad_uhid *gamepad,
|
||||
uint16_t hid_id, const uint8_t *data,
|
||||
size_t size);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -141,6 +141,7 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb,
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
|
||||
msg.uhid_create.id = SC_HID_ID_KEYBOARD;
|
||||
msg.uhid_create.name = hid_open.name;
|
||||
msg.uhid_create.report_desc = hid_open.report_desc;
|
||||
msg.uhid_create.report_desc_size = hid_open.report_desc_size;
|
||||
if (!sc_controller_push_msg(controller, &msg)) {
|
||||
|
||||
@@ -81,6 +81,7 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse,
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
|
||||
msg.uhid_create.id = SC_HID_ID_MOUSE;
|
||||
msg.uhid_create.name = hid_open.name;
|
||||
msg.uhid_create.report_desc = hid_open.report_desc;
|
||||
msg.uhid_create.report_desc_size = hid_open.report_desc_size;
|
||||
if (!sc_controller_push_msg(controller, &msg)) {
|
||||
|
||||
@@ -4,15 +4,12 @@
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "uhid/keyboard_uhid.h"
|
||||
#include "uhid/gamepad_uhid.h"
|
||||
#include "util/log.h"
|
||||
|
||||
void
|
||||
sc_uhid_devices_init(struct sc_uhid_devices *devices,
|
||||
struct sc_keyboard_uhid *keyboard,
|
||||
struct sc_gamepad_uhid *gamepad) {
|
||||
struct sc_keyboard_uhid *keyboard) {
|
||||
devices->keyboard = keyboard;
|
||||
devices->gamepad = gamepad;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -24,13 +21,6 @@ sc_uhid_devices_process_hid_output(struct sc_uhid_devices *devices, uint16_t id,
|
||||
} else {
|
||||
LOGW("Unexpected keyboard HID output without UHID keyboard");
|
||||
}
|
||||
} else if (id >= SC_HID_ID_GAMEPAD_FIRST && id <= SC_HID_ID_GAMEPAD_LAST) {
|
||||
if (devices->gamepad) {
|
||||
sc_gamepad_uhid_process_hid_output(devices->gamepad, id, data,
|
||||
size);
|
||||
} else {
|
||||
LOGW("Unexpected gamepad HID output without UHID gamepad");
|
||||
}
|
||||
} else {
|
||||
LOGW("HID output ignored for id %" PRIu16, id);
|
||||
}
|
||||
|
||||
@@ -14,13 +14,11 @@
|
||||
|
||||
struct sc_uhid_devices {
|
||||
struct sc_keyboard_uhid *keyboard;
|
||||
struct sc_gamepad_uhid *gamepad;
|
||||
};
|
||||
|
||||
void
|
||||
sc_uhid_devices_init(struct sc_uhid_devices *devices,
|
||||
struct sc_keyboard_uhid *keyboard,
|
||||
struct sc_gamepad_uhid *gamepad);
|
||||
struct sc_keyboard_uhid *keyboard);
|
||||
|
||||
void
|
||||
sc_uhid_devices_process_hid_output(struct sc_uhid_devices *devices, uint16_t id,
|
||||
|
||||
@@ -51,7 +51,7 @@ sc_hid_open_log(const struct sc_hid_open *hid_open) {
|
||||
static void
|
||||
sc_hid_close_log(const struct sc_hid_close *hid_close) {
|
||||
// HID close: [00]
|
||||
LOGV("HD close: [%" PRIu16 "]", hid_close->hid_id);
|
||||
LOGV("HID close: [%" PRIu16 "]", hid_close->hid_id);
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -227,8 +227,11 @@ sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa,
|
||||
}
|
||||
|
||||
sc_mutex_lock(&aoa->mutex);
|
||||
bool full = sc_vecdeque_is_full(&aoa->queue);
|
||||
if (!full) {
|
||||
|
||||
bool pushed = false;
|
||||
|
||||
size_t size = sc_vecdeque_size(&aoa->queue);
|
||||
if (size < SC_AOA_EVENT_QUEUE_LIMIT) {
|
||||
bool was_empty = sc_vecdeque_is_empty(&aoa->queue);
|
||||
|
||||
struct sc_aoa_event *aoa_event =
|
||||
@@ -236,16 +239,17 @@ sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa,
|
||||
aoa_event->type = SC_AOA_EVENT_TYPE_INPUT;
|
||||
aoa_event->input.hid = *hid_input;
|
||||
aoa_event->input.ack_to_wait = ack_to_wait;
|
||||
pushed = true;
|
||||
|
||||
if (was_empty) {
|
||||
sc_cond_signal(&aoa->event_cond);
|
||||
}
|
||||
}
|
||||
// Otherwise (if the queue is full), the event is discarded
|
||||
// Otherwise, the event is discarded
|
||||
|
||||
sc_mutex_unlock(&aoa->mutex);
|
||||
|
||||
return !full;
|
||||
return pushed;
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -289,7 +293,7 @@ sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close) {
|
||||
sc_mutex_lock(&aoa->mutex);
|
||||
bool was_empty = sc_vecdeque_is_empty(&aoa->queue);
|
||||
|
||||
// an OPEN event is non-droppable, so push it to the queue even above the
|
||||
// a CLOSE event is non-droppable, so push it to the queue even above the
|
||||
// SC_AOA_EVENT_QUEUE_LIMIT
|
||||
struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole(&aoa->queue);
|
||||
if (!aoa_event) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef SC_AOA_HID_H
|
||||
#define SC_AOA_HID_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
|
||||
@@ -22,10 +22,7 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) {
|
||||
(void) usb;
|
||||
(void) userdata;
|
||||
|
||||
bool ok = sc_push_event(SC_EVENT_USB_DEVICE_DISCONNECTED);
|
||||
if (!ok) {
|
||||
LOGE("Could not post USB disconnection event: %s", SDL_GetError());
|
||||
}
|
||||
sc_push_event(SC_EVENT_USB_DEVICE_DISCONNECTED);
|
||||
}
|
||||
|
||||
static enum scrcpy_exit_code
|
||||
@@ -61,6 +58,10 @@ scrcpy_otg(struct scrcpy_options *options) {
|
||||
LOGW("Could not enable linear filtering");
|
||||
}
|
||||
|
||||
if (!SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1")) {
|
||||
LOGW("Could not allow joystick background events");
|
||||
}
|
||||
|
||||
// Minimal SDL initialization
|
||||
if (SDL_Init(SDL_INIT_EVENTS)) {
|
||||
LOGE("Could not initialize SDL: %s", SDL_GetError());
|
||||
@@ -184,6 +185,7 @@ scrcpy_otg(struct scrcpy_options *options) {
|
||||
.window_width = options->window_width,
|
||||
.window_height = options->window_height,
|
||||
.window_borderless = options->window_borderless,
|
||||
.shortcut_mods = options->shortcut_mods,
|
||||
};
|
||||
|
||||
ok = sc_screen_otg_init(&s->screen_otg, ¶ms);
|
||||
|
||||
@@ -4,47 +4,6 @@
|
||||
#include "options.h"
|
||||
#include "util/log.h"
|
||||
|
||||
static void
|
||||
sc_screen_otg_set_mouse_capture(struct sc_screen_otg *screen, bool capture) {
|
||||
#ifdef __APPLE__
|
||||
// Workaround for SDL bug on macOS:
|
||||
// <https://github.com/libsdl-org/SDL/issues/5340>
|
||||
if (capture) {
|
||||
int mouse_x, mouse_y;
|
||||
SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
|
||||
|
||||
int x, y, w, h;
|
||||
SDL_GetWindowPosition(screen->window, &x, &y);
|
||||
SDL_GetWindowSize(screen->window, &w, &h);
|
||||
|
||||
bool outside_window = mouse_x < x || mouse_x >= x + w
|
||||
|| mouse_y < y || mouse_y >= y + h;
|
||||
if (outside_window) {
|
||||
SDL_WarpMouseInWindow(screen->window, w / 2, h / 2);
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void) screen;
|
||||
#endif
|
||||
if (SDL_SetRelativeMouseMode(capture)) {
|
||||
LOGE("Could not set relative mouse mode to %s: %s",
|
||||
capture ? "true" : "false", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
static inline bool
|
||||
sc_screen_otg_get_mouse_capture(struct sc_screen_otg *screen) {
|
||||
(void) screen;
|
||||
return SDL_GetRelativeMouseMode();
|
||||
}
|
||||
|
||||
static inline void
|
||||
sc_screen_otg_toggle_mouse_capture(struct sc_screen_otg *screen) {
|
||||
(void) screen;
|
||||
bool new_value = !sc_screen_otg_get_mouse_capture(screen);
|
||||
sc_screen_otg_set_mouse_capture(screen, new_value);
|
||||
}
|
||||
|
||||
static void
|
||||
sc_screen_otg_render(struct sc_screen_otg *screen) {
|
||||
SDL_RenderClear(screen->renderer);
|
||||
@@ -61,8 +20,6 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
|
||||
screen->mouse = params->mouse;
|
||||
screen->gamepad = params->gamepad;
|
||||
|
||||
screen->mouse_capture_key_pressed = 0;
|
||||
|
||||
const char *title = params->window_title;
|
||||
assert(title);
|
||||
|
||||
@@ -113,9 +70,11 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
|
||||
LOGW("Could not load icon");
|
||||
}
|
||||
|
||||
sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods);
|
||||
|
||||
if (screen->mouse) {
|
||||
// Capture mouse on start
|
||||
sc_screen_otg_set_mouse_capture(screen, true);
|
||||
sc_mouse_capture_set_active(&screen->mc, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -137,11 +96,6 @@ sc_screen_otg_destroy(struct sc_screen_otg *screen) {
|
||||
SDL_DestroyWindow(screen->window);
|
||||
}
|
||||
|
||||
static inline bool
|
||||
sc_screen_otg_is_mouse_capture_key(SDL_Keycode key) {
|
||||
return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_screen_otg_process_key(struct sc_screen_otg *screen,
|
||||
const SDL_KeyboardEvent *event) {
|
||||
@@ -264,9 +218,14 @@ sc_screen_otg_process_gamepad_axis(struct sc_screen_otg *screen,
|
||||
assert(screen->gamepad);
|
||||
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
|
||||
|
||||
enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis);
|
||||
if (axis == SC_GAMEPAD_AXIS_UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct sc_gamepad_axis_event evt = {
|
||||
.gamepad_id = event->which,
|
||||
.axis = sc_gamepad_axis_from_sdl(event->axis),
|
||||
.axis = axis,
|
||||
.value = event->value,
|
||||
};
|
||||
gp->ops->process_gamepad_axis(gp, &evt);
|
||||
@@ -278,90 +237,61 @@ sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen,
|
||||
assert(screen->gamepad);
|
||||
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
|
||||
|
||||
enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button);
|
||||
if (button == SC_GAMEPAD_BUTTON_UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct sc_gamepad_button_event evt = {
|
||||
.gamepad_id = event->which,
|
||||
.action = sc_action_from_sdl_controllerbutton_type(event->type),
|
||||
.button = sc_gamepad_button_from_sdl(event->button),
|
||||
.button = button,
|
||||
};
|
||||
gp->ops->process_gamepad_button(gp, &evt);
|
||||
}
|
||||
|
||||
void
|
||||
sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) {
|
||||
if (sc_mouse_capture_handle_event(&screen->mc, event)) {
|
||||
// The mouse capture handler consumed the event
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event->type) {
|
||||
case SDL_WINDOWEVENT:
|
||||
switch (event->window.event) {
|
||||
case SDL_WINDOWEVENT_EXPOSED:
|
||||
sc_screen_otg_render(screen);
|
||||
break;
|
||||
case SDL_WINDOWEVENT_FOCUS_LOST:
|
||||
if (screen->mouse) {
|
||||
sc_screen_otg_set_mouse_capture(screen, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return;
|
||||
case SDL_KEYDOWN:
|
||||
if (screen->mouse) {
|
||||
SDL_Keycode key = event->key.keysym.sym;
|
||||
if (sc_screen_otg_is_mouse_capture_key(key)) {
|
||||
if (!screen->mouse_capture_key_pressed) {
|
||||
screen->mouse_capture_key_pressed = key;
|
||||
} else {
|
||||
// Another mouse capture key has been pressed, cancel
|
||||
// mouse (un)capture
|
||||
screen->mouse_capture_key_pressed = 0;
|
||||
}
|
||||
// Mouse capture keys are never forwarded to the device
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (screen->keyboard) {
|
||||
sc_screen_otg_process_key(screen, &event->key);
|
||||
}
|
||||
break;
|
||||
case SDL_KEYUP:
|
||||
if (screen->mouse) {
|
||||
SDL_Keycode key = event->key.keysym.sym;
|
||||
SDL_Keycode cap = screen->mouse_capture_key_pressed;
|
||||
screen->mouse_capture_key_pressed = 0;
|
||||
if (sc_screen_otg_is_mouse_capture_key(key)) {
|
||||
if (key == cap) {
|
||||
// A mouse capture key has been pressed then released:
|
||||
// toggle the capture mouse mode
|
||||
sc_screen_otg_toggle_mouse_capture(screen);
|
||||
}
|
||||
// Mouse capture keys are never forwarded to the device
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (screen->keyboard) {
|
||||
sc_screen_otg_process_key(screen, &event->key);
|
||||
}
|
||||
break;
|
||||
case SDL_MOUSEMOTION:
|
||||
if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) {
|
||||
if (screen->mouse) {
|
||||
sc_screen_otg_process_mouse_motion(screen, &event->motion);
|
||||
}
|
||||
break;
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) {
|
||||
if (screen->mouse) {
|
||||
sc_screen_otg_process_mouse_button(screen, &event->button);
|
||||
}
|
||||
break;
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
if (screen->mouse) {
|
||||
if (sc_screen_otg_get_mouse_capture(screen)) {
|
||||
sc_screen_otg_process_mouse_button(screen, &event->button);
|
||||
} else {
|
||||
sc_screen_otg_set_mouse_capture(screen, true);
|
||||
}
|
||||
sc_screen_otg_process_mouse_button(screen, &event->button);
|
||||
}
|
||||
break;
|
||||
case SDL_MOUSEWHEEL:
|
||||
if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) {
|
||||
if (screen->mouse) {
|
||||
sc_screen_otg_process_mouse_wheel(screen, &event->wheel);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "keyboard_aoa.h"
|
||||
#include "mouse_aoa.h"
|
||||
#include "mouse_capture.h"
|
||||
#include "gamepad_aoa.h"
|
||||
|
||||
struct sc_screen_otg {
|
||||
@@ -19,8 +20,7 @@ struct sc_screen_otg {
|
||||
SDL_Renderer *renderer;
|
||||
SDL_Texture *texture;
|
||||
|
||||
// See equivalent mechanism in screen.h
|
||||
SDL_Keycode mouse_capture_key_pressed;
|
||||
struct sc_mouse_capture mc;
|
||||
};
|
||||
|
||||
struct sc_screen_otg_params {
|
||||
@@ -35,6 +35,7 @@ struct sc_screen_otg_params {
|
||||
uint16_t window_width;
|
||||
uint16_t window_height;
|
||||
bool window_borderless;
|
||||
uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values
|
||||
};
|
||||
|
||||
bool
|
||||
|
||||
@@ -44,7 +44,7 @@ sc_write64be(uint8_t *buf, uint64_t value) {
|
||||
static inline void
|
||||
sc_write64le(uint8_t *buf, uint64_t value) {
|
||||
sc_write32le(buf, (uint32_t) value);
|
||||
sc_write32le(buf, value >> 32);
|
||||
sc_write32le(&buf[4], value >> 32);
|
||||
}
|
||||
|
||||
static inline uint16_t
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# include <sys/types.h>
|
||||
# include <sys/socket.h>
|
||||
# include <netinet/in.h>
|
||||
# include <netinet/tcp.h>
|
||||
# include <arpa/inet.h>
|
||||
# include <unistd.h>
|
||||
# include <fcntl.h>
|
||||
@@ -273,6 +274,22 @@ net_close(sc_socket socket) {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool
|
||||
net_set_tcp_nodelay(sc_socket socket, bool tcp_nodelay) {
|
||||
sc_raw_socket raw_sock = unwrap(socket);
|
||||
|
||||
int value = tcp_nodelay ? 1 : 0;
|
||||
int ret = setsockopt(raw_sock, IPPROTO_TCP, TCP_NODELAY,
|
||||
(const void *) &value, sizeof(value));
|
||||
if (ret == -1) {
|
||||
net_perror("setsockopt(TCP_NODELAY)");
|
||||
return false;
|
||||
}
|
||||
|
||||
assert(ret == 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
net_parse_ipv4(const char *s, uint32_t *ipv4) {
|
||||
struct in_addr addr;
|
||||
|
||||
@@ -67,6 +67,10 @@ net_interrupt(sc_socket socket);
|
||||
bool
|
||||
net_close(sc_socket socket);
|
||||
|
||||
// Disable Nagle's algorithm (if tcp_nodelay is true)
|
||||
bool
|
||||
net_set_tcp_nodelay(sc_socket socket, bool tcp_nodelay);
|
||||
|
||||
/**
|
||||
* Parse `ip` "xxx.xxx.xxx.xxx" to an IPv4 host representation
|
||||
*/
|
||||
|
||||
@@ -42,6 +42,44 @@ static void test_write64be(void) {
|
||||
assert(buf[7] == 0xEF);
|
||||
}
|
||||
|
||||
static void test_write16le(void) {
|
||||
uint16_t val = 0xABCD;
|
||||
uint8_t buf[2];
|
||||
|
||||
sc_write16le(buf, val);
|
||||
|
||||
assert(buf[0] == 0xCD);
|
||||
assert(buf[1] == 0xAB);
|
||||
}
|
||||
|
||||
static void test_write32le(void) {
|
||||
uint32_t val = 0xABCD1234;
|
||||
uint8_t buf[4];
|
||||
|
||||
sc_write32le(buf, val);
|
||||
|
||||
assert(buf[0] == 0x34);
|
||||
assert(buf[1] == 0x12);
|
||||
assert(buf[2] == 0xCD);
|
||||
assert(buf[3] == 0xAB);
|
||||
}
|
||||
|
||||
static void test_write64le(void) {
|
||||
uint64_t val = 0xABCD1234567890EF;
|
||||
uint8_t buf[8];
|
||||
|
||||
sc_write64le(buf, val);
|
||||
|
||||
assert(buf[0] == 0xEF);
|
||||
assert(buf[1] == 0x90);
|
||||
assert(buf[2] == 0x78);
|
||||
assert(buf[3] == 0x56);
|
||||
assert(buf[4] == 0x34);
|
||||
assert(buf[5] == 0x12);
|
||||
assert(buf[6] == 0xCD);
|
||||
assert(buf[7] == 0xAB);
|
||||
}
|
||||
|
||||
static void test_read16be(void) {
|
||||
uint8_t buf[2] = {0xAB, 0xCD};
|
||||
|
||||
@@ -108,6 +146,10 @@ int main(int argc, char *argv[]) {
|
||||
test_read32be();
|
||||
test_read64be();
|
||||
|
||||
test_write16le();
|
||||
test_write32le();
|
||||
test_write64le();
|
||||
|
||||
test_float_to_u16fp();
|
||||
test_float_to_i16fp();
|
||||
return 0;
|
||||
|
||||
@@ -78,7 +78,7 @@ static void test_options(void) {
|
||||
assert(opts->video_bit_rate == 5000000);
|
||||
assert(!strcmp(opts->crop, "100:200:300:400"));
|
||||
assert(opts->fullscreen);
|
||||
assert(opts->max_fps == 30);
|
||||
assert(!strcmp(opts->max_fps, "30"));
|
||||
assert(opts->max_size == 1024);
|
||||
assert(opts->lock_video_orientation == 2);
|
||||
assert(opts->port_range.first == 1234);
|
||||
|
||||
@@ -329,6 +329,7 @@ static void test_serialize_uhid_create(void) {
|
||||
.type = SC_CONTROL_MSG_TYPE_UHID_CREATE,
|
||||
.uhid_create = {
|
||||
.id = 42,
|
||||
.name = "ABC",
|
||||
.report_desc_size = sizeof(report_desc),
|
||||
.report_desc = report_desc,
|
||||
},
|
||||
@@ -336,12 +337,14 @@ static void test_serialize_uhid_create(void) {
|
||||
|
||||
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = sc_control_msg_serialize(&msg, buf);
|
||||
assert(size == 16);
|
||||
assert(size == 20);
|
||||
|
||||
const uint8_t expected[] = {
|
||||
SC_CONTROL_MSG_TYPE_UHID_CREATE,
|
||||
0, 42, // id
|
||||
0, 11, // size
|
||||
3, // name size
|
||||
65, 66, 67, // "ABC"
|
||||
0, 11, // report desc size
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
|
||||
@@ -233,10 +233,10 @@ install` must be run as root)._
|
||||
|
||||
#### Option 2: Use prebuilt server
|
||||
|
||||
- [`scrcpy-server-v2.6.1`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b`</sub>
|
||||
- [`scrcpy-server-v2.7`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba`</sub>
|
||||
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-server-v2.6.1
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
|
||||
@@ -94,14 +94,18 @@ the content (if supported by the app) relative to the center of the screen.
|
||||
|
||||
https://github.com/Genymobile/scrcpy/assets/543275/26c4a920-9805-43f1-8d4c-608752d04767
|
||||
|
||||
To simulate a tilt gesture: <kbd>Shift</kbd>+_click-and-move-up-or-down_.
|
||||
To simulate a vertical tilt gesture: <kbd>Shift</kbd>+_click-and-move-up-or-down_.
|
||||
|
||||
https://github.com/Genymobile/scrcpy/assets/543275/1e252341-4a90-4b29-9d11-9153b324669f
|
||||
|
||||
Similarly, to simulate a horizontal tilt gesture:
|
||||
<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+_click-and-move-left-or-right_.
|
||||
|
||||
Technically, _scrcpy_ generates additional touch events from a "virtual finger"
|
||||
at a location inverted through the center of the screen. When pressing
|
||||
<kbd>Ctrl</kbd> the _x_ and _y_ coordinates are inverted. Using <kbd>Shift</kbd>
|
||||
only inverts _x_.
|
||||
only inverts _x_, whereas using <kbd>Ctrl</kbd>+<kbd>Shift</kbd> only inverts
|
||||
_y_.
|
||||
|
||||
This only works for the default mouse mode (`--mouse=sdk`).
|
||||
|
||||
|
||||
58
doc/gamepad.md
Normal file
58
doc/gamepad.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Gamepad
|
||||
|
||||
Several gamepad input modes are available:
|
||||
|
||||
- `--gamepad=disabled` (default)
|
||||
- `--gamepad=uhid` (or `-G`): simulates physical HID gamepads using the UHID
|
||||
kernel module on the device
|
||||
- `--gamepad=aoa`: simulates physical HID gamepads using the AOAv2 protocol
|
||||
|
||||
|
||||
## Physical gamepad simulation
|
||||
|
||||
Two modes allow to simulate physical HID gamepads on the device, one for each
|
||||
physical gamepad plugged into the computer.
|
||||
|
||||
|
||||
### UHID
|
||||
|
||||
This mode simulates physical HID gamepads using the [UHID] kernel module on the
|
||||
device.
|
||||
|
||||
[UHID]: https://kernel.org/doc/Documentation/hid/uhid.txt
|
||||
|
||||
To enable UHID gamepads, use:
|
||||
|
||||
```bash
|
||||
scrcpy --gamepad=uhid
|
||||
scrcpy -G # short version
|
||||
```
|
||||
|
||||
Note: UHID may not work on old Android versions due to permission errors.
|
||||
|
||||
|
||||
### AOA
|
||||
|
||||
This mode simulates physical HID gamepads using the [AOAv2] protocol.
|
||||
|
||||
[AOAv2]: https://source.android.com/devices/accessories/aoa2#hid-support
|
||||
|
||||
To enable AOA gamepads, use:
|
||||
|
||||
```bash
|
||||
scrcpy --gamepad=aoa
|
||||
```
|
||||
|
||||
Contrary to the other mode, it works at the USB level directly (so it only works
|
||||
over USB).
|
||||
|
||||
It does not use the scrcpy server, and does not require `adb` (USB debugging).
|
||||
Therefore, it is possible to control the device (but not mirror) even with USB
|
||||
debugging disabled (see [OTG](otg.md)).
|
||||
|
||||
Note: For some reason, in this mode, Android detects multiple physical gamepads
|
||||
as a single misbehaving one. Use UHID if you need multiple gamepads.
|
||||
|
||||
Note: On Windows, it may only work in [OTG mode](otg.md), not while mirroring
|
||||
(it is not possible to open a USB device if it is already open by another
|
||||
process like the _adb daemon_).
|
||||
@@ -53,6 +53,8 @@ scrcpy --mouse=uhid
|
||||
scrcpy -M # short version
|
||||
```
|
||||
|
||||
Note: UHID may not work on old Android versions due to permission errors.
|
||||
|
||||
|
||||
### AOA
|
||||
|
||||
|
||||
29
doc/otg.md
29
doc/otg.md
@@ -6,16 +6,18 @@ was a [physical keyboard] and/or a [physical mouse] connected to the Android
|
||||
device (see [keyboard](keyboard.md) and [mouse](mouse.md)).
|
||||
|
||||
[physical keyboard]: keyboard.md#physical-keyboard-simulation
|
||||
[physical mouse]: physical-keyboard-simulation
|
||||
[physical mouse]: mouse.md#physical-mouse-simulation
|
||||
|
||||
A special mode (OTG) allows to control the device using AOA
|
||||
[keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa), without using _adb_ at
|
||||
all (so USB debugging is not necessary). In this mode, video and audio are
|
||||
disabled, and `--keyboard=aoa and `--mouse=aoa` are implicitly set.
|
||||
[keyboard](keyboard.md#aoa), [mouse](mouse.md#aoa) and
|
||||
[gamepad](gamepad.md#aoa), without using _adb_ at all (so USB debugging is not
|
||||
necessary). In this mode, video and audio are disabled, and `--keyboard=aoa` and
|
||||
`--mouse=aoa` are implicitly set. However, gamepads are disabled by default, so
|
||||
`--gamepad=aoa` (or `-G` in OTG mode) must be explicitly set.
|
||||
|
||||
Therefore, it is possible to run _scrcpy_ with only physical keyboard and mouse
|
||||
simulation, as if the computer keyboard and mouse were plugged directly to the
|
||||
device via an OTG cable.
|
||||
Therefore, it is possible to run _scrcpy_ with only physical keyboard, mouse and
|
||||
gamepad simulation, as if the computer keyboard, mouse and gamepads were plugged
|
||||
directly to the device via an OTG cable.
|
||||
|
||||
To enable OTG mode:
|
||||
|
||||
@@ -32,6 +34,13 @@ scrcpy --otg --keyboard=disabled
|
||||
scrcpy --otg --mouse=disabled
|
||||
```
|
||||
|
||||
and to enable gamepads:
|
||||
|
||||
```bash
|
||||
scrcpy --otg --gamepad=aoa
|
||||
scrcpy --otg -G # short version
|
||||
```
|
||||
|
||||
It only works if the device is connected over USB.
|
||||
|
||||
## OTG issues on Windows
|
||||
@@ -50,9 +59,9 @@ is enabled, then OTG mode is not necessary.
|
||||
Instead, disable video and audio, and select UHID (or AOA):
|
||||
|
||||
```bash
|
||||
scrcpy --no-video --no-audio --keyboard=uhid --mouse=uhid
|
||||
scrcpy --no-video --no-audio -KM # short version
|
||||
scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa
|
||||
scrcpy --no-video --no-audio --keyboard=uhid --mouse=uhid --gamepad=uhid
|
||||
scrcpy --no-video --no-audio -KMG # short version
|
||||
scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa --gamepad=aoa
|
||||
```
|
||||
|
||||
One benefit of UHID is that it also works wirelessly.
|
||||
|
||||
@@ -53,7 +53,8 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
|
||||
| Open keyboard settings (HID keyboard only) | <kbd>MOD</kbd>+<kbd>k</kbd>
|
||||
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
|
||||
| Pinch-to-zoom/rotate | <kbd>Ctrl</kbd>+_click-and-move_
|
||||
| Tilt (slide vertically with 2 fingers) | <kbd>Shift</kbd>+_click-and-move_
|
||||
| Tilt vertically (slide with 2 fingers) | <kbd>Shift</kbd>+_click-and-move_
|
||||
| Tilt horizontally (slide with 2 fingers) | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+_click-and-move_
|
||||
| Drag & drop APK file | Install APK from computer
|
||||
| Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device)
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
Download the [latest release]:
|
||||
|
||||
- [`scrcpy-win64-v2.6.1.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `041fc3abf8578ddcead5a8c4a8be8960b7c4d45b21d3370ee2683605e86a728c`</sub>
|
||||
- [`scrcpy-win32-v2.6.1.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `17a5d4d17230b4c90fad45af6395efda9aea287a03c04e6b4ecc9ceb8134ea04`</sub>
|
||||
- [`scrcpy-win64-v2.7.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `5910bc18d5a16f42d84185ddc7e16a4cee6a6f5f33451559c1a1d6d0099bd5f5`</sub>
|
||||
- [`scrcpy-win32-v2.7.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `ef4daf89d500f33d78b830625536ecb18481429dd94433e7634c824292059d06`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-win64-v2.6.1.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-win32-v2.6.1.zip
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win64-v2.7.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win32-v2.7.zip
|
||||
|
||||
and extract it.
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
set -e
|
||||
|
||||
BUILDDIR=build-auto
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-server-v2.6.1
|
||||
PREBUILT_SERVER_SHA256=ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7
|
||||
PREBUILT_SERVER_SHA256=a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba
|
||||
|
||||
echo "[scrcpy] Downloading prebuilt server..."
|
||||
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
project('scrcpy', 'c',
|
||||
version: '2.6.1',
|
||||
version: '2.7',
|
||||
meson_version: '>= 0.48',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "com.genymobile.scrcpy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
versionCode 20601
|
||||
versionName "2.6.1"
|
||||
versionCode 20700
|
||||
versionName "2.7"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
set -e
|
||||
|
||||
SCRCPY_DEBUG=false
|
||||
SCRCPY_VERSION_NAME=2.6.1
|
||||
SCRCPY_VERSION_NAME=2.7
|
||||
|
||||
PLATFORM=${ANDROID_PLATFORM:-34}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0}
|
||||
|
||||
@@ -29,7 +29,7 @@ public class Options {
|
||||
private boolean audioDup;
|
||||
private int videoBitRate = 8000000;
|
||||
private int audioBitRate = 128000;
|
||||
private int maxFps;
|
||||
private float maxFps;
|
||||
private int lockVideoOrientation = -1;
|
||||
private boolean tunnelForward;
|
||||
private Rect crop;
|
||||
@@ -113,7 +113,7 @@ public class Options {
|
||||
return audioBitRate;
|
||||
}
|
||||
|
||||
public int getMaxFps() {
|
||||
public float getMaxFps() {
|
||||
return maxFps;
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@ public class Options {
|
||||
options.audioBitRate = Integer.parseInt(value);
|
||||
break;
|
||||
case "max_fps":
|
||||
options.maxFps = Integer.parseInt(value);
|
||||
options.maxFps = parseFloat("max_fps", value);
|
||||
break;
|
||||
case "lock_video_orientation":
|
||||
options.lockVideoOrientation = Integer.parseInt(value);
|
||||
@@ -456,8 +456,14 @@ public class Options {
|
||||
}
|
||||
int width = Integer.parseInt(tokens[0]);
|
||||
int height = Integer.parseInt(tokens[1]);
|
||||
if (width <= 0 || height <= 0) {
|
||||
throw new IllegalArgumentException("Invalid crop size: " + width + "x" + height);
|
||||
}
|
||||
int x = Integer.parseInt(tokens[2]);
|
||||
int y = Integer.parseInt(tokens[3]);
|
||||
if (x < 0 || y < 0) {
|
||||
throw new IllegalArgumentException("Invalid crop offset: " + x + ":" + y);
|
||||
}
|
||||
return new Rect(x, y, x + width, y + height);
|
||||
}
|
||||
|
||||
@@ -487,4 +493,12 @@ public class Options {
|
||||
float floatAr = Float.parseFloat(tokens[0]);
|
||||
return CameraAspectRatio.fromFloat(floatAr);
|
||||
}
|
||||
|
||||
private static float parseFloat(String key, String value) {
|
||||
try {
|
||||
return Float.parseFloat(value);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid float value for " + key + ": \"" + value + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +190,8 @@ public final class Server {
|
||||
options.getSendFrameMeta());
|
||||
SurfaceCapture surfaceCapture;
|
||||
if (options.getVideoSource() == VideoSource.DISPLAY) {
|
||||
surfaceCapture = new ScreenCapture(device);
|
||||
surfaceCapture = new ScreenCapture(device, options.getDisplayId(), options.getMaxSize(), options.getCrop(),
|
||||
options.getLockVideoOrientation());
|
||||
} else {
|
||||
surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(),
|
||||
options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed());
|
||||
|
||||
@@ -56,8 +56,11 @@ public class AudioDirectCapture implements AudioCapture {
|
||||
builder.setAudioSource(audioSource);
|
||||
builder.setAudioFormat(AudioConfig.createAudioFormat());
|
||||
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING);
|
||||
// This buffer size does not impact latency
|
||||
builder.setBufferSizeInBytes(8 * minBufferSize);
|
||||
if (minBufferSize > 0) {
|
||||
// This buffer size does not impact latency
|
||||
builder.setBufferSizeInBytes(8 * minBufferSize);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.genymobile.scrcpy.audio;
|
||||
|
||||
import com.genymobile.scrcpy.AsyncProcessor;
|
||||
import com.genymobile.scrcpy.device.ConfigurationException;
|
||||
import com.genymobile.scrcpy.device.Streamer;
|
||||
import com.genymobile.scrcpy.util.Codec;
|
||||
import com.genymobile.scrcpy.util.CodecOption;
|
||||
import com.genymobile.scrcpy.util.CodecUtils;
|
||||
import com.genymobile.scrcpy.device.ConfigurationException;
|
||||
import com.genymobile.scrcpy.util.IO;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
import com.genymobile.scrcpy.device.Streamer;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaCodec;
|
||||
@@ -287,7 +287,13 @@ public final class AudioEncoder implements AsyncProcessor {
|
||||
if (encoderName != null) {
|
||||
Ln.d("Creating audio encoder by name: '" + encoderName + "'");
|
||||
try {
|
||||
return MediaCodec.createByCodecName(encoderName);
|
||||
MediaCodec mediaCodec = MediaCodec.createByCodecName(encoderName);
|
||||
String mimeType = Codec.getMimeType(mediaCodec);
|
||||
if (!codec.getMimeType().equals(mimeType)) {
|
||||
Ln.e("Audio encoder type for \"" + encoderName + "\" (" + mimeType + ") does not match codec type (" + codec.getMimeType() + ")");
|
||||
throw new ConfigurationException("Incorrect encoder type: " + encoderName);
|
||||
}
|
||||
return mediaCodec;
|
||||
} catch (IllegalArgumentException e) {
|
||||
Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
|
||||
throw new ConfigurationException("Unknown encoder: " + encoderName);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.genymobile.scrcpy.audio;
|
||||
|
||||
import com.genymobile.scrcpy.AsyncProcessor;
|
||||
import com.genymobile.scrcpy.device.Streamer;
|
||||
import com.genymobile.scrcpy.util.IO;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.device.Streamer;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.os.Build;
|
||||
|
||||
@@ -131,10 +131,11 @@ public final class ControlMessage {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createUhidCreate(int id, byte[] reportDesc) {
|
||||
public static ControlMessage createUhidCreate(int id, String name, byte[] reportDesc) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_UHID_CREATE;
|
||||
msg.id = id;
|
||||
msg.text = name;
|
||||
msg.data = reportDesc;
|
||||
return msg;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.genymobile.scrcpy.control;
|
||||
|
||||
import com.genymobile.scrcpy.util.Binary;
|
||||
import com.genymobile.scrcpy.device.Position;
|
||||
import com.genymobile.scrcpy.util.Binary;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.DataInputStream;
|
||||
@@ -75,11 +75,16 @@ public class ControlMessageReader {
|
||||
return value;
|
||||
}
|
||||
|
||||
private String parseString() throws IOException {
|
||||
byte[] data = parseByteArray(4);
|
||||
private String parseString(int sizeBytes) throws IOException {
|
||||
assert sizeBytes > 0 && sizeBytes <= 4;
|
||||
byte[] data = parseByteArray(sizeBytes);
|
||||
return new String(data, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String parseString() throws IOException {
|
||||
return parseString(4);
|
||||
}
|
||||
|
||||
private byte[] parseByteArray(int sizeBytes) throws IOException {
|
||||
int len = parseBufferLength(sizeBytes);
|
||||
byte[] data = new byte[len];
|
||||
@@ -134,8 +139,9 @@ public class ControlMessageReader {
|
||||
|
||||
private ControlMessage parseUhidCreate() throws IOException {
|
||||
int id = dis.readUnsignedShort();
|
||||
String name = parseString(1);
|
||||
byte[] data = parseByteArray(2);
|
||||
return ControlMessage.createUhidCreate(id, data);
|
||||
return ControlMessage.createUhidCreate(id, name, data);
|
||||
}
|
||||
|
||||
private ControlMessage parseUhidInput() throws IOException {
|
||||
|
||||
@@ -3,9 +3,9 @@ package com.genymobile.scrcpy.control;
|
||||
import com.genymobile.scrcpy.AsyncProcessor;
|
||||
import com.genymobile.scrcpy.CleanUp;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.device.Point;
|
||||
import com.genymobile.scrcpy.device.Position;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
@@ -210,7 +210,7 @@ public class Controller implements AsyncProcessor {
|
||||
device.rotateDevice();
|
||||
break;
|
||||
case ControlMessage.TYPE_UHID_CREATE:
|
||||
getUhidManager().open(msg.getId(), msg.getData());
|
||||
getUhidManager().open(msg.getId(), msg.getText(), msg.getData());
|
||||
break;
|
||||
case ControlMessage.TYPE_UHID_INPUT:
|
||||
getUhidManager().writeInput(msg.getId(), msg.getData());
|
||||
@@ -243,7 +243,7 @@ public class Controller implements AsyncProcessor {
|
||||
return false;
|
||||
}
|
||||
for (KeyEvent event : events) {
|
||||
if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) {
|
||||
if (!device.injectMainDisplayEvent(event, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -324,7 +324,7 @@ public class Controller implements AsyncProcessor {
|
||||
// First button pressed: ACTION_DOWN
|
||||
MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!device.injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
if (!device.injectVirtualDisplayEvent(downEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -335,7 +335,7 @@ public class Controller implements AsyncProcessor {
|
||||
if (!InputManager.setActionButton(pressEvent, actionButton)) {
|
||||
return false;
|
||||
}
|
||||
if (!device.injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
if (!device.injectVirtualDisplayEvent(pressEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -349,7 +349,7 @@ public class Controller implements AsyncProcessor {
|
||||
if (!InputManager.setActionButton(releaseEvent, actionButton)) {
|
||||
return false;
|
||||
}
|
||||
if (!device.injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
if (!device.injectVirtualDisplayEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -357,7 +357,7 @@ public class Controller implements AsyncProcessor {
|
||||
// Last button released: ACTION_UP
|
||||
MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!device.injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
if (!device.injectVirtualDisplayEvent(upEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -368,7 +368,7 @@ public class Controller implements AsyncProcessor {
|
||||
|
||||
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
|
||||
DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
||||
return device.injectVirtualDisplayEvent(event, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) {
|
||||
@@ -390,7 +390,7 @@ public class Controller implements AsyncProcessor {
|
||||
|
||||
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
|
||||
DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0);
|
||||
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
||||
return device.injectVirtualDisplayEvent(event, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.genymobile.scrcpy.control;
|
||||
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.StringUtils;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.HandlerThread;
|
||||
@@ -46,7 +47,7 @@ public final class UhidManager {
|
||||
}
|
||||
}
|
||||
|
||||
public void open(int id, byte[] reportDesc) throws IOException {
|
||||
public void open(int id, String name, byte[] reportDesc) throws IOException {
|
||||
try {
|
||||
FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0);
|
||||
try {
|
||||
@@ -56,7 +57,7 @@ public final class UhidManager {
|
||||
close(old);
|
||||
}
|
||||
|
||||
byte[] req = buildUhidCreate2Req(reportDesc);
|
||||
byte[] req = buildUhidCreate2Req(name, reportDesc);
|
||||
Os.write(fd, req, 0, req.length);
|
||||
|
||||
registerUhidListener(id, fd);
|
||||
@@ -146,7 +147,7 @@ public final class UhidManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] buildUhidCreate2Req(byte[] reportDesc) {
|
||||
private static byte[] buildUhidCreate2Req(String name, byte[] reportDesc) {
|
||||
/*
|
||||
* struct uhid_event {
|
||||
* uint32_t type;
|
||||
@@ -171,8 +172,14 @@ public final class UhidManager {
|
||||
byte[] empty = new byte[256];
|
||||
ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder());
|
||||
buf.putInt(UHID_CREATE2);
|
||||
buf.put("scrcpy".getBytes(StandardCharsets.US_ASCII));
|
||||
buf.put(empty, 0, 256 - "scrcpy".length());
|
||||
|
||||
String actualName = name.isEmpty() ? "scrcpy" : "scrcpy: " + name;
|
||||
byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8);
|
||||
int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127);
|
||||
assert len <= 127;
|
||||
buf.put(utf8Name, 0, len);
|
||||
buf.put(empty, 0, 256 - len);
|
||||
|
||||
buf.putShort((short) reportDesc.length);
|
||||
buf.putShort(BUS_VIRTUAL);
|
||||
buf.putInt(0); // vendor id
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.genymobile.scrcpy.device;
|
||||
|
||||
import com.genymobile.scrcpy.Options;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
import com.genymobile.scrcpy.video.ScreenInfo;
|
||||
import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
||||
import com.genymobile.scrcpy.wrappers.DisplayControl;
|
||||
@@ -16,8 +15,6 @@ import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.SystemClock;
|
||||
import android.view.IDisplayFoldListener;
|
||||
import android.view.IRotationWatcher;
|
||||
import android.view.InputDevice;
|
||||
import android.view.InputEvent;
|
||||
import android.view.KeyCharacterMap;
|
||||
@@ -37,26 +34,10 @@ public final class Device {
|
||||
public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
|
||||
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
|
||||
|
||||
public interface RotationListener {
|
||||
void onRotationChanged(int rotation);
|
||||
}
|
||||
|
||||
public interface FoldListener {
|
||||
void onFoldChanged(int displayId, boolean folded);
|
||||
}
|
||||
|
||||
public interface ClipboardListener {
|
||||
void onClipboardTextChanged(String text);
|
||||
}
|
||||
|
||||
private final Rect crop;
|
||||
private int maxSize;
|
||||
private final int lockVideoOrientation;
|
||||
|
||||
private Size deviceSize;
|
||||
private ScreenInfo screenInfo;
|
||||
private RotationListener rotationListener;
|
||||
private FoldListener foldListener;
|
||||
private ClipboardListener clipboardListener;
|
||||
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
|
||||
|
||||
@@ -65,71 +46,15 @@ public final class Device {
|
||||
*/
|
||||
private final int displayId;
|
||||
|
||||
/**
|
||||
* The surface flinger layer stack associated with this logical display
|
||||
*/
|
||||
private final int layerStack;
|
||||
|
||||
private final boolean supportsInputEvents;
|
||||
|
||||
public Device(Options options) throws ConfigurationException {
|
||||
// set by the ScreenCapture instance
|
||||
private ScreenInfo screenInfo;
|
||||
private int virtualDisplayId;
|
||||
|
||||
public Device(Options options) {
|
||||
displayId = options.getDisplayId();
|
||||
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
|
||||
if (displayInfo == null) {
|
||||
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
|
||||
throw new ConfigurationException("Unknown display id: " + displayId);
|
||||
}
|
||||
|
||||
int displayInfoFlags = displayInfo.getFlags();
|
||||
|
||||
deviceSize = displayInfo.getSize();
|
||||
crop = options.getCrop();
|
||||
maxSize = options.getMaxSize();
|
||||
lockVideoOrientation = options.getLockVideoOrientation();
|
||||
|
||||
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation);
|
||||
layerStack = displayInfo.getLayerStack();
|
||||
|
||||
ServiceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
|
||||
@Override
|
||||
public void onRotationChanged(int rotation) {
|
||||
synchronized (Device.this) {
|
||||
screenInfo = screenInfo.withDeviceRotation(rotation);
|
||||
|
||||
// notify
|
||||
if (rotationListener != null) {
|
||||
rotationListener.onRotationChanged(rotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, displayId);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() {
|
||||
@Override
|
||||
public void onDisplayFoldChanged(int displayId, boolean folded) {
|
||||
if (Device.this.displayId != displayId) {
|
||||
// Ignore events related to other display ids
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (Device.this) {
|
||||
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
|
||||
if (displayInfo == null) {
|
||||
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
deviceSize = displayInfo.getSize();
|
||||
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation);
|
||||
// notify
|
||||
if (foldListener != null) {
|
||||
foldListener.onFoldChanged(displayId, folded);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
virtualDisplayId = displayId; // by default
|
||||
|
||||
if (options.getControl() && options.getClipboardAutosync()) {
|
||||
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically
|
||||
@@ -157,38 +82,20 @@ public final class Device {
|
||||
}
|
||||
}
|
||||
|
||||
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
|
||||
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
|
||||
}
|
||||
|
||||
// main display or any display on Android >= Q
|
||||
supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
|
||||
supportsInputEvents = options.getDisplayId() == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
|
||||
if (!supportsInputEvents) {
|
||||
Ln.w("Input events are not supported for secondary displays before Android 10");
|
||||
}
|
||||
}
|
||||
|
||||
public int getDisplayId() {
|
||||
return displayId;
|
||||
}
|
||||
|
||||
public synchronized void setMaxSize(int newMaxSize) {
|
||||
maxSize = newMaxSize;
|
||||
screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation);
|
||||
}
|
||||
|
||||
public synchronized ScreenInfo getScreenInfo() {
|
||||
return screenInfo;
|
||||
}
|
||||
|
||||
public int getLayerStack() {
|
||||
return layerStack;
|
||||
}
|
||||
|
||||
public Point getPhysicalPoint(Position position) {
|
||||
// it hides the field on purpose, to read it with a lock
|
||||
@SuppressWarnings("checkstyle:HiddenField")
|
||||
ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
|
||||
if (screenInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation
|
||||
Size unlockedVideoSize = screenInfo.getUnlockedVideoSize();
|
||||
@@ -222,6 +129,22 @@ public final class Device {
|
||||
return supportsInputEvents;
|
||||
}
|
||||
|
||||
private synchronized ScreenInfo getScreenInfo() {
|
||||
return screenInfo;
|
||||
}
|
||||
|
||||
public synchronized void setScreenInfo(ScreenInfo screenInfo) {
|
||||
this.screenInfo = screenInfo;
|
||||
}
|
||||
|
||||
private synchronized int getVirtualDisplayId() {
|
||||
return virtualDisplayId;
|
||||
}
|
||||
|
||||
public synchronized void setVirtualDisplayId(int virtualDisplayId) {
|
||||
this.virtualDisplayId = virtualDisplayId;
|
||||
}
|
||||
|
||||
public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) {
|
||||
if (!supportsInputEvents(displayId)) {
|
||||
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
|
||||
@@ -234,10 +157,14 @@ public final class Device {
|
||||
return ServiceManager.getInputManager().injectInputEvent(inputEvent, injectMode);
|
||||
}
|
||||
|
||||
public boolean injectEvent(InputEvent event, int injectMode) {
|
||||
public boolean injectMainDisplayEvent(InputEvent event, int injectMode) {
|
||||
return injectEvent(event, displayId, injectMode);
|
||||
}
|
||||
|
||||
public boolean injectVirtualDisplayEvent(InputEvent event, int injectMode) {
|
||||
return injectEvent(event, virtualDisplayId, injectMode);
|
||||
}
|
||||
|
||||
public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
|
||||
@@ -262,14 +189,6 @@ public final class Device {
|
||||
return ServiceManager.getPowerManager().isScreenOn();
|
||||
}
|
||||
|
||||
public synchronized void setRotationListener(RotationListener rotationListener) {
|
||||
this.rotationListener = rotationListener;
|
||||
}
|
||||
|
||||
public synchronized void setFoldListener(FoldListener foldlistener) {
|
||||
this.foldListener = foldlistener;
|
||||
}
|
||||
|
||||
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
|
||||
this.clipboardListener = clipboardListener;
|
||||
}
|
||||
|
||||
@@ -6,15 +6,17 @@ public final class DisplayInfo {
|
||||
private final int rotation;
|
||||
private final int layerStack;
|
||||
private final int flags;
|
||||
private final int logicalDensityDpi;
|
||||
|
||||
public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001;
|
||||
|
||||
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) {
|
||||
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int logicalDensityDpi) {
|
||||
this.displayId = displayId;
|
||||
this.size = size;
|
||||
this.rotation = rotation;
|
||||
this.layerStack = layerStack;
|
||||
this.flags = flags;
|
||||
this.logicalDensityDpi = logicalDensityDpi;
|
||||
}
|
||||
|
||||
public int getDisplayId() {
|
||||
@@ -36,5 +38,9 @@ public final class DisplayInfo {
|
||||
public int getFlags() {
|
||||
return flags;
|
||||
}
|
||||
|
||||
public int getLogicalDensityDpi() {
|
||||
return logicalDensityDpi;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
|
||||
public interface Codec {
|
||||
|
||||
enum Type {
|
||||
@@ -14,4 +16,9 @@ public interface Codec {
|
||||
String getName();
|
||||
|
||||
String getMimeType();
|
||||
|
||||
static String getMimeType(MediaCodec codec) {
|
||||
String[] types = codec.getCodecInfo().getSupportedTypes();
|
||||
return types.length > 0 ? types[0] : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.genymobile.scrcpy.util;
|
||||
|
||||
import com.genymobile.scrcpy.BuildConfig;
|
||||
|
||||
import android.os.Build;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.OsConstants;
|
||||
@@ -17,23 +18,38 @@ public final class IO {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException {
|
||||
// ByteBuffer position is not updated as expected by Os.write() on old Android versions, so
|
||||
// count the remaining bytes manually.
|
||||
// See <https://github.com/Genymobile/scrcpy/issues/291>.
|
||||
int remaining = from.remaining();
|
||||
while (remaining > 0) {
|
||||
private static int write(FileDescriptor fd, ByteBuffer from) throws IOException {
|
||||
while (true) {
|
||||
try {
|
||||
int w = Os.write(fd, from);
|
||||
return Os.write(fd, from);
|
||||
} catch (ErrnoException e) {
|
||||
if (e.errno != OsConstants.EINTR) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
while (from.hasRemaining()) {
|
||||
write(fd, from);
|
||||
}
|
||||
} else {
|
||||
// ByteBuffer position is not updated as expected by Os.write() on old Android versions, so
|
||||
// handle the position and the remaining bytes manually.
|
||||
// See <https://github.com/Genymobile/scrcpy/issues/291>.
|
||||
int position = from.position();
|
||||
int remaining = from.remaining();
|
||||
while (remaining > 0) {
|
||||
int w = write(fd, from);
|
||||
if (BuildConfig.DEBUG && w < 0) {
|
||||
// w should not be negative, since an exception is thrown on error
|
||||
throw new AssertionError("Os.write() returned a negative value (" + w + ")");
|
||||
}
|
||||
remaining -= w;
|
||||
} catch (ErrnoException e) {
|
||||
if (e.errno != OsConstants.EINTR) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
position += w;
|
||||
from.position(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public final class LogUtils {
|
||||
} else {
|
||||
for (CodecUtils.DeviceEncoder encoder : videoEncoders) {
|
||||
builder.append("\n --video-codec=").append(encoder.getCodec().getName());
|
||||
builder.append(" --video-encoder='").append(encoder.getInfo().getName()).append("'");
|
||||
builder.append(" --video-encoder=").append(encoder.getInfo().getName());
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
@@ -45,7 +45,7 @@ public final class LogUtils {
|
||||
} else {
|
||||
for (CodecUtils.DeviceEncoder encoder : audioEncoders) {
|
||||
builder.append("\n --audio-codec=").append(encoder.getCodec().getName());
|
||||
builder.append(" --audio-encoder='").append(encoder.getInfo().getName()).append("'");
|
||||
builder.append(" --audio-encoder=").append(encoder.getInfo().getName());
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.genymobile.scrcpy.video;
|
||||
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.HandlerExecutor;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
@@ -1,42 +1,115 @@
|
||||
package com.genymobile.scrcpy.video;
|
||||
|
||||
import com.genymobile.scrcpy.device.ConfigurationException;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.device.DisplayInfo;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.view.IDisplayFoldListener;
|
||||
import android.view.IRotationWatcher;
|
||||
import android.view.Surface;
|
||||
|
||||
public class ScreenCapture extends SurfaceCapture implements Device.RotationListener, Device.FoldListener {
|
||||
public class ScreenCapture extends SurfaceCapture {
|
||||
|
||||
private final Device device;
|
||||
|
||||
private final int displayId;
|
||||
private int maxSize;
|
||||
private final Rect crop;
|
||||
private final int lockVideoOrientation;
|
||||
private int layerStack;
|
||||
private int dpi;
|
||||
|
||||
private Size deviceSize;
|
||||
private ScreenInfo screenInfo;
|
||||
|
||||
private IBinder display;
|
||||
private VirtualDisplay virtualDisplay;
|
||||
|
||||
public ScreenCapture(Device device) {
|
||||
private IRotationWatcher rotationWatcher;
|
||||
private IDisplayFoldListener displayFoldListener;
|
||||
|
||||
public ScreenCapture(Device device, int displayId, int maxSize, Rect crop, int lockVideoOrientation) {
|
||||
this.device = device;
|
||||
this.displayId = displayId;
|
||||
this.maxSize = maxSize;
|
||||
this.crop = crop;
|
||||
this.lockVideoOrientation = lockVideoOrientation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
device.setRotationListener(this);
|
||||
device.setFoldListener(this);
|
||||
public void init() throws ConfigurationException {
|
||||
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
|
||||
if (displayInfo == null) {
|
||||
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
|
||||
throw new ConfigurationException("Unknown display id: " + displayId);
|
||||
}
|
||||
|
||||
deviceSize = displayInfo.getSize();
|
||||
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation);
|
||||
device.setScreenInfo(screenInfo);
|
||||
layerStack = displayInfo.getLayerStack();
|
||||
dpi = displayInfo.getLogicalDensityDpi();
|
||||
|
||||
if (displayId == 0) {
|
||||
rotationWatcher = new IRotationWatcher.Stub() {
|
||||
@Override
|
||||
public void onRotationChanged(int rotation) {
|
||||
synchronized (ScreenCapture.this) {
|
||||
screenInfo = screenInfo.withDeviceRotation(rotation);
|
||||
device.setScreenInfo(screenInfo);
|
||||
}
|
||||
}
|
||||
};
|
||||
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
displayFoldListener = new IDisplayFoldListener.Stub() {
|
||||
@Override
|
||||
public void onDisplayFoldChanged(int displayId, boolean folded) {
|
||||
if (ScreenCapture.this.displayId != displayId) {
|
||||
// Ignore events related to other display ids
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (ScreenCapture.this) {
|
||||
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
|
||||
if (displayInfo == null) {
|
||||
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
deviceSize = displayInfo.getSize();
|
||||
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation);
|
||||
device.setScreenInfo(screenInfo);
|
||||
}
|
||||
}
|
||||
};
|
||||
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
|
||||
}
|
||||
|
||||
if ((displayInfo.getFlags() & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
|
||||
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Surface surface) {
|
||||
ScreenInfo screenInfo = device.getScreenInfo();
|
||||
Rect contentRect = screenInfo.getContentRect();
|
||||
|
||||
// does not include the locked video orientation
|
||||
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
|
||||
int videoRotation = screenInfo.getVideoRotation();
|
||||
int layerStack = device.getLayerStack();
|
||||
|
||||
if (display != null) {
|
||||
SurfaceControl.destroyDisplay(display);
|
||||
@@ -49,8 +122,12 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList
|
||||
|
||||
try {
|
||||
Rect videoRect = screenInfo.getVideoSize().toRect();
|
||||
int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | (1
|
||||
<< 6) /* DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORT_TOUCH */ | 1 << 8 | 1 << 9 | 1 << 10 | 1 << 11 | 1 << 12 | 1 << 13 | 1 << 14;
|
||||
|
||||
virtualDisplay = ServiceManager.getDisplayManager()
|
||||
.createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), device.getDisplayId(), surface);
|
||||
.createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), dpi, surface, flags);
|
||||
device.setVirtualDisplayId(virtualDisplay.getDisplay().getDisplayId());
|
||||
Ln.d("Display: using DisplayManager API");
|
||||
} catch (Exception displayManagerException) {
|
||||
try {
|
||||
@@ -67,8 +144,12 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
device.setRotationListener(null);
|
||||
device.setFoldListener(null);
|
||||
if (rotationWatcher != null) {
|
||||
ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener);
|
||||
}
|
||||
if (display != null) {
|
||||
SurfaceControl.destroyDisplay(display);
|
||||
display = null;
|
||||
@@ -80,26 +161,18 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList
|
||||
}
|
||||
|
||||
@Override
|
||||
public Size getSize() {
|
||||
return device.getScreenInfo().getVideoSize();
|
||||
public synchronized Size getSize() {
|
||||
return screenInfo.getVideoSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setMaxSize(int maxSize) {
|
||||
device.setMaxSize(maxSize);
|
||||
public synchronized boolean setMaxSize(int newMaxSize) {
|
||||
maxSize = newMaxSize;
|
||||
screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation);
|
||||
device.setScreenInfo(screenInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFoldChanged(int displayId, boolean folded) {
|
||||
requestReset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRotationChanged(int rotation) {
|
||||
requestReset();
|
||||
}
|
||||
|
||||
private static IBinder createDisplay() throws Exception {
|
||||
// Since Android 12 (preview), secure displays could not be created with shell permissions anymore.
|
||||
// On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S".
|
||||
|
||||
@@ -2,8 +2,8 @@ package com.genymobile.scrcpy.video;
|
||||
|
||||
import com.genymobile.scrcpy.BuildConfig;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.genymobile.scrcpy.video;
|
||||
|
||||
import com.genymobile.scrcpy.device.ConfigurationException;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
|
||||
import android.view.Surface;
|
||||
@@ -34,7 +35,7 @@ public abstract class SurfaceCapture {
|
||||
/**
|
||||
* Called once before the capture starts.
|
||||
*/
|
||||
public abstract void init() throws IOException;
|
||||
public abstract void init() throws ConfigurationException, IOException;
|
||||
|
||||
/**
|
||||
* Called after the capture ends (if and only if {@link #init()} has been called).
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package com.genymobile.scrcpy.video;
|
||||
|
||||
import com.genymobile.scrcpy.AsyncProcessor;
|
||||
import com.genymobile.scrcpy.device.ConfigurationException;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.device.Streamer;
|
||||
import com.genymobile.scrcpy.util.Codec;
|
||||
import com.genymobile.scrcpy.util.CodecOption;
|
||||
import com.genymobile.scrcpy.util.CodecUtils;
|
||||
import com.genymobile.scrcpy.device.ConfigurationException;
|
||||
import com.genymobile.scrcpy.util.IO;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.device.Streamer;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
@@ -39,7 +39,7 @@ public class SurfaceEncoder implements AsyncProcessor {
|
||||
private final String encoderName;
|
||||
private final List<CodecOption> codecOptions;
|
||||
private final int videoBitRate;
|
||||
private final int maxFps;
|
||||
private final float maxFps;
|
||||
private final boolean downsizeOnError;
|
||||
|
||||
private boolean firstFrameSent;
|
||||
@@ -48,8 +48,8 @@ public class SurfaceEncoder implements AsyncProcessor {
|
||||
private Thread thread;
|
||||
private final AtomicBoolean stopped = new AtomicBoolean();
|
||||
|
||||
public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
|
||||
boolean downsizeOnError) {
|
||||
public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, float maxFps, List<CodecOption> codecOptions,
|
||||
String encoderName, boolean downsizeOnError) {
|
||||
this.capture = capture;
|
||||
this.streamer = streamer;
|
||||
this.videoBitRate = videoBitRate;
|
||||
@@ -205,7 +205,13 @@ public class SurfaceEncoder implements AsyncProcessor {
|
||||
if (encoderName != null) {
|
||||
Ln.d("Creating encoder by name: '" + encoderName + "'");
|
||||
try {
|
||||
return MediaCodec.createByCodecName(encoderName);
|
||||
MediaCodec mediaCodec = MediaCodec.createByCodecName(encoderName);
|
||||
String mimeType = Codec.getMimeType(mediaCodec);
|
||||
if (!codec.getMimeType().equals(mimeType)) {
|
||||
Ln.e("Video encoder type for \"" + encoderName + "\" (" + mimeType + ") does not match codec type (" + codec.getMimeType() + ")");
|
||||
throw new ConfigurationException("Incorrect encoder type: " + encoderName);
|
||||
}
|
||||
return mediaCodec;
|
||||
} catch (IllegalArgumentException e) {
|
||||
Ln.e("Video encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
|
||||
throw new ConfigurationException("Unknown encoder: " + encoderName);
|
||||
@@ -225,7 +231,7 @@ public class SurfaceEncoder implements AsyncProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||
private static MediaFormat createFormat(String videoMimeType, int bitRate, float maxFps, List<CodecOption> codecOptions) {
|
||||
MediaFormat format = new MediaFormat();
|
||||
format.setString(MediaFormat.KEY_MIME, videoMimeType);
|
||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.util.Command;
|
||||
import com.genymobile.scrcpy.FakeContext;
|
||||
import com.genymobile.scrcpy.device.DisplayInfo;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.Command;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.view.Display;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.regex.Matcher;
|
||||
@@ -39,7 +42,7 @@ public final class DisplayManager {
|
||||
public static DisplayInfo parseDisplayInfo(String dumpsysDisplayOutput, int displayId) {
|
||||
Pattern regex = Pattern.compile(
|
||||
"^ mOverrideDisplayInfo=DisplayInfo\\{\".*?, displayId " + displayId + ".*?(, FLAG_.*)?, real ([0-9]+) x ([0-9]+).*?, "
|
||||
+ "rotation ([0-9]+).*?, layerStack ([0-9]+)",
|
||||
+ "rotation ([0-9]+).*?, density ([0-9]+).*?, layerStack ([0-9]+)",
|
||||
Pattern.MULTILINE);
|
||||
Matcher m = regex.matcher(dumpsysDisplayOutput);
|
||||
if (!m.find()) {
|
||||
@@ -49,9 +52,10 @@ public final class DisplayManager {
|
||||
int width = Integer.parseInt(m.group(2));
|
||||
int height = Integer.parseInt(m.group(3));
|
||||
int rotation = Integer.parseInt(m.group(4));
|
||||
int layerStack = Integer.parseInt(m.group(5));
|
||||
int density = Integer.parseInt(m.group(5));
|
||||
int layerStack = Integer.parseInt(m.group(6));
|
||||
|
||||
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
|
||||
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density);
|
||||
}
|
||||
|
||||
private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) {
|
||||
@@ -98,7 +102,8 @@ public final class DisplayManager {
|
||||
int rotation = cls.getDeclaredField("rotation").getInt(displayInfo);
|
||||
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
|
||||
int flags = cls.getDeclaredField("flags").getInt(displayInfo);
|
||||
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
|
||||
int logicalDensityDpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo);
|
||||
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, logicalDensityDpi);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
@@ -115,13 +120,17 @@ public final class DisplayManager {
|
||||
private Method getCreateVirtualDisplayMethod() throws NoSuchMethodException {
|
||||
if (createVirtualDisplayMethod == null) {
|
||||
createVirtualDisplayMethod = android.hardware.display.DisplayManager.class
|
||||
.getMethod("createVirtualDisplay", String.class, int.class, int.class, int.class, Surface.class);
|
||||
.getMethod("createVirtualDisplay", String.class, int.class, int.class, int.class, Surface.class, int.class);
|
||||
}
|
||||
return createVirtualDisplayMethod;
|
||||
}
|
||||
|
||||
public VirtualDisplay createVirtualDisplay(String name, int width, int height, int displayIdToMirror, Surface surface) throws Exception {
|
||||
Method method = getCreateVirtualDisplayMethod();
|
||||
return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface);
|
||||
public VirtualDisplay createVirtualDisplay(String name, int width, int height, int dpi, Surface surface, int flags) throws Exception {
|
||||
//Method method = getCreateVirtualDisplayMethod();
|
||||
Constructor<android.hardware.display.DisplayManager> ctor = android.hardware.display.DisplayManager.class.getDeclaredConstructor(Context.class);
|
||||
ctor.setAccessible(true);
|
||||
android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get());
|
||||
return dm.createVirtualDisplay(name, width, height, dpi, surface, flags);
|
||||
//return (VirtualDisplay) method.invoke(null, name, width, height, dpi, surface, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,13 +200,29 @@ public final class WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterRotationWatcher(IRotationWatcher rotationWatcher) {
|
||||
try {
|
||||
manager.getClass().getMethod("removeRotationWatcher", IRotationWatcher.class).invoke(manager, rotationWatcher);
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not unregister rotation watcher", e);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(29)
|
||||
public void registerDisplayFoldListener(IDisplayFoldListener foldListener) {
|
||||
try {
|
||||
Class<?> cls = manager.getClass();
|
||||
cls.getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener);
|
||||
manager.getClass().getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener);
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not register display fold listener", e);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(29)
|
||||
public void unregisterDisplayFoldListener(IDisplayFoldListener foldListener) {
|
||||
try {
|
||||
manager.getClass().getMethod("unregisterDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener);
|
||||
} catch (Exception e) {
|
||||
Ln.e("Could not unregister display fold listener", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,8 +324,10 @@ public class ControlMessageReaderTest {
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_UHID_CREATE);
|
||||
dos.writeShort(42); // id
|
||||
dos.writeByte(3); // name size
|
||||
dos.write("ABC".getBytes(StandardCharsets.US_ASCII));
|
||||
byte[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
|
||||
dos.writeShort(data.length); // size
|
||||
dos.writeShort(data.length); // report desc size
|
||||
dos.write(data);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
@@ -335,6 +337,7 @@ public class ControlMessageReaderTest {
|
||||
ControlMessage event = reader.read();
|
||||
Assert.assertEquals(ControlMessage.TYPE_UHID_CREATE, event.getType());
|
||||
Assert.assertEquals(42, event.getId());
|
||||
Assert.assertEquals("ABC", event.getText());
|
||||
Assert.assertArrayEquals(data, event.getData());
|
||||
|
||||
Assert.assertEquals(-1, bis.read()); // EOS
|
||||
|
||||
Reference in New Issue
Block a user