Compare commits

...

28 Commits

Author SHA1 Message Date
1029835d75 audioplayer without locks 2024-01-01 19:22:55 +01:00
6a58891e13 Use current time as initial timestamp on error
If the initial timestamp could not be retrieved, use the current time as
returned by System.nanoTime(). In practice, it is the same time base as
AudioRecord timestamps.

Fixes #4536 <https://github.com/Genymobile/scrcpy/issues/4536>
2023-12-16 20:17:33 +01:00
ec41896c85 Fix integer overflow for audio packet duration
The result is assigned to a long (64-bit signed integer), but the
intermediate multiplication was stored in an int (32-bit signed
integer).

This value is only used as a fallback when no timestamp could be
retrieved, that's why it did not cause too much harm so far.

Fixes #4536 <https://github.com/Genymobile/scrcpy/issues/4536>
2023-12-16 20:16:31 +01:00
4cd61b5a90 Fix checkstyle violation
Reported by checkstyle:

> [ant:checkstyle] [INFO]
> scrcpy/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java:48:
> Line is longer than 150 characters (found 167). [LineLength]
2023-12-16 20:12:58 +01:00
d2ed4510a7 Simulate tilt multitouch event by pressing Shift
PR #4529 <https://github.com/Genymobile/scrcpy/pull/4529>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-12-15 22:12:07 +01:00
604dfd7c6b Fix incorrect compgen usage
PR #4532 <https://github.com/Genymobile/scrcpy/pull/4532>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-12-14 17:08:19 +01:00
af69689ec1 Fix bash completion syntax
PR #4532 <https://github.com/Genymobile/scrcpy/pull/4532>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-12-14 17:07:20 +01:00
cbce42336d Fix manpage syntax
The '-' character must be escaped.

Fixes #4528 <https://github.com/Genymobile/scrcpy/issues/4528>
2023-12-13 13:42:57 +01:00
c9a4d2b38f Use up-to-date values on display fold change
When a display is folded or unfolded, the maxSize may have been updated
since the option was passed, and deviceSize must be updated.

Refs #4469 <https://github.com/Genymobile/scrcpy/pull/4469>
2023-12-04 08:50:12 +01:00
1beec99f82 Explicitly exit cleanup process
This avoids an internal crash reported in `adb logcat`.

Refs #4456 <https://github.com/Genymobile/scrcpy/pull/4456#issuecomment-1837427802>
2023-12-04 08:45:35 +01:00
5ce8672ebc Add clipboard workaround for IQOO device
Fixes #4492 <https://github.com/Genymobile/scrcpy/issues/4492>
2023-12-04 08:43:31 +01:00
3001f8a2d5 Adapt AudioRecord workaround to Android 14
Android 14 added a new int parameter "halInputFlags" to an internal
method:
<f6135d75db>

Fixes #4492 <https://github.com/Genymobile/scrcpy/issues/4492>
2023-12-03 18:01:11 +01:00
c6ff78f414 Update links to v2.3.1 2023-12-02 12:39:05 +01:00
40f2560d98 Bump version to 2.3.1 2023-12-02 12:30:19 +01:00
26aa28c998 Merge branch 'master' into release 2023-12-02 12:29:31 +01:00
ef79fcbbd2 Fix AV1 demuxing
For AV1, the config packet must not be merged with the next non-config
packet.

This fixes the following error when passing --video-codec=av1:

> INFO: [FFmpeg] libdav1d 1.3.0
> ERROR: [FFmpeg] Unknown OBU type 0 of size 29393
> ERROR: [FFmpeg] Error parsing OBU data
> ERROR: Decoder 'video': could not send video packet: -1094995529

PR #4487 <https://github.com/Genymobile/scrcpy/pull/4487>
2023-12-02 12:20:01 +01:00
9497f39fb4 Do not fail if SDL_INIT_VIDEO fails without video
The SDL video subsystem may be initialized so that clipboard
synchronization works even without video playback.

But if the video subsystem initialization fails (e.g. because no video
device is available), consider it as an error only if video playback is
enabled.

Refs 5e59ed3135
Fixes #4477 <https://github.com/Genymobile/scrcpy/issues/4477>
2023-11-29 12:16:05 +01:00
bf056b1fee Do not initialize SDL video when not necessary
The SDL video subsystem is required for video playback and clipboard
synchronization.

If neither is used, it is not necessary to initialize it.

Refs 5e59ed3135
Refs 110b3a16f6
Refs #4418 <https://github.com/Genymobile/scrcpy/issues/4418>
Refs #4477 <https://github.com/Genymobile/scrcpy/issues/4477>
2023-11-29 12:14:07 +01:00
bd9292931e Mention exclusive_caps mode in v4l2 documentation
PR #4435 <https://github.com/Genymobile/scrcpy/pull/4435>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-11-28 08:32:28 +01:00
140a49b8be Add workaround for Samsung devices issues
On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked()
calls ActivityThread.currentActivityThread().getConfiguration(), which
requires a non-null ConfigurationController.

Fixes <https://github.com/Genymobile/scrcpy/issues/4467>
2023-11-27 09:29:06 +01:00
4135c411af Fix compilation error
Fix the following warning/error:

    ../app/src/cli.c:2158:17: warning: a label can only be part of a
    statement and a declaration is not a statement [-Wpedantic]

With some compilers, this is an error rather than a pedantic warning.

Refs <https://github.com/Genymobile/scrcpy/issues/2256#issuecomment-1467008307>
2023-11-25 23:56:46 +01:00
5e061636f6 Update links to v2.3 2023-11-25 22:15:07 +01:00
5f3fb843f5 Bump version to 2.3
The previous version bump to 2.2 was incorrect, it was updated by:

    ./bump_version v2.2

instead of:

    ./bump_version 2.2

Correctly bump to version 2.3.

Refs #4433 <https://github.com/Genymobile/scrcpy/issues/4433#issuecomment-1816830875>
2023-11-25 21:40:27 +01:00
ce8126f322 Merge branch 'master' into release 2023-11-25 21:37:37 +01:00
d037b02cc2 Fix scrcpy-console.desktop
The argument passed to scrcpy was not applied, the full command must be
passed as a single argument.

PR #4448 <https://github.com/Genymobile/scrcpy/pull/4448>
2023-11-25 21:35:04 +01:00
Kid
89761213c3 Do not quote $SHELL in .desktop files
This does not work properly on some desktop environments (KDE), and
$SHELL is unlikely to require quoting.

Fixes #4367 <https://github.com/Genymobile/scrcpy/issues/4367>
PR #4448 <https://github.com/Genymobile/scrcpy/pull/4448>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-11-25 21:28:43 +01:00
Kid
8db4e78b34 Fix Linux desktop files
There were too many backslashes in the Exec line.

Fixes #4367 <https://github.com/Genymobile/scrcpy/issues/4367>
PR #4448 <https://github.com/Genymobile/scrcpy/pull/4448>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-11-25 21:28:43 +01:00
25e33566f5 Mention turning off audio in camera documentation 2023-11-21 08:46:38 +01:00
35 changed files with 550 additions and 574 deletions

View File

@ -1,4 +1,4 @@
# scrcpy (v2.2) # scrcpy (v2.3.1)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" /> <img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />

View File

@ -115,13 +115,12 @@ _scrcpy() {
COMPREPLY=($(compgen -W 'front back external' -- "$cur")) COMPREPLY=($(compgen -W 'front back external' -- "$cur"))
return return
;; ;;
--orientation --orientation|--display-orientation)
--display-orientation) COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
return return
;; ;;
--record-orientation) --record-orientation)
COMPREPLY=($(compgen -> '0 90 180 270' -- "$cur")) COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur"))
return return
;; ;;
--lock-video-orientation) --lock-video-orientation)

View File

@ -5,7 +5,7 @@ Comment=Display and control your Android device
# For some users, the PATH or ADB environment variables are set from the shell # For some users, the PATH or ADB environment variables are set from the shell
# startup file, like .bashrc or .zshrc… Run an interactive shell to get # startup file, like .bashrc or .zshrc… Run an interactive shell to get
# environment correctly initialized. # environment correctly initialized.
Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy --pause-on-exit=if-error" Exec=/bin/sh -c "\\$SHELL -i -c 'scrcpy --pause-on-exit=if-error'"
Icon=scrcpy Icon=scrcpy
Terminal=true Terminal=true
Type=Application Type=Application

View File

@ -5,7 +5,7 @@ Comment=Display and control your Android device
# For some users, the PATH or ADB environment variables are set from the shell # For some users, the PATH or ADB environment variables are set from the shell
# startup file, like .bashrc or .zshrc… Run an interactive shell to get # startup file, like .bashrc or .zshrc… Run an interactive shell to get
# environment correctly initialized. # environment correctly initialized.
Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy" Exec=/bin/sh -c "\\$SHELL -i -c scrcpy"
Icon=scrcpy Icon=scrcpy
Terminal=false Terminal=false
Type=Application Type=Application

View File

@ -34,8 +34,8 @@ src = [
'src/trait/frame_source.c', 'src/trait/frame_source.c',
'src/trait/packet_source.c', 'src/trait/packet_source.c',
'src/util/acksync.c', 'src/util/acksync.c',
'src/util/audiobuf.c',
'src/util/average.c', 'src/util/average.c',
'src/util/bytebuf.c',
'src/util/file.c', 'src/util/file.c',
'src/util/intmap.c', 'src/util/intmap.c',
'src/util/intr.c', 'src/util/intr.c',
@ -212,9 +212,10 @@ if get_option('buildtype') == 'debug'
['test_binary', [ ['test_binary', [
'tests/test_binary.c', 'tests/test_binary.c',
]], ]],
['test_bytebuf', [ ['test_audiobuf', [
'tests/test_bytebuf.c', 'tests/test_audiobuf.c',
'src/util/bytebuf.c', 'src/util/audiobuf.c',
'src/util/memory.c',
]], ]],
['test_cli', [ ['test_cli', [
'tests/test_cli.c', 'tests/test_cli.c',

View File

@ -13,7 +13,7 @@ BEGIN
VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "LegalCopyright", "Romain Vimont, Genymobile"
VALUE "OriginalFilename", "scrcpy.exe" VALUE "OriginalFilename", "scrcpy.exe"
VALUE "ProductName", "scrcpy" VALUE "ProductName", "scrcpy"
VALUE "ProductVersion", "v2.2" VALUE "ProductVersion", "2.3.1"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View File

@ -124,7 +124,7 @@ Use USB device (if there is exactly one, like adb -d).
Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
.TP .TP
.BI "\-\-disable-screensaver" .BI "\-\-disable\-screensaver"
Disable screensaver while scrcpy is running. Disable screensaver while scrcpy is running.
.TP .TP
@ -642,7 +642,11 @@ Enable/disable FPS counter (print frames/second in logs)
.TP .TP
.B Ctrl+click-and-move .B Ctrl+click-and-move
Pinch-to-zoom from the center of the screen Pinch-to-zoom and rotate from the center of the screen
.TP
.B Shift+click-and-move
Tilt (slide vertically with two fingers)
.TP .TP
.B Drag & drop APK file .B Drag & drop APK file

View File

@ -66,8 +66,7 @@ static void SDLCALL
sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
struct sc_audio_player *ap = userdata; struct sc_audio_player *ap = userdata;
// This callback is called with the lock used by SDL_AudioDeviceLock(), so // This callback is called with the lock used by SDL_AudioDeviceLock()
// the audiobuf is protected
assert(len_int > 0); assert(len_int > 0);
size_t len = len_int; size_t len = len_int;
@ -77,8 +76,9 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count);
#endif #endif
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); bool played = atomic_load_explicit(&ap->played, memory_order_relaxed);
if (!ap->played) { if (!played) {
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf);
// Part of the buffering is handled by inserting initial silence. The // Part of the buffering is handled by inserting initial silence. The
// remaining (margin) last samples will be handled by compensation. // remaining (margin) last samples will be handled by compensation.
uint32_t margin = 30 * ap->sample_rate / 1000; // 30ms uint32_t margin = 30 * ap->sample_rate / 1000; // 30ms
@ -93,10 +93,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
} }
} }
uint32_t read = MIN(buffered_samples, count); uint32_t read = sc_audiobuf_read(&ap->buf, stream, count);
if (read) {
sc_audiobuf_read(&ap->buf, stream, read);
}
if (read < count) { if (read < count) {
uint32_t silence = count - read; uint32_t silence = count - read;
@ -109,13 +106,16 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
silence); silence);
memset(stream + TO_BYTES(read), 0, TO_BYTES(silence)); memset(stream + TO_BYTES(read), 0, TO_BYTES(silence));
if (ap->received) { bool received = atomic_load_explicit(&ap->received,
memory_order_relaxed);
if (received) {
// Inserting additional samples immediately increases buffering // Inserting additional samples immediately increases buffering
ap->underflow += silence; atomic_fetch_add_explicit(&ap->underflow, silence,
memory_order_relaxed);
} }
} }
ap->played = true; atomic_store_explicit(&ap->played, true, memory_order_relaxed);
} }
static uint8_t * static uint8_t *
@ -162,123 +162,120 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
// swr_convert() returns the number of samples which would have been // swr_convert() returns the number of samples which would have been
// written if the buffer was big enough. // written if the buffer was big enough.
uint32_t samples_written = MIN(ret, dst_nb_samples); uint32_t samples = MIN(ret, dst_nb_samples);
#ifndef SC_AUDIO_PLAYER_NDEBUG #ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] %" PRIu32 " samples written to buffer", samples_written); LOGD("[Audio] %" PRIu32 " samples written to buffer", samples);
#endif #endif
// Since this function is the only writer, the current available space is uint32_t cap = sc_audiobuf_capacity(&ap->buf);
// at least the previous available space. In practice, it should almost if (samples > cap) {
// always be possible to write without lock. // Very very unlikely: a single resampled frame should never
bool lockless_write = samples_written <= ap->previous_can_write; // exceed the audio buffer size (or something is very wrong).
if (lockless_write) { // Ignore the first bytes in swr_buf to avoid memory corruption anyway.
sc_audiobuf_prepare_write(&ap->buf, swr_buf, samples_written); //skipped_samples = samples - cap;
swr_buf += TO_BYTES(samples - cap);
samples = cap;
} }
SDL_LockAudioDevice(ap->device); uint32_t written = sc_audiobuf_write(&ap->buf, swr_buf, samples);
if (written < samples) {
uint32_t remaining = samples - written;
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); // 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);
if (lockless_write) { // Retry with the lock
sc_audiobuf_commit_write(&ap->buf, samples_written); written += sc_audiobuf_write(&ap->buf,
} else { swr_buf + TO_BYTES(written),
uint32_t can_write = sc_audiobuf_can_write(&ap->buf); remaining);
if (samples_written > can_write) { if (written < samples) {
// Entering this branch is very unlikely, the audio buffer is remaining = samples - written;
// allocated with a size sufficient to store 1 second more than the // Still insufficient, drop old samples to make space
// target buffering. If this happens, though, we have to skip old uint32_t skipped_samples =
// samples. sc_audiobuf_read(&ap->buf, NULL, remaining);
uint32_t cap = sc_audiobuf_capacity(&ap->buf); assert(skipped_samples == remaining);
if (samples_written > 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
swr_buf += TO_BYTES(samples_written - cap);
// This change in samples_written will impact the
// instant_compensation below
samples_written = cap;
}
assert(samples_written >= can_write); // Dropping input samples instantly decreases buffering
if (samples_written > can_write) { ap->avg_buffering.avg -= skipped_samples;
uint32_t skip_samples = samples_written - can_write;
assert(buffered_samples >= skip_samples);
sc_audiobuf_skip(&ap->buf, skip_samples);
buffered_samples -= skip_samples;
if (ap->played) {
// Dropping input samples instantly decreases buffering
ap->avg_buffering.avg -= skip_samples;
}
}
// It should remain exactly the expected size to write the new // Now there is enough space
// samples. uint32_t w = sc_audiobuf_write(&ap->buf,
assert(sc_audiobuf_can_write(&ap->buf) == samples_written); swr_buf + TO_BYTES(written),
remaining);
assert(w == remaining);
} }
sc_audiobuf_write(&ap->buf, swr_buf, samples_written); SDL_UnlockAudioDevice(ap->device);
} }
buffered_samples += samples_written; uint32_t underflow = 0;
assert(buffered_samples == sc_audiobuf_can_read(&ap->buf)); uint32_t max_buffered_samples;
bool played = atomic_load_explicit(&ap->played, memory_order_relaxed);
// Read with lock held, to be used after unlocking
bool played = ap->played;
uint32_t underflow = ap->underflow;
if (played) { if (played) {
uint32_t max_buffered_samples = ap->target_buffering underflow = atomic_exchange_explicit(&ap->underflow, 0,
+ 12 * ap->output_buffer memory_order_relaxed);
+ ap->target_buffering / 10;
if (buffered_samples > max_buffered_samples) {
uint32_t skip_samples = buffered_samples - max_buffered_samples;
sc_audiobuf_skip(&ap->buf, skip_samples);
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
" samples", skip_samples);
}
// reset (the current value was copied to a local variable) max_buffered_samples = ap->target_buffering
ap->underflow = 0; + 12 * ap->output_buffer
+ ap->target_buffering / 10;
} else { } else {
// SDL playback not started yet, do not accumulate more than // SDL playback not started yet, do not accumulate more than
// max_initial_buffering samples, this would cause unnecessary delay // max_initial_buffering samples, this would cause unnecessary delay
// (and glitches to compensate) on start. // (and glitches to compensate) on start.
uint32_t max_initial_buffering = ap->target_buffering max_buffered_samples = ap->target_buffering + 2 * ap->output_buffer;
+ 2 * ap->output_buffer; }
if (buffered_samples > max_initial_buffering) {
uint32_t skip_samples = buffered_samples - max_initial_buffering; uint32_t can_read = sc_audiobuf_can_read(&ap->buf);
sc_audiobuf_skip(&ap->buf, skip_samples); 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);
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
" samples", skip_samples);
}
SDL_UnlockAudioDevice(ap->device);
if (skip_samples) {
// The buffer contained too much data, ignore any reported underflow
underflow = 0;
if (played) {
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
" samples", skip_samples);
#ifndef SC_AUDIO_PLAYER_NDEBUG #ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples", } else {
skip_samples); LOGD("[Audio] Playback not started, skipping %" PRIu32
" samples", skip_samples);
#endif #endif
}
} }
} }
ap->previous_can_write = sc_audiobuf_can_write(&ap->buf); atomic_store_explicit(&ap->received, true, memory_order_relaxed);
ap->received = true;
SDL_UnlockAudioDevice(ap->device);
if (played) { if (played) {
// Number of samples added (or removed, if negative) for compensation // Number of samples added (or removed, if negative) for compensation
int32_t instant_compensation = int32_t instant_compensation = (int32_t) written - frame->nb_samples;
(int32_t) samples_written - frame->nb_samples;
int32_t inserted_silence = (int32_t) underflow; int32_t inserted_silence = (int32_t) underflow;
// The compensation must apply instantly, it must not be smoothed // The compensation must apply instantly, it must not be smoothed
ap->avg_buffering.avg += instant_compensation + inserted_silence; ap->avg_buffering.avg += instant_compensation + inserted_silence;
// However, the buffering level must be smoothed // However, the buffering level must be smoothed
sc_average_push(&ap->avg_buffering, buffered_samples); sc_average_push(&ap->avg_buffering, can_read);
#ifndef SC_AUDIO_PLAYER_NDEBUG #ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] buffered_samples=%" PRIu32 " avg_buffering=%f", LOGD("[Audio] buffered_samples=%" PRIu32 " avg_buffering=%f",
buffered_samples, sc_average_get(&ap->avg_buffering)); buffered_samples, sc_average_get(&ap->avg_buffering));
#endif #endif
ap->samples_since_resync += samples_written; ap->samples_since_resync += written;
if (ap->samples_since_resync >= ap->sample_rate) { if (ap->samples_since_resync >= ap->sample_rate) {
// Recompute compensation every second // Recompute compensation every second
ap->samples_since_resync = 0; ap->samples_since_resync = 0;
@ -288,7 +285,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
if (abs(diff) < (int) ap->sample_rate / 1000) { if (abs(diff) < (int) ap->sample_rate / 1000) {
// Do not compensate for less than 1ms, the error is just noise // Do not compensate for less than 1ms, the error is just noise
diff = 0; diff = 0;
} else if (diff < 0 && buffered_samples < ap->target_buffering) { } else if (diff < 0 && can_read < ap->target_buffering) {
// Do not accelerate if the instant buffering level is below // Do not accelerate if the instant buffering level is below
// the average, this would increase underflow // the average, this would increase underflow
diff = 0; diff = 0;
@ -300,8 +297,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
int abs_max_diff = distance / 50; int abs_max_diff = distance / 50;
diff = CLAMP(diff, -abs_max_diff, abs_max_diff); diff = CLAMP(diff, -abs_max_diff, abs_max_diff);
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
" compensation=%d", ap->target_buffering, avg, " compensation=%d", ap->target_buffering, avg, can_read, diff);
buffered_samples, diff);
if (diff != ap->compensation) { if (diff != ap->compensation) {
int ret = swr_set_compensation(swr_ctx, diff, distance); int ret = swr_set_compensation(swr_ctx, diff, distance);
@ -413,16 +409,14 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
} }
ap->swr_buf_alloc_size = initial_swr_buf_size; ap->swr_buf_alloc_size = initial_swr_buf_size;
ap->previous_can_write = sc_audiobuf_can_write(&ap->buf);
// Samples are produced and consumed by blocks, so the buffering must be // Samples are produced and consumed by blocks, so the buffering must be
// smoothed to get a relatively stable value. // smoothed to get a relatively stable value.
sc_average_init(&ap->avg_buffering, 32); sc_average_init(&ap->avg_buffering, 32);
ap->samples_since_resync = 0; ap->samples_since_resync = 0;
ap->received = false; ap->received = false;
ap->played = false; atomic_init(&ap->played, false);
ap->underflow = 0; atomic_init(&ap->underflow, 0);
ap->compensation = 0; ap->compensation = 0;
// The thread calling open() is the thread calling push(), which fills the // The thread calling open() is the thread calling push(), which fills the

View File

@ -32,7 +32,7 @@ struct sc_audio_player {
uint16_t output_buffer; uint16_t output_buffer;
// Audio buffer to communicate between the receiver and the SDL audio // Audio buffer to communicate between the receiver and the SDL audio
// callback (protected by SDL_AudioDeviceLock()) // callback
struct sc_audiobuf buf; struct sc_audiobuf buf;
// The previous empty space in the buffer (only used by the receiver // The previous empty space in the buffer (only used by the receiver
@ -61,19 +61,16 @@ struct sc_audio_player {
uint32_t samples_since_resync; uint32_t samples_since_resync;
// Number of silence samples inserted since the last received packet // Number of silence samples inserted since the last received packet
// (protected by SDL_AudioDeviceLock()) atomic_uint_least32_t underflow;
uint32_t underflow;
// Current applied compensation value (only used by the receiver thread) // Current applied compensation value (only used by the receiver thread)
int compensation; int compensation;
// Set to true the first time a sample is received (protected by // Set to true the first time a sample is received
// SDL_AudioDeviceLock()) atomic_bool received;
bool received;
// Set to true the first time the SDL callback is called (protected by // Set to true the first time the SDL callback is called
// SDL_AudioDeviceLock()) atomic_bool played;
bool played;
const struct sc_audio_player_callbacks *cbs; const struct sc_audio_player_callbacks *cbs;
void *cbs_userdata; void *cbs_userdata;

View File

@ -947,7 +947,11 @@ static const struct sc_shortcut shortcuts[] = {
}, },
{ {
.shortcuts = { "Ctrl+click-and-move" }, .shortcuts = { "Ctrl+click-and-move" },
.text = "Pinch-to-zoom from the center of the screen", .text = "Pinch-to-zoom and rotate from the center of the screen",
},
{
.shortcuts = { "Shift+click-and-move" },
.text = "Tilt (slide vertically with two fingers)",
}, },
{ {
.shortcuts = { "Drag & drop APK file" }, .shortcuts = { "Drag & drop APK file" },
@ -2154,7 +2158,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false; return false;
} }
break; break;
case OPT_ORIENTATION: case OPT_ORIENTATION: {
enum sc_orientation orientation; enum sc_orientation orientation;
if (!parse_orientation(optarg, &orientation)) { if (!parse_orientation(optarg, &orientation)) {
return false; return false;
@ -2162,6 +2166,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->display_orientation = orientation; opts->display_orientation = orientation;
opts->record_orientation = orientation; opts->record_orientation = orientation;
break; break;
}
case OPT_RENDER_DRIVER: case OPT_RENDER_DRIVER:
opts->render_driver = optarg; opts->render_driver = optarg;
break; break;

View File

@ -227,8 +227,9 @@ run_demuxer(void *data) {
} }
// Config packets must be merged with the next non-config packet only for // Config packets must be merged with the next non-config packet only for
// video streams // H.26x
bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO; bool must_merge_config_packet = raw_codec_id == SC_CODEC_ID_H264
|| raw_codec_id == SC_CODEC_ID_H265;
struct sc_packet_merger merger; struct sc_packet_merger merger;

View File

@ -76,6 +76,8 @@ sc_input_manager_init(struct sc_input_manager *im,
im->sdl_shortcut_mods.count = shortcut_mods->count; im->sdl_shortcut_mods.count = shortcut_mods->count;
im->vfinger_down = false; im->vfinger_down = false;
im->vfinger_invert_x = false;
im->vfinger_invert_y = false;
im->last_keycode = SDLK_UNKNOWN; im->last_keycode = SDLK_UNKNOWN;
im->last_mod = 0; im->last_mod = 0;
@ -347,9 +349,14 @@ simulate_virtual_finger(struct sc_input_manager *im,
} }
static struct sc_point static struct sc_point
inverse_point(struct sc_point point, struct sc_size size) { inverse_point(struct sc_point point, struct sc_size size,
point.x = size.width - point.x; bool invert_x, bool invert_y) {
point.y = size.height - point.y; if (invert_x) {
point.x = size.width - point.x;
}
if (invert_y) {
point.y = size.height - point.y;
}
return point; return point;
} }
@ -605,7 +612,9 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
struct sc_point mouse = struct sc_point mouse =
sc_screen_convert_window_to_frame_coords(im->screen, event->x, sc_screen_convert_window_to_frame_coords(im->screen, event->x,
event->y); event->y);
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size,
im->vfinger_invert_x,
im->vfinger_invert_y);
simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger); simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger);
} }
} }
@ -726,7 +735,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
return; return;
} }
// Pinch-to-zoom simulation. // Pinch-to-zoom, rotate and tilt simulation.
// //
// If Ctrl is hold when the left-click button is pressed, then // If Ctrl is hold when the left-click button is pressed, then
// pinch-to-zoom mode is enabled: on every mouse event until the left-click // pinch-to-zoom mode is enabled: on every mouse event until the left-click
@ -735,14 +744,29 @@ 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 // In other words, the center of the rotation/scaling is the center of the
// screen. // screen.
#define CTRL_PRESSED (SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL)) //
// To simulate a 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.
const SDL_Keymod keymod = SDL_GetModState();
const bool ctrl_pressed = keymod & KMOD_CTRL;
const bool shift_pressed = keymod & KMOD_SHIFT;
if (event->button == SDL_BUTTON_LEFT && if (event->button == SDL_BUTTON_LEFT &&
((down && !im->vfinger_down && CTRL_PRESSED) || ((down && !im->vfinger_down &&
((ctrl_pressed && !shift_pressed) ||
(!ctrl_pressed && shift_pressed))) ||
(!down && im->vfinger_down))) { (!down && im->vfinger_down))) {
struct sc_point mouse = struct sc_point mouse =
sc_screen_convert_window_to_frame_coords(im->screen, event->x, sc_screen_convert_window_to_frame_coords(im->screen, event->x,
event->y); event->y);
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); if (down) {
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,
im->vfinger_invert_x,
im->vfinger_invert_y);
enum android_motionevent_action action = down enum android_motionevent_action action = down
? AMOTION_EVENT_ACTION_DOWN ? AMOTION_EVENT_ACTION_DOWN
: AMOTION_EVENT_ACTION_UP; : AMOTION_EVENT_ACTION_UP;

View File

@ -32,6 +32,8 @@ struct sc_input_manager {
} sdl_shortcut_mods; } sdl_shortcut_mods;
bool vfinger_down; bool vfinger_down;
bool vfinger_invert_x;
bool vfinger_invert_y;
// Tracks the number of identical consecutive shortcut key down events. // Tracks the number of identical consecutive shortcut key down events.
// Not to be confused with event->repeat, which counts the number of // Not to be confused with event->repeat, which counts the number of

View File

@ -419,12 +419,21 @@ scrcpy(struct scrcpy_options *options) {
sdl_set_hints(options->render_driver); sdl_set_hints(options->render_driver);
} }
// Initialize the video subsystem even if --no-video or --no-video-playback if (options->video_playback ||
// is passed so that clipboard synchronization still works. (options->control && options->clipboard_autosync)) {
// <https://github.com/Genymobile/scrcpy/issues/4418> // Initialize the video subsystem even if --no-video or
if (SDL_Init(SDL_INIT_VIDEO)) { // --no-video-playback is passed so that clipboard synchronization
LOGE("Could not initialize SDL video: %s", SDL_GetError()); // still works.
goto end; // <https://github.com/Genymobile/scrcpy/issues/4418>
if (SDL_Init(SDL_INIT_VIDEO)) {
// If it fails, it is an error only if video playback is enabled
if (options->video_playback) {
LOGE("Could not initialize SDL video: %s", SDL_GetError());
goto end;
} else {
LOGW("Could not initialize SDL video: %s", SDL_GetError());
}
}
} }
if (options->audio_playback) { if (options->audio_playback) {

109
app/src/util/audiobuf.c Normal file
View File

@ -0,0 +1,109 @@
#include "audiobuf.h"
#include <stdlib.h>
#include <string.h>
#include <util/log.h>
#include <util/memory.h>
bool
sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size,
uint32_t capacity) {
assert(sample_size);
assert(capacity);
// The actual capacity is (alloc_size - 1) so that head == tail is
// non-ambiguous
buf->alloc_size = capacity + 1;
buf->data = sc_allocarray(buf->alloc_size, sample_size);
if (!buf->data) {
LOG_OOM();
return false;
}
buf->sample_size = sample_size;
atomic_init(&buf->head, 0);
atomic_init(&buf->tail, 0);
return true;
}
void
sc_audiobuf_destroy(struct sc_audiobuf *buf) {
free(buf->data);
}
uint32_t
sc_audiobuf_read(struct sc_audiobuf *buf, void *to, uint32_t samples_count) {
assert(samples_count);
// Only the reader thread can write tail without synchronization, so
// memory_order_relaxed is sufficient
uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_relaxed);
// The head cursor is updated after the data is written to the array
uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire);
uint32_t can_read = (buf->alloc_size + head - tail) % buf->alloc_size;
if (samples_count > can_read) {
samples_count = can_read;
}
if (to) {
uint32_t right_limit = tail < head ? head : buf->alloc_size;
uint32_t right_count = right_limit - tail;
if (right_count > samples_count) {
right_count = samples_count;
}
memcpy(to,
buf->data + (tail * buf->sample_size),
right_count * buf->sample_size);
if (samples_count > right_count) {
uint32_t left_count = samples_count - right_count;
memcpy(to + (right_count * buf->sample_size),
buf->data,
left_count * buf->sample_size);
}
}
uint32_t new_tail = (tail + samples_count) % buf->alloc_size;
atomic_store_explicit(&buf->tail, new_tail, memory_order_release);
return samples_count;
}
uint32_t
sc_audiobuf_write(struct sc_audiobuf *buf, const void *from,
uint32_t samples_count) {
// Only the writer thread can write head, so memory_order_relaxed is
// sufficient
uint32_t head = atomic_load_explicit(&buf->head, memory_order_relaxed);
// The tail cursor is updated after the data is consumed by the reader
uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire);
uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size;
if (samples_count > can_write) {
samples_count = can_write;
}
uint32_t right_count = buf->alloc_size - head;
if (right_count > samples_count) {
right_count = samples_count;
}
memcpy(buf->data + (head * buf->sample_size),
from,
right_count * buf->sample_size);
if (samples_count > right_count) {
uint32_t left_count = samples_count - right_count;
memcpy(buf->data,
from + (right_count * buf->sample_size),
left_count * buf->sample_size);
}
uint32_t new_head = (head + samples_count) % buf->alloc_size;
atomic_store_explicit(&buf->head, new_head, memory_order_release);
return samples_count;
}

View File

@ -3,19 +3,25 @@
#include "common.h" #include "common.h"
#include <assert.h>
#include <stdatomic.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include "util/bytebuf.h"
/** /**
* Wrapper around bytebuf to read and write samples * Wrapper around bytebuf to read and write samples
* *
* Each sample takes sample_size bytes. * Each sample takes sample_size bytes.
*/ */
struct sc_audiobuf { struct sc_audiobuf {
struct sc_bytebuf buf; void *data;
size_t sample_size; uint32_t sample_size;
uint32_t alloc_size; // in samples
atomic_uint_least32_t head; // writer cursor, in samples
atomic_uint_least32_t tail; // reader cursor, in samples
// empty: tail == head
// full: ((tail + 1) % alloc_size) == head
}; };
static inline uint32_t static inline uint32_t
@ -29,66 +35,31 @@ sc_audiobuf_to_bytes(struct sc_audiobuf *buf, uint32_t samples) {
return samples * buf->sample_size; return samples * buf->sample_size;
} }
static inline bool bool
sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size, sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size,
uint32_t capacity) { uint32_t capacity);
buf->sample_size = sample_size;
return sc_bytebuf_init(&buf->buf, capacity * sample_size + 1);
}
static inline void void
sc_audiobuf_read(struct sc_audiobuf *buf, uint8_t *to, uint32_t samples) { sc_audiobuf_destroy(struct sc_audiobuf *buf);
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_read(&buf->buf, to, bytes);
}
static inline void uint32_t
sc_audiobuf_skip(struct sc_audiobuf *buf, uint32_t samples) { sc_audiobuf_read(struct sc_audiobuf *buf, void *to, uint32_t samples_count);
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_skip(&buf->buf, bytes);
}
static inline void uint32_t
sc_audiobuf_write(struct sc_audiobuf *buf, const uint8_t *from, sc_audiobuf_write(struct sc_audiobuf *buf, const void *from,
uint32_t samples) { uint32_t samples_count);
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_write(&buf->buf, from, bytes);
}
static inline void static inline uint32_t
sc_audiobuf_prepare_write(struct sc_audiobuf *buf, const uint8_t *from, sc_audiobuf_capacity(struct sc_audiobuf *buf) {
uint32_t samples) { assert(buf->alloc_size);
size_t bytes = sc_audiobuf_to_bytes(buf, samples); return buf->alloc_size - 1;
sc_bytebuf_prepare_write(&buf->buf, from, bytes);
}
static inline void
sc_audiobuf_commit_write(struct sc_audiobuf *buf, uint32_t samples) {
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_commit_write(&buf->buf, bytes);
} }
static inline uint32_t static inline uint32_t
sc_audiobuf_can_read(struct sc_audiobuf *buf) { sc_audiobuf_can_read(struct sc_audiobuf *buf) {
size_t bytes = sc_bytebuf_can_read(&buf->buf); uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire);
return sc_audiobuf_to_samples(buf, bytes); uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire);
} return (buf->alloc_size + head - tail) % buf->alloc_size;
static inline uint32_t
sc_audiobuf_can_write(struct sc_audiobuf *buf) {
size_t bytes = sc_bytebuf_can_write(&buf->buf);
return sc_audiobuf_to_samples(buf, bytes);
}
static inline uint32_t
sc_audiobuf_capacity(struct sc_audiobuf *buf) {
size_t bytes = sc_bytebuf_capacity(&buf->buf);
return sc_audiobuf_to_samples(buf, bytes);
}
static inline void
sc_audiobuf_destroy(struct sc_audiobuf *buf) {
sc_bytebuf_destroy(&buf->buf);
} }
#endif #endif

View File

@ -1,104 +0,0 @@
#include "bytebuf.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include "util/log.h"
bool
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size) {
assert(alloc_size);
buf->data = malloc(alloc_size);
if (!buf->data) {
LOG_OOM();
return false;
}
buf->alloc_size = alloc_size;
buf->head = 0;
buf->tail = 0;
return true;
}
void
sc_bytebuf_destroy(struct sc_bytebuf *buf) {
free(buf->data);
}
void
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) {
assert(len);
assert(len <= sc_bytebuf_can_read(buf));
assert(buf->tail != buf->head); // the buffer could not be empty
size_t right_limit = buf->tail < buf->head ? buf->head : buf->alloc_size;
size_t right_len = right_limit - buf->tail;
if (len < right_len) {
right_len = len;
}
memcpy(to, buf->data + buf->tail, right_len);
if (len > right_len) {
memcpy(to + right_len, buf->data, len - right_len);
}
buf->tail = (buf->tail + len) % buf->alloc_size;
}
void
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) {
assert(len);
assert(len <= sc_bytebuf_can_read(buf));
assert(buf->tail != buf->head); // the buffer could not be empty
buf->tail = (buf->tail + len) % buf->alloc_size;
}
static inline void
sc_bytebuf_write_step0(struct sc_bytebuf *buf, const uint8_t *from,
size_t len) {
size_t right_len = buf->alloc_size - buf->head;
if (len < right_len) {
right_len = len;
}
memcpy(buf->data + buf->head, from, right_len);
if (len > right_len) {
memcpy(buf->data, from + right_len, len - right_len);
}
}
static inline void
sc_bytebuf_write_step1(struct sc_bytebuf *buf, size_t len) {
buf->head = (buf->head + len) % buf->alloc_size;
}
void
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) {
assert(len);
assert(len <= sc_bytebuf_can_write(buf));
sc_bytebuf_write_step0(buf, from, len);
sc_bytebuf_write_step1(buf, len);
}
void
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
size_t len) {
// *This function MUST NOT access buf->tail (even in assert()).*
// The purpose of this function is to allow a reader and a writer to access
// different parts of the buffer in parallel simultaneously. It is intended
// to be called without lock (only sc_bytebuf_commit_write() is intended to
// be called with lock held).
assert(len < buf->alloc_size - 1);
sc_bytebuf_write_step0(buf, from, len);
}
void
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len) {
assert(len <= sc_bytebuf_can_write(buf));
sc_bytebuf_write_step1(buf, len);
}

View File

@ -1,114 +0,0 @@
#ifndef SC_BYTEBUF_H
#define SC_BYTEBUF_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
struct sc_bytebuf {
uint8_t *data;
// The actual capacity is (allocated - 1) so that head == tail is
// non-ambiguous
size_t alloc_size;
size_t head; // writter cursor
size_t tail; // reader cursor
// empty: tail == head
// full: ((tail + 1) % alloc_size) == head
};
bool
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size);
/**
* Copy from the bytebuf to a user-provided array
*
* The caller must check that len <= sc_bytebuf_read_available() (it is an
* error to attempt to read more bytes than available).
*
* This function is guaranteed not to write to buf->head.
*/
void
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len);
/**
* Drop len bytes from the buffer
*
* The caller must check that len <= sc_bytebuf_read_available() (it is an
* error to attempt to skip more bytes than available).
*
* This function is guaranteed not to write to buf->head.
*
* It is equivalent to call sc_bytebuf_read() to some array and discard the
* array (but this function is more efficient since there is no copy).
*/
void
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len);
/**
* Copy the user-provided array to the bytebuf
*
* The caller must check that len <= sc_bytebuf_write_available() (it is an
* error to write more bytes than the remaining available space).
*
* This function is guaranteed not to write to buf->tail.
*/
void
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len);
/**
* Copy the user-provided array to the bytebuf, but do not advance the cursor
*
* The caller must check that len <= sc_bytebuf_write_available() (it is an
* error to write more bytes than the remaining available space).
*
* After this function is called, the write must be committed with
* sc_bytebuf_commit_write().
*
* The purpose of this mechanism is to acquire a lock only to commit the write,
* but not to perform the actual copy.
*
* This function is guaranteed not to access buf->tail.
*/
void
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
size_t len);
/**
* Commit a prepared write
*/
void
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len);
/**
* Return the number of bytes which can be read
*
* It is an error to read more bytes than available.
*/
static inline size_t
sc_bytebuf_can_read(struct sc_bytebuf *buf) {
return (buf->alloc_size + buf->head - buf->tail) % buf->alloc_size;
}
/**
* Return the number of bytes which can be written
*
* It is an error to write more bytes than available.
*/
static inline size_t
sc_bytebuf_can_write(struct sc_bytebuf *buf) {
return (buf->alloc_size + buf->tail - buf->head - 1) % buf->alloc_size;
}
/**
* Return the actual capacity of the buffer (can_read() + can_write())
*/
static inline size_t
sc_bytebuf_capacity(struct sc_bytebuf *buf) {
return buf->alloc_size - 1;
}
void
sc_bytebuf_destroy(struct sc_bytebuf *buf);
#endif

128
app/tests/test_audiobuf.c Normal file
View File

@ -0,0 +1,128 @@
#include "common.h"
#include <assert.h>
#include <string.h>
#include "util/audiobuf.h"
static void test_audiobuf_simple(void) {
struct sc_audiobuf buf;
uint32_t data[20];
bool ok = sc_audiobuf_init(&buf, 4, 20);
assert(ok);
uint32_t samples[] = {1, 2, 3, 4, 5};
uint32_t w = sc_audiobuf_write(&buf, samples, 5);
assert(w == 5);
uint32_t r = sc_audiobuf_read(&buf, data, 4);
assert(r == 4);
assert(!memcmp(data, samples, 16));
uint32_t samples2[] = {6, 7, 8};
w = sc_audiobuf_write(&buf, samples2, 3);
assert(w == 3);
uint32_t single = 9;
w = sc_audiobuf_write(&buf, &single, 1);
assert(w == 1);
r = sc_audiobuf_read(&buf, &data[4], 8);
assert(r == 5);
uint32_t expected[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
assert(!memcmp(data, expected, 36));
sc_audiobuf_destroy(&buf);
}
static void test_audiobuf_boundaries(void) {
struct sc_audiobuf buf;
uint32_t data[20];
bool ok = sc_audiobuf_init(&buf, 4, 20);
assert(ok);
uint32_t samples[] = {1, 2, 3, 4, 5, 6};
uint32_t w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 6);
w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 6);
w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 6);
uint32_t r = sc_audiobuf_read(&buf, data, 9);
assert(r == 9);
uint32_t expected[] = {1, 2, 3, 4, 5, 6, 1, 2, 3};
assert(!memcmp(data, expected, 36));
uint32_t samples2[] = {7, 8, 9, 10, 11};
w = sc_audiobuf_write(&buf, samples2, 5);
assert(w == 5);
uint32_t single = 12;
w = sc_audiobuf_write(&buf, &single, 1);
assert(w == 1);
w = sc_audiobuf_read(&buf, NULL, 3);
assert(w == 3);
assert(sc_audiobuf_can_read(&buf) == 12);
r = sc_audiobuf_read(&buf, data, 12);
assert(r == 12);
uint32_t expected2[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
assert(!memcmp(data, expected2, 48));
sc_audiobuf_destroy(&buf);
}
static void test_audiobuf_partial_read_write(void) {
struct sc_audiobuf buf;
uint32_t data[15];
bool ok = sc_audiobuf_init(&buf, 4, 10);
assert(ok);
uint32_t samples[] = {1, 2, 3, 4, 5, 6};
uint32_t w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 6);
w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 4);
w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 0);
uint32_t r = sc_audiobuf_read(&buf, data, 3);
assert(r == 3);
uint32_t expected[] = {1, 2, 3};
assert(!memcmp(data, expected, 12));
w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 3);
r = sc_audiobuf_read(&buf, data, 15);
assert(r == 10);
uint32_t expected2[] = {4, 5, 6, 1, 2, 3, 4, 1, 2, 3};
assert(!memcmp(data, expected2, 12));
sc_audiobuf_destroy(&buf);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_audiobuf_simple();
test_audiobuf_boundaries();
test_audiobuf_partial_read_write();
return 0;
}

View File

@ -1,126 +0,0 @@
#include "common.h"
#include <assert.h>
#include <string.h>
#include "util/bytebuf.h"
static void test_bytebuf_simple(void) {
struct sc_bytebuf buf;
uint8_t data[20];
bool ok = sc_bytebuf_init(&buf, 20);
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello", sizeof("hello") - 1);
assert(sc_bytebuf_can_read(&buf) == 5);
sc_bytebuf_read(&buf, data, 4);
assert(!strncmp((char *) data, "hell", 4));
sc_bytebuf_write(&buf, (uint8_t *) " world", sizeof(" world") - 1);
assert(sc_bytebuf_can_read(&buf) == 7);
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
assert(sc_bytebuf_can_read(&buf) == 8);
sc_bytebuf_read(&buf, &data[4], 8);
assert(sc_bytebuf_can_read(&buf) == 0);
data[12] = '\0';
assert(!strcmp((char *) data, "hello world!"));
assert(sc_bytebuf_can_read(&buf) == 0);
sc_bytebuf_destroy(&buf);
}
static void test_bytebuf_boundaries(void) {
struct sc_bytebuf buf;
uint8_t data[20];
bool ok = sc_bytebuf_init(&buf, 20);
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 6);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 12);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 18);
sc_bytebuf_read(&buf, data, 9);
assert(!strncmp((char *) data, "hello hel", 9));
assert(sc_bytebuf_can_read(&buf) == 9);
sc_bytebuf_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
assert(sc_bytebuf_can_read(&buf) == 14);
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
assert(sc_bytebuf_can_read(&buf) == 15);
sc_bytebuf_skip(&buf, 3);
assert(sc_bytebuf_can_read(&buf) == 12);
sc_bytebuf_read(&buf, data, 12);
data[12] = '\0';
assert(!strcmp((char *) data, "hello world!"));
assert(sc_bytebuf_can_read(&buf) == 0);
sc_bytebuf_destroy(&buf);
}
static void test_bytebuf_two_steps_write(void) {
struct sc_bytebuf buf;
uint8_t data[20];
bool ok = sc_bytebuf_init(&buf, 20);
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 6);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 12);
sc_bytebuf_prepare_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 12); // write not committed yet
sc_bytebuf_read(&buf, data, 9);
assert(!strncmp((char *) data, "hello hel", 3));
assert(sc_bytebuf_can_read(&buf) == 3);
sc_bytebuf_commit_write(&buf, sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 9);
sc_bytebuf_prepare_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
assert(sc_bytebuf_can_read(&buf) == 9); // write not committed yet
sc_bytebuf_commit_write(&buf, sizeof("world") - 1);
assert(sc_bytebuf_can_read(&buf) == 14);
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
assert(sc_bytebuf_can_read(&buf) == 15);
sc_bytebuf_skip(&buf, 3);
assert(sc_bytebuf_can_read(&buf) == 12);
sc_bytebuf_read(&buf, data, 12);
data[12] = '\0';
assert(!strcmp((char *) data, "hello world!"));
assert(sc_bytebuf_can_read(&buf) == 0);
sc_bytebuf_destroy(&buf);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_bytebuf_simple();
test_bytebuf_boundaries();
test_bytebuf_two_steps_write();
return 0;
}

View File

@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server #### Option 2: Use prebuilt server
- [`scrcpy-server-v2.2`][direct-scrcpy-server] - [`scrcpy-server-v2.3.1`][direct-scrcpy-server]
<sub>SHA-256: `c85c4aa84305efb69115cd497a120ebdd10258993b4cf123a8245b3d99d49874`</sub> <sub>SHA-256: `f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b`</sub>
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-server-v2.2 [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1
Download the prebuilt server somewhere, and specify its path during the Meson Download the prebuilt server somewhere, and specify its path during the Meson
configuration: configuration:

View File

@ -18,6 +18,17 @@ scrcpy --video-source=display --audio-source=mic # force display AND micropho
scrcpy --video-source=camera --audio-source=output # force camera AND device audio output scrcpy --video-source=camera --audio-source=output # force camera AND device audio output
``` ```
Audio can be disabled:
```bash
# audio not captured at all
scrcpy --video-source=camera --no-audio
scrcpy --video-source=camera --no-audio --record=file.mp4
# audio captured and recorded, but not played
scrcpy --video-source=camera --no-audio-playback --record=file.mp4
```
## List ## List

View File

@ -85,7 +85,7 @@ way as <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>).
To disable automatic clipboard synchronization, use To disable automatic clipboard synchronization, use
`--no-clipboard-autosync`. `--no-clipboard-autosync`.
## Pinch-to-zoom ## Pinch-to-zoom, rotate and tilt simulation
To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_. To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_.
@ -93,8 +93,12 @@ More precisely, hold down <kbd>Ctrl</kbd> while pressing the left-click button.
Until the left-click button is released, all mouse movements scale and rotate Until the left-click button is released, all mouse movements scale and rotate
the content (if supported by the app) relative to the center of the screen. the content (if supported by the app) relative to the center of the screen.
To simulate a tilt gesture: <kbd>Shift</kbd>+_click-and-move-up-or-down_.
Technically, _scrcpy_ generates additional touch events from a "virtual finger" Technically, _scrcpy_ generates additional touch events from a "virtual finger"
at a location inverted through the center of the screen. 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.
## Key repeat ## Key repeat

View File

@ -49,7 +49,8 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Synchronize clipboards and paste⁵ | <kbd>MOD</kbd>+<kbd>v</kbd> | Synchronize clipboards and paste⁵ | <kbd>MOD</kbd>+<kbd>v</kbd>
| Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> | Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd> | Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
| Pinch-to-zoom | <kbd>Ctrl</kbd>+_click-and-move_ | Pinch-to-zoom/rotate | <kbd>Ctrl</kbd>+_click-and-move_
| Tilt (slide vertically with 2 fingers) | <kbd>Shift</kbd>+_click-and-move_
| Drag & drop APK file | Install APK from computer | Drag & drop APK file | Install APK from computer
| Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device) | Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device)

View File

@ -21,6 +21,13 @@ This will create a new video device in `/dev/videoN`, where `N` is an integer
(more [options](https://github.com/umlaeute/v4l2loopback#options) are available (more [options](https://github.com/umlaeute/v4l2loopback#options) are available
to create several devices or devices with specific IDs). to create several devices or devices with specific IDs).
If you encounter problems detecting your device with Chrome/WebRTC, you can try
`exclusive_caps` mode:
```
sudo modprobe v4l2loopback exclusive_caps=1
```
To list the enabled devices: To list the enabled devices:
```bash ```bash

View File

@ -4,14 +4,14 @@
Download the [latest release]: Download the [latest release]:
- [`scrcpy-win64-v2.2.zip`][direct-win64] (64-bit) - [`scrcpy-win64-v2.3.1.zip`][direct-win64] (64-bit)
<sub>SHA-256: `9f9da88ac4c8319dcb9bf852f2d9bba942bac663413383419cddf64eaa5685bd`</sub> <sub>SHA-256: `f1f78ac98214078425804e524a1bed515b9d4b8a05b78d210a4ced2b910b262d`</sub>
- [`scrcpy-win32-v2.2.zip`][direct-win32] (32-bit) - [`scrcpy-win32-v2.3.1.zip`][direct-win32] (32-bit)
<sub>SHA-256: `cb84269fc847b8b880e320879492a1ae6c017b42175f03e199530f7a53be9d74`</sub> <sub>SHA-256: `5dffc2d432e9b8b5b0e16f12e71428c37c70d9124cfbe7620df0b41b7efe91ff`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest [latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-win64-v2.2.zip [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win64-v2.3.1.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-win32-v2.2.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win32-v2.3.1.zip
and extract it. and extract it.

View File

@ -2,8 +2,8 @@
set -e set -e
BUILDDIR=build-auto BUILDDIR=build-auto
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-server-v2.2 PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1
PREBUILT_SERVER_SHA256=c85c4aa84305efb69115cd497a120ebdd10258993b4cf123a8245b3d99d49874 PREBUILT_SERVER_SHA256=f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b
echo "[scrcpy] Downloading prebuilt server..." echo "[scrcpy] Downloading prebuilt server..."
wget "$PREBUILT_SERVER_URL" -O scrcpy-server wget "$PREBUILT_SERVER_URL" -O scrcpy-server

View File

@ -1,5 +1,5 @@
project('scrcpy', 'c', project('scrcpy', 'c',
version: 'v2.2', version: '2.3.1',
meson_version: '>= 0.48', meson_version: '>= 0.48',
default_options: [ default_options: [
'c_std=c11', 'c_std=c11',

View File

@ -7,8 +7,8 @@ android {
applicationId "com.genymobile.scrcpy" applicationId "com.genymobile.scrcpy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 34 targetSdkVersion 34
versionCode 200 versionCode 20301
versionName "v2.2" versionName "2.3.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {

View File

@ -12,7 +12,7 @@
set -e set -e
SCRCPY_DEBUG=false SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=v2.2 SCRCPY_VERSION_NAME=2.3.1
PLATFORM=${ANDROID_PLATFORM:-34} PLATFORM=${ANDROID_PLATFORM:-34}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0}

View File

@ -153,13 +153,14 @@ public final class AudioCapture {
previousRecorderTimestamp = timestamp.nanoTime; previousRecorderTimestamp = timestamp.nanoTime;
} else { } else {
if (nextPts == 0) { if (nextPts == 0) {
Ln.w("Could not get any audio timestamp"); Ln.w("Could not get initial audio timestamp");
nextPts = System.nanoTime() / 1000;
} }
// compute from previous timestamp and packet size // compute from previous timestamp and packet size
pts = nextPts; pts = nextPts;
} }
long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE); long durationUs = r * 1000000L / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE);
nextPts = pts + durationUs; nextPts = pts + durationUs;
if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) { if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) {

View File

@ -187,5 +187,7 @@ public final class CleanUp {
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
} }
} }
System.exit(0);
} }
} }

View File

@ -45,11 +45,11 @@ public final class Device {
void onClipboardTextChanged(String text); void onClipboardTextChanged(String text);
} }
private final Size deviceSize;
private final Rect crop; private final Rect crop;
private int maxSize; private int maxSize;
private final int lockVideoOrientation; private final int lockVideoOrientation;
private Size deviceSize;
private ScreenInfo screenInfo; private ScreenInfo screenInfo;
private RotationListener rotationListener; private RotationListener rotationListener;
private FoldListener foldListener; private FoldListener foldListener;
@ -116,8 +116,8 @@ public final class Device {
return; return;
} }
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(), deviceSize = displayInfo.getSize();
options.getMaxSize(), options.getLockVideoOrientation()); screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation);
// notify // notify
if (foldListener != null) { if (foldListener != null) {
foldListener.onFoldChanged(displayId, folded); foldListener.onFoldChanged(displayId, folded);

View File

@ -49,6 +49,7 @@ public final class Workarounds {
} }
public static void apply(boolean audio, boolean camera) { public static void apply(boolean audio, boolean camera) {
boolean mustFillConfigurationController = false;
boolean mustFillAppInfo = false; boolean mustFillAppInfo = false;
boolean mustFillAppContext = false; boolean mustFillAppContext = false;
@ -85,11 +86,23 @@ public final class Workarounds {
mustFillAppContext = true; mustFillAppContext = true;
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(),
// which requires a non-null ConfigurationController.
// ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions.
// <https://github.com/Genymobile/scrcpy/issues/4467>
mustFillConfigurationController = true;
}
if (mustFillConfigurationController) {
// Must be call before fillAppContext() because it is necessary to get a valid system context
fillConfigurationController();
}
if (mustFillAppInfo) { if (mustFillAppInfo) {
Workarounds.fillAppInfo(); fillAppInfo();
} }
if (mustFillAppContext) { if (mustFillAppContext) {
Workarounds.fillAppContext(); fillAppContext();
} }
} }
@ -149,6 +162,22 @@ public final class Workarounds {
} }
} }
private static void fillConfigurationController() {
try {
Class<?> configurationControllerClass = Class.forName("android.app.ConfigurationController");
Class<?> activityThreadInternalClass = Class.forName("android.app.ActivityThreadInternal");
Constructor<?> configurationControllerConstructor = configurationControllerClass.getDeclaredConstructor(activityThreadInternalClass);
configurationControllerConstructor.setAccessible(true);
Object configurationController = configurationControllerConstructor.newInstance(ACTIVITY_THREAD);
Field configurationControllerField = ACTIVITY_THREAD_CLASS.getDeclaredField("mConfigurationController");
configurationControllerField.setAccessible(true);
configurationControllerField.set(ACTIVITY_THREAD, configurationController);
} catch (Throwable throwable) {
Ln.d("Could not fill configuration: " + throwable.getMessage());
}
}
static Context getSystemContext() { static Context getSystemContext() {
try { try {
Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext"); Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext");
@ -256,16 +285,28 @@ public final class Workarounds {
Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel"); Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel");
Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState); Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState);
// private native int native_setup(Object audiorecordThis, if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// Object /*AudioAttributes*/ attributes, // private native int native_setup(Object audiorecordThis,
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, // Object /*AudioAttributes*/ attributes,
// int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource, // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
// long nativeRecordInJavaObj, int maxSharedAudioHistoryMs); // int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource,
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class, // long nativeRecordInJavaObj, int maxSharedAudioHistoryMs);
int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class); Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class,
nativeSetupMethod.setAccessible(true); int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class);
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes, sampleRateArray, nativeSetupMethod.setAccessible(true);
channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, attributionSourceParcel, 0L, 0); initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes,
sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session,
attributionSourceParcel, 0L, 0);
} else {
// Android 14 added a new int parameter "halInputFlags"
// <https://github.com/aosp-mirror/platform_frameworks_base/commit/f6135d75db79b1d48fad3a3b3080d37be20a2313>
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class,
int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class, int.class);
nativeSetupMethod.setAccessible(true);
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes,
sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session,
attributionSourceParcel, 0L, 0, 0);
}
} }
} }

View File

@ -41,8 +41,14 @@ public final class ClipboardManager {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
getMethodVersion = 2; getMethodVersion = 2;
} catch (NoSuchMethodException e3) { } catch (NoSuchMethodException e3) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); try {
getMethodVersion = 3; getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
getMethodVersion = 3;
} catch (NoSuchMethodException e4) {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class);
getMethodVersion = 4;
}
} }
} }
} }
@ -87,8 +93,11 @@ public final class ClipboardManager {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
case 2: case 2:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
default: case 3:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null); return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null);
default:
// The last boolean parameter is "userOperate"
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
} }
} }