Compare commits
13 Commits
split_work
...
clock_nosl
Author | SHA1 | Date | |
---|---|---|---|
0969f4eae7 | |||
2fff9b9edf | |||
57f879d68a | |||
3626d90004 | |||
02f4ff7534 | |||
a3871130cc | |||
53cb5635cf | |||
d7841664f4 | |||
39544f34b4 | |||
4755b97908 | |||
cba2501254 | |||
6ba99a62ff | |||
d2b7315ba6 |
@ -3,9 +3,11 @@ _scrcpy() {
|
||||
local opts="
|
||||
--always-on-top
|
||||
--audio-bit-rate=
|
||||
--audio-buffer=
|
||||
--audio-codec=
|
||||
--audio-codec-options=
|
||||
--audio-encoder=
|
||||
--audio-output-buffer=
|
||||
-b --video-bit-rate=
|
||||
--crop=
|
||||
-d --select-usb
|
||||
@ -115,20 +117,26 @@ _scrcpy() {
|
||||
COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur}))
|
||||
return
|
||||
;;
|
||||
-b|--video-bit-rate \
|
||||
|--codec-options \
|
||||
--audio-bit-rate \
|
||||
|--audio-buffer \
|
||||
|-b|--video-bit-rate \
|
||||
|--audio-codec-options \
|
||||
|--audio-encoder \
|
||||
|--audio-output-buffer \
|
||||
|--crop \
|
||||
|--display \
|
||||
|--display-buffer \
|
||||
|--encoder \
|
||||
|--max-fps \
|
||||
|-m|--max-size \
|
||||
|-p|--port \
|
||||
|--push-target \
|
||||
|--rotation \
|
||||
|--tunnel-host \
|
||||
|--tunnel-port \
|
||||
|--v4l2-buffer \
|
||||
|--v4l2-sink \
|
||||
|--video-codec-options \
|
||||
|--video-encoder \
|
||||
|--tcpip \
|
||||
|--window-*)
|
||||
# Option accepting an argument, but nothing to auto-complete
|
||||
|
@ -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
|
||||
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
|
||||
# environment correctly initialized.
|
||||
Exec=/bin/bash --norc --noprofile -i -c '"$SHELL" -i -c scrcpy || read -p "Press any key to quit..."'
|
||||
Exec=/bin/bash --norc --noprofile -i -c "\"\\$SHELL\" -i -c scrcpy || read -p 'Press Enter to quit...'"
|
||||
Icon=scrcpy
|
||||
Terminal=true
|
||||
Type=Application
|
||||
|
@ -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
|
||||
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
|
||||
# environment correctly initialized.
|
||||
Exec=/bin/sh -c '"$SHELL" -i -c scrcpy'
|
||||
Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy"
|
||||
Icon=scrcpy
|
||||
Terminal=false
|
||||
Type=Application
|
||||
|
@ -10,9 +10,11 @@ local arguments
|
||||
arguments=(
|
||||
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
|
||||
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
|
||||
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
|
||||
'--audio-codec=[Select the audio codec]:codec:(opus aac raw)'
|
||||
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
|
||||
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
|
||||
'--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]'
|
||||
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
|
||||
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
|
||||
{-d,--select-usb}'[Use USB device]'
|
||||
|
@ -277,10 +277,6 @@ if get_option('buildtype') == 'debug'
|
||||
'src/util/strbuf.c',
|
||||
'src/util/term.c',
|
||||
]],
|
||||
['test_clock', [
|
||||
'tests/test_clock.c',
|
||||
'src/clock.c',
|
||||
]],
|
||||
['test_control_msg_serialize', [
|
||||
'tests/test_control_msg_serialize.c',
|
||||
'src/control_msg.c',
|
||||
|
@ -33,6 +33,14 @@ Lower values decrease the latency, but increase the likelyhood of buffer underru
|
||||
|
||||
Default is 50.
|
||||
|
||||
.TP
|
||||
.BI "\-\-audio\-output\-buffer ms
|
||||
Configure the size of the SDL audio output buffer (in milliseconds).
|
||||
|
||||
If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise.
|
||||
|
||||
Default is 5.
|
||||
|
||||
.TP
|
||||
.BI "\-\-audio\-codec " name
|
||||
Select an audio codec (opus, aac or raw).
|
||||
|
@ -59,8 +59,6 @@
|
||||
#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT
|
||||
#define SC_SDL_SAMPLE_FMT AUDIO_F32
|
||||
|
||||
#define SC_AUDIO_OUTPUT_BUFFER_MS 5
|
||||
|
||||
#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ap->buf, (SAMPLES))
|
||||
#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ap->buf, (BYTES))
|
||||
|
||||
@ -230,8 +228,8 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
||||
|
||||
if (played) {
|
||||
uint32_t max_buffered_samples = ap->target_buffering
|
||||
+ 12 * SC_AUDIO_OUTPUT_BUFFER_MS * ap->sample_rate / 1000
|
||||
+ ap->target_buffering / 10;
|
||||
+ 12 * ap->output_buffer
|
||||
+ 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);
|
||||
@ -246,7 +244,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
||||
// max_initial_buffering samples, this would cause unnecessary delay
|
||||
// (and glitches to compensate) on start.
|
||||
uint32_t max_initial_buffering = ap->target_buffering
|
||||
+ 2 * SC_AUDIO_OUTPUT_BUFFER_MS * ap->sample_rate / 1000;
|
||||
+ 2 * ap->output_buffer;
|
||||
if (buffered_samples > max_initial_buffering) {
|
||||
uint32_t skip_samples = buffered_samples - max_initial_buffering;
|
||||
sc_audiobuf_skip(&ap->buf, skip_samples);
|
||||
@ -333,11 +331,28 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
unsigned nb_channels = tmp;
|
||||
#endif
|
||||
|
||||
assert(ctx->sample_rate > 0);
|
||||
assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT));
|
||||
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;
|
||||
|
||||
ap->target_buffering = ap->target_buffering_delay * ap->sample_rate
|
||||
/ SC_TICK_FREQ;
|
||||
|
||||
uint64_t aout_samples = ap->output_buffer_duration * ap->sample_rate
|
||||
/ SC_TICK_FREQ;
|
||||
assert(aout_samples <= 0xFFFF);
|
||||
ap->output_buffer = (uint16_t) aout_samples;
|
||||
|
||||
SDL_AudioSpec desired = {
|
||||
.freq = ctx->sample_rate,
|
||||
.format = SC_SDL_SAMPLE_FMT,
|
||||
.channels = nb_channels,
|
||||
.samples = SC_AUDIO_OUTPUT_BUFFER_MS * ctx->sample_rate / 1000,
|
||||
.samples = aout_samples,
|
||||
.callback = sc_audio_player_sdl_callback,
|
||||
.userdata = ap,
|
||||
};
|
||||
@ -356,11 +371,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
}
|
||||
ap->swr_ctx = swr_ctx;
|
||||
|
||||
assert(ctx->sample_rate > 0);
|
||||
assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT));
|
||||
int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT);
|
||||
assert(out_bytes_per_sample > 0);
|
||||
|
||||
#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);
|
||||
@ -383,13 +393,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
goto error_free_swr_ctx;
|
||||
}
|
||||
|
||||
ap->sample_rate = ctx->sample_rate;
|
||||
ap->nb_channels = nb_channels;
|
||||
ap->out_bytes_per_sample = out_bytes_per_sample;
|
||||
|
||||
ap->target_buffering = ap->target_buffering_delay * ap->sample_rate
|
||||
/ SC_TICK_FREQ;
|
||||
|
||||
// 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
|
||||
@ -458,8 +461,10 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) {
|
||||
}
|
||||
|
||||
void
|
||||
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering) {
|
||||
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering,
|
||||
sc_tick output_buffer_duration) {
|
||||
ap->target_buffering_delay = target_buffering;
|
||||
ap->output_buffer_duration = output_buffer_duration;
|
||||
|
||||
static const struct sc_frame_sink_ops ops = {
|
||||
.open = sc_audio_player_frame_sink_open,
|
||||
|
@ -27,6 +27,10 @@ struct sc_audio_player {
|
||||
sc_tick target_buffering_delay;
|
||||
uint32_t target_buffering; // in samples
|
||||
|
||||
// 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 (protected by SDL_AudioDeviceLock())
|
||||
struct sc_audiobuf buf;
|
||||
@ -80,6 +84,7 @@ struct sc_audio_player_callbacks {
|
||||
};
|
||||
|
||||
void
|
||||
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering);
|
||||
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering,
|
||||
sc_tick audio_output_buffer);
|
||||
|
||||
#endif
|
||||
|
@ -71,6 +71,7 @@ enum {
|
||||
OPT_LIST_DISPLAYS,
|
||||
OPT_REQUIRE_AUDIO,
|
||||
OPT_AUDIO_BUFFER,
|
||||
OPT_AUDIO_OUTPUT_BUFFER,
|
||||
};
|
||||
|
||||
struct sc_option {
|
||||
@ -129,6 +130,16 @@ static const struct sc_option options[] = {
|
||||
"likelyhood of buffer underrun (causing audio glitches).\n"
|
||||
"Default is 50.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_AUDIO_OUTPUT_BUFFER,
|
||||
.longopt = "audio-output-buffer",
|
||||
.argdesc = "ms",
|
||||
.text = "Configure the size of the SDL audio output buffer (in "
|
||||
"milliseconds).\n"
|
||||
"If you get \"robotic\" audio playback, you should test with "
|
||||
"a higher value (10). Do not change this setting otherwise.\n"
|
||||
"Default is 5.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_AUDIO_CODEC,
|
||||
.longopt = "audio-codec",
|
||||
@ -1204,6 +1215,19 @@ parse_buffering_time(const char *s, sc_tick *tick) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_audio_output_buffer(const char *s, sc_tick *tick) {
|
||||
long value;
|
||||
bool ok = parse_integer_arg(s, &value, false, 0, 1000,
|
||||
"audio output buffer");
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*tick = SC_TICK_FROM_MS(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_lock_video_orientation(const char *s,
|
||||
enum sc_lock_video_orientation *lock_mode) {
|
||||
@ -1831,6 +1855,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_AUDIO_OUTPUT_BUFFER:
|
||||
if (!parse_audio_output_buffer(optarg,
|
||||
&opts->audio_output_buffer)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// getopt prints the error message on stderr
|
||||
return false;
|
||||
|
106
app/src/clock.c
106
app/src/clock.c
@ -1,116 +1,34 @@
|
||||
#include "clock.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
#define SC_CLOCK_NDEBUG // comment to debug
|
||||
|
||||
void
|
||||
sc_clock_init(struct sc_clock *clock) {
|
||||
clock->count = 0;
|
||||
clock->head = 0;
|
||||
clock->left_sum.system = 0;
|
||||
clock->left_sum.stream = 0;
|
||||
clock->right_sum.system = 0;
|
||||
clock->right_sum.stream = 0;
|
||||
}
|
||||
|
||||
// Estimate the affine function f(stream) = slope * stream + offset
|
||||
static void
|
||||
sc_clock_estimate(struct sc_clock *clock,
|
||||
double *out_slope, sc_tick *out_offset) {
|
||||
assert(clock->count);
|
||||
|
||||
if (clock->count == 1) {
|
||||
// If there is only 1 point, we can't compute a slope. Assume it is 1.
|
||||
struct sc_clock_point *single_point = &clock->right_sum;
|
||||
*out_slope = 1;
|
||||
*out_offset = single_point->system - single_point->stream;
|
||||
return;
|
||||
}
|
||||
|
||||
struct sc_clock_point left_avg = {
|
||||
.system = clock->left_sum.system / (clock->count / 2),
|
||||
.stream = clock->left_sum.stream / (clock->count / 2),
|
||||
};
|
||||
struct sc_clock_point right_avg = {
|
||||
.system = clock->right_sum.system / ((clock->count + 1) / 2),
|
||||
.stream = clock->right_sum.stream / ((clock->count + 1) / 2),
|
||||
};
|
||||
|
||||
double slope = (double) (right_avg.system - left_avg.system)
|
||||
/ (right_avg.stream - left_avg.stream);
|
||||
|
||||
if (clock->count < SC_CLOCK_RANGE) {
|
||||
/* The first frames are typically received and decoded with more delay
|
||||
* than the others, causing a wrong slope estimation on start. To
|
||||
* compensate, assume an initial slope of 1, then progressively use the
|
||||
* estimated slope. */
|
||||
slope = (clock->count * slope + (SC_CLOCK_RANGE - clock->count))
|
||||
/ SC_CLOCK_RANGE;
|
||||
}
|
||||
|
||||
struct sc_clock_point global_avg = {
|
||||
.system = (clock->left_sum.system + clock->right_sum.system)
|
||||
/ clock->count,
|
||||
.stream = (clock->left_sum.stream + clock->right_sum.stream)
|
||||
/ clock->count,
|
||||
};
|
||||
|
||||
sc_tick offset = global_avg.system - (sc_tick) (global_avg.stream * slope);
|
||||
|
||||
*out_slope = slope;
|
||||
*out_offset = offset;
|
||||
clock->range = 0;
|
||||
clock->offset = 0;
|
||||
}
|
||||
|
||||
void
|
||||
sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
|
||||
struct sc_clock_point *point = &clock->points[clock->head];
|
||||
|
||||
if (clock->count == SC_CLOCK_RANGE || clock->count & 1) {
|
||||
// One point passes from the right sum to the left sum
|
||||
|
||||
unsigned mid;
|
||||
if (clock->count == SC_CLOCK_RANGE) {
|
||||
mid = (clock->head + SC_CLOCK_RANGE / 2) % SC_CLOCK_RANGE;
|
||||
} else {
|
||||
// Only for the first frames
|
||||
mid = clock->count / 2;
|
||||
}
|
||||
|
||||
struct sc_clock_point *mid_point = &clock->points[mid];
|
||||
clock->left_sum.system += mid_point->system;
|
||||
clock->left_sum.stream += mid_point->stream;
|
||||
clock->right_sum.system -= mid_point->system;
|
||||
clock->right_sum.stream -= mid_point->stream;
|
||||
if (clock->range < SC_CLOCK_RANGE) {
|
||||
++clock->range;
|
||||
}
|
||||
|
||||
if (clock->count == SC_CLOCK_RANGE) {
|
||||
// The current point overwrites the previous value in the circular
|
||||
// array, update the left sum accordingly
|
||||
clock->left_sum.system -= point->system;
|
||||
clock->left_sum.stream -= point->stream;
|
||||
} else {
|
||||
++clock->count;
|
||||
}
|
||||
|
||||
point->system = system;
|
||||
point->stream = stream;
|
||||
|
||||
clock->right_sum.system += system;
|
||||
clock->right_sum.stream += stream;
|
||||
|
||||
clock->head = (clock->head + 1) % SC_CLOCK_RANGE;
|
||||
|
||||
// Update estimation
|
||||
sc_clock_estimate(clock, &clock->slope, &clock->offset);
|
||||
sc_tick offset = system - stream;
|
||||
clock->offset = ((clock->range - 1) * clock->offset + offset)
|
||||
/ clock->range;
|
||||
|
||||
#ifndef SC_CLOCK_NDEBUG
|
||||
LOGD("Clock estimation: %f * pts + %" PRItick, clock->slope, clock->offset);
|
||||
LOGD("Clock estimation: pts + %" PRItick, clock->offset);
|
||||
#endif
|
||||
}
|
||||
|
||||
sc_tick
|
||||
sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) {
|
||||
assert(clock->count); // sc_clock_update() must have been called
|
||||
return (sc_tick) (stream * clock->slope) + clock->offset;
|
||||
assert(clock->range); // sc_clock_update() must have been called
|
||||
return stream + clock->offset;
|
||||
}
|
||||
|
@ -3,12 +3,9 @@
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include "util/tick.h"
|
||||
|
||||
#define SC_CLOCK_RANGE 32
|
||||
static_assert(!(SC_CLOCK_RANGE & 1), "SC_CLOCK_RANGE must be even");
|
||||
|
||||
struct sc_clock_point {
|
||||
sc_tick system;
|
||||
@ -21,40 +18,18 @@ struct sc_clock_point {
|
||||
*
|
||||
* f(stream) = slope * stream + offset
|
||||
*
|
||||
* To that end, it stores the SC_CLOCK_RANGE last clock points (the timestamps
|
||||
* of a frame expressed both in stream time and system time) in a circular
|
||||
* array.
|
||||
* Theoretically, the slope encodes the drift between the device clock and the
|
||||
* computer clock. It is expected to be very close to 1.
|
||||
*
|
||||
* To estimate the slope, it splits the last SC_CLOCK_RANGE points into two
|
||||
* sets of SC_CLOCK_RANGE/2 points, and computes their centroid ("average
|
||||
* point"). The slope of the estimated affine function is that of the line
|
||||
* passing through these two points.
|
||||
* Since the clock is used to estimate very close points in the future (which
|
||||
* are reestimated on every clock update, see delay_buffer), the error caused
|
||||
* by clock drift is totally negligible, so it is better to assume that the
|
||||
* slope is 1 than to estimate it (the estimation error would be larger).
|
||||
*
|
||||
* To estimate the offset, it computes the centroid of all the SC_CLOCK_RANGE
|
||||
* points. The resulting affine function passes by this centroid.
|
||||
*
|
||||
* With a circular array, the rolling sums (and average) are quick to compute.
|
||||
* In practice, the estimation is stable and the evolution is smooth.
|
||||
* Therefore, only the offset is estimated.
|
||||
*/
|
||||
struct sc_clock {
|
||||
// Circular array
|
||||
struct sc_clock_point points[SC_CLOCK_RANGE];
|
||||
|
||||
// Number of points in the array (count <= SC_CLOCK_RANGE)
|
||||
unsigned count;
|
||||
|
||||
// Index of the next point to write
|
||||
unsigned head;
|
||||
|
||||
// Sum of the first count/2 points
|
||||
struct sc_clock_point left_sum;
|
||||
|
||||
// Sum of the last (count+1)/2 points
|
||||
struct sc_clock_point right_sum;
|
||||
|
||||
// Estimated slope and offset
|
||||
// (computed on sc_clock_update(), used by sc_clock_to_system_time())
|
||||
double slope;
|
||||
unsigned range;
|
||||
sc_tick offset;
|
||||
};
|
||||
|
||||
|
@ -194,7 +194,7 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink,
|
||||
sc_clock_update(&db->clock, sc_tick_now(), pts);
|
||||
sc_cond_signal(&db->wait_cond);
|
||||
|
||||
if (db->first_frame_asap && db->clock.count == 1) {
|
||||
if (db->first_frame_asap && db->clock.range == 1) {
|
||||
sc_mutex_unlock(&db->mutex);
|
||||
return sc_frame_source_sinks_push(&db->frame_source, frame);
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.display_buffer = 0,
|
||||
.v4l2_buffer = 0,
|
||||
.audio_buffer = SC_TICK_FROM_MS(50),
|
||||
.audio_output_buffer = SC_TICK_FROM_MS(5),
|
||||
#ifdef HAVE_USB
|
||||
.otg = false,
|
||||
#endif
|
||||
|
@ -127,6 +127,7 @@ struct scrcpy_options {
|
||||
sc_tick display_buffer;
|
||||
sc_tick v4l2_buffer;
|
||||
sc_tick audio_buffer;
|
||||
sc_tick audio_output_buffer;
|
||||
#ifdef HAVE_USB
|
||||
bool otg;
|
||||
#endif
|
||||
|
@ -688,7 +688,8 @@ aoa_hid_end:
|
||||
sc_frame_source_add_sink(src, &s->screen.frame_sink);
|
||||
|
||||
if (options->audio) {
|
||||
sc_audio_player_init(&s->audio_player, options->audio_buffer);
|
||||
sc_audio_player_init(&s->audio_player, options->audio_buffer,
|
||||
options->audio_output_buffer);
|
||||
sc_frame_source_add_sink(&s->audio_decoder.frame_source,
|
||||
&s->audio_player.frame_sink);
|
||||
}
|
||||
|
@ -1,79 +0,0 @@
|
||||
#include "common.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include "clock.h"
|
||||
|
||||
void test_small_rolling_sum(void) {
|
||||
struct sc_clock clock;
|
||||
sc_clock_init(&clock);
|
||||
|
||||
assert(clock.count == 0);
|
||||
assert(clock.left_sum.system == 0);
|
||||
assert(clock.left_sum.stream == 0);
|
||||
assert(clock.right_sum.system == 0);
|
||||
assert(clock.right_sum.stream == 0);
|
||||
|
||||
sc_clock_update(&clock, 2, 3);
|
||||
assert(clock.count == 1);
|
||||
assert(clock.left_sum.system == 0);
|
||||
assert(clock.left_sum.stream == 0);
|
||||
assert(clock.right_sum.system == 2);
|
||||
assert(clock.right_sum.stream == 3);
|
||||
|
||||
sc_clock_update(&clock, 10, 20);
|
||||
assert(clock.count == 2);
|
||||
assert(clock.left_sum.system == 2);
|
||||
assert(clock.left_sum.stream == 3);
|
||||
assert(clock.right_sum.system == 10);
|
||||
assert(clock.right_sum.stream == 20);
|
||||
|
||||
sc_clock_update(&clock, 40, 80);
|
||||
assert(clock.count == 3);
|
||||
assert(clock.left_sum.system == 2);
|
||||
assert(clock.left_sum.stream == 3);
|
||||
assert(clock.right_sum.system == 50);
|
||||
assert(clock.right_sum.stream == 100);
|
||||
|
||||
sc_clock_update(&clock, 400, 800);
|
||||
assert(clock.count == 4);
|
||||
assert(clock.left_sum.system == 12);
|
||||
assert(clock.left_sum.stream == 23);
|
||||
assert(clock.right_sum.system == 440);
|
||||
assert(clock.right_sum.stream == 880);
|
||||
}
|
||||
|
||||
void test_large_rolling_sum(void) {
|
||||
const unsigned half_range = SC_CLOCK_RANGE / 2;
|
||||
|
||||
struct sc_clock clock1;
|
||||
sc_clock_init(&clock1);
|
||||
for (unsigned i = 0; i < 5 * half_range; ++i) {
|
||||
sc_clock_update(&clock1, i, 2 * i + 1);
|
||||
}
|
||||
|
||||
struct sc_clock clock2;
|
||||
sc_clock_init(&clock2);
|
||||
for (unsigned i = 3 * half_range; i < 5 * half_range; ++i) {
|
||||
sc_clock_update(&clock2, i, 2 * i + 1);
|
||||
}
|
||||
|
||||
assert(clock1.count == SC_CLOCK_RANGE);
|
||||
assert(clock2.count == SC_CLOCK_RANGE);
|
||||
|
||||
// The values before the last SC_CLOCK_RANGE points in clock1 should have
|
||||
// no impact
|
||||
assert(clock1.left_sum.system == clock2.left_sum.system);
|
||||
assert(clock1.left_sum.stream == clock2.left_sum.stream);
|
||||
assert(clock1.right_sum.system == clock2.right_sum.system);
|
||||
assert(clock1.right_sum.stream == clock2.right_sum.stream);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
test_small_rolling_sum();
|
||||
test_large_rolling_sum();
|
||||
return 0;
|
||||
};
|
11
doc/audio.md
11
doc/audio.md
@ -88,3 +88,14 @@ avoid glitches and smooth the playback:
|
||||
```
|
||||
scrcpy --display-buffer=200 --audio-buffer=200
|
||||
```
|
||||
|
||||
It is also possible to configure another audio buffer (the audio output buffer),
|
||||
by default set to 5ms. Don't change it, unless you get some [robotic and glitchy
|
||||
sound][#3793]:
|
||||
|
||||
```bash
|
||||
# Only if absolutely necessary
|
||||
scrcpy --audio-output-buffer=10
|
||||
```
|
||||
|
||||
[#3793]: https://github.com/Genymobile/scrcpy/issues/3793
|
||||
|
@ -59,45 +59,58 @@ public final class AudioCapture {
|
||||
}
|
||||
|
||||
private static void startWorkaroundAndroid11() {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||
// Android 11 requires Apps to be at foreground to record audio.
|
||||
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
|
||||
// But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android
|
||||
// shell ("com.android.shell").
|
||||
// If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the
|
||||
// foreground.
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
|
||||
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
|
||||
// Wait for activity to start
|
||||
SystemClock.sleep(150);
|
||||
}
|
||||
}
|
||||
// Android 11 requires Apps to be at foreground to record audio.
|
||||
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
|
||||
// But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android
|
||||
// shell ("com.android.shell").
|
||||
// If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the
|
||||
// foreground.
|
||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
|
||||
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
|
||||
}
|
||||
|
||||
private static void stopWorkaroundAndroid11() {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
|
||||
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
|
||||
}
|
||||
|
||||
private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureForegroundException {
|
||||
while (attempts-- > 0) {
|
||||
// Wait for activity to start
|
||||
SystemClock.sleep(delayMs);
|
||||
try {
|
||||
startRecording();
|
||||
return; // it worked
|
||||
} catch (UnsupportedOperationException e) {
|
||||
if (attempts == 0) {
|
||||
Ln.e("Failed to start audio capture");
|
||||
Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " +
|
||||
"scrcpy.");
|
||||
throw new AudioCaptureForegroundException();
|
||||
} else {
|
||||
Ln.d("Failed to start audio capture, retrying...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startRecording() {
|
||||
recorder = createAudioRecord();
|
||||
recorder.startRecording();
|
||||
}
|
||||
|
||||
public void start() throws AudioCaptureForegroundException {
|
||||
startWorkaroundAndroid11();
|
||||
try {
|
||||
recorder = createAudioRecord();
|
||||
recorder.startRecording();
|
||||
} catch (UnsupportedOperationException e) {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||
Ln.e("Failed to start audio capture");
|
||||
Ln.e("On Android 11, it is only possible to capture in foreground, make sure that the device is unlocked when starting scrcpy.");
|
||||
throw new AudioCaptureForegroundException();
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||
startWorkaroundAndroid11();
|
||||
try {
|
||||
tryStartRecording(3, 100);
|
||||
} finally {
|
||||
stopWorkaroundAndroid11();
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
stopWorkaroundAndroid11();
|
||||
} else {
|
||||
startRecording();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -271,13 +271,22 @@ public final class AudioEncoder implements AsyncProcessor {
|
||||
try {
|
||||
return MediaCodec.createByCodecName(encoderName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
|
||||
Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
|
||||
throw new ConfigurationException("Unknown encoder: " + encoderName);
|
||||
} catch (IOException e) {
|
||||
Ln.e("Could not create audio encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
||||
Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'");
|
||||
return mediaCodec;
|
||||
|
||||
try {
|
||||
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
||||
Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'");
|
||||
return mediaCodec;
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Ln.e("Could not create default audio encoder for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private class EncoderCallback extends MediaCodec.Callback {
|
||||
|
@ -288,10 +288,7 @@ public final class Device {
|
||||
boolean allOk = true;
|
||||
for (long physicalDisplayId : physicalDisplayIds) {
|
||||
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
|
||||
boolean ok = SurfaceControl.setDisplayPowerMode(binder, mode);
|
||||
if (!ok) {
|
||||
allOk = false;
|
||||
}
|
||||
allOk &= SurfaceControl.setDisplayPowerMode(binder, mode);
|
||||
}
|
||||
return allOk;
|
||||
}
|
||||
|
@ -38,4 +38,10 @@ public final class FakeContext extends ContextWrapper {
|
||||
builder.setPackageName(PACKAGE_NAME);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
// @Override to be added on SDK upgrade for Android 14
|
||||
@SuppressWarnings("unused")
|
||||
public int getDeviceId() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
@ -202,13 +202,22 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
try {
|
||||
return MediaCodec.createByCodecName(encoderName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
|
||||
Ln.e("Video encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
|
||||
throw new ConfigurationException("Unknown encoder: " + encoderName);
|
||||
} catch (IOException e) {
|
||||
Ln.e("Could not create video encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
||||
Ln.d("Using encoder: '" + mediaCodec.getName() + "'");
|
||||
return mediaCodec;
|
||||
|
||||
try {
|
||||
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
||||
Ln.d("Using video encoder: '" + mediaCodec.getName() + "'");
|
||||
return mediaCodec;
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Ln.e("Could not create default video encoder for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||
|
@ -81,15 +81,15 @@ public final class Server {
|
||||
// But only apply when strictly necessary, since workarounds can cause other issues:
|
||||
// - <https://github.com/Genymobile/scrcpy/issues/940>
|
||||
// - <https://github.com/Genymobile/scrcpy/issues/994>
|
||||
boolean mustFillAppInfo = Build.BRAND.equalsIgnoreCase("meizu");
|
||||
if (Build.BRAND.equalsIgnoreCase("meizu")) {
|
||||
Workarounds.fillAppInfo();
|
||||
}
|
||||
|
||||
// Before Android 11, audio is not supported.
|
||||
// Since Android 12, we can properly set a context on the AudioRecord.
|
||||
// Only on Android 11 we must fill app info for the AudioRecord to work.
|
||||
mustFillAppInfo |= audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R;
|
||||
|
||||
if (mustFillAppInfo) {
|
||||
Workarounds.fillAppInfo();
|
||||
// Only on Android 11 we must fill the application context for the AudioRecord to work.
|
||||
if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||
Workarounds.fillAppContext();
|
||||
}
|
||||
|
||||
List<AsyncProcessor> asyncProcessors = new ArrayList<>();
|
||||
|
@ -10,6 +10,10 @@ import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public final class Workarounds {
|
||||
|
||||
private static Class<?> activityThreadClass;
|
||||
private static Object activityThread;
|
||||
|
||||
private Workarounds() {
|
||||
// not instantiable
|
||||
}
|
||||
@ -28,18 +32,25 @@ public final class Workarounds {
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||
public static void fillAppInfo() {
|
||||
try {
|
||||
private static void fillActivityThread() throws Exception {
|
||||
if (activityThread == null) {
|
||||
// ActivityThread activityThread = new ActivityThread();
|
||||
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
|
||||
activityThreadClass = Class.forName("android.app.ActivityThread");
|
||||
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
|
||||
activityThreadConstructor.setAccessible(true);
|
||||
Object activityThread = activityThreadConstructor.newInstance();
|
||||
activityThread = activityThreadConstructor.newInstance();
|
||||
|
||||
// ActivityThread.sCurrentActivityThread = activityThread;
|
||||
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
|
||||
sCurrentActivityThreadField.setAccessible(true);
|
||||
sCurrentActivityThreadField.set(null, activityThread);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||
public static void fillAppInfo() {
|
||||
try {
|
||||
fillActivityThread();
|
||||
|
||||
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
|
||||
Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
|
||||
@ -59,6 +70,16 @@ public final class Workarounds {
|
||||
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
|
||||
mBoundApplicationField.setAccessible(true);
|
||||
mBoundApplicationField.set(activityThread, appBindData);
|
||||
} catch (Throwable throwable) {
|
||||
// this is a workaround, so failing is not an error
|
||||
Ln.d("Could not fill app info: " + throwable.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||
public static void fillAppContext() {
|
||||
try {
|
||||
fillActivityThread();
|
||||
|
||||
Application app = Application.class.newInstance();
|
||||
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
|
||||
@ -71,7 +92,7 @@ public final class Workarounds {
|
||||
mInitialApplicationField.set(activityThread, app);
|
||||
} catch (Throwable throwable) {
|
||||
// this is a workaround, so failing is not an error
|
||||
Ln.d("Could not fill app info: " + throwable.getMessage());
|
||||
Ln.d("Could not fill app context: " + throwable.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,9 @@ public class ClipboardManager {
|
||||
private Method getPrimaryClipMethod;
|
||||
private Method setPrimaryClipMethod;
|
||||
private Method addPrimaryClipChangedListener;
|
||||
private boolean alternativeGetMethod;
|
||||
private boolean alternativeSetMethod;
|
||||
private boolean alternativeAddListenerMethod;
|
||||
private int getMethodVersion;
|
||||
private int setMethodVersion;
|
||||
private int addListenerMethodVersion;
|
||||
|
||||
public ClipboardManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
@ -31,9 +31,15 @@ public class ClipboardManager {
|
||||
} else {
|
||||
try {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
|
||||
alternativeGetMethod = true;
|
||||
getMethodVersion = 0;
|
||||
} catch (NoSuchMethodException e1) {
|
||||
try {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
|
||||
getMethodVersion = 1;
|
||||
} catch (NoSuchMethodException e2) {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
|
||||
getMethodVersion = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -47,41 +53,62 @@ public class ClipboardManager {
|
||||
} else {
|
||||
try {
|
||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
|
||||
alternativeSetMethod = true;
|
||||
setMethodVersion = 0;
|
||||
} catch (NoSuchMethodException e1) {
|
||||
try {
|
||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
|
||||
setMethodVersion = 1;
|
||||
} catch (NoSuchMethodException e2) {
|
||||
setPrimaryClipMethod = manager.getClass()
|
||||
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class);
|
||||
setMethodVersion = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return setPrimaryClipMethod;
|
||||
}
|
||||
|
||||
private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager)
|
||||
private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager)
|
||||
throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
|
||||
}
|
||||
if (alternativeMethod) {
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
|
||||
switch (methodVersion) {
|
||||
case 0:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
case 1:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
default:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
||||
}
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
}
|
||||
|
||||
private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData)
|
||||
private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData)
|
||||
throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
|
||||
} else if (alternativeMethod) {
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
} else {
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (methodVersion) {
|
||||
case 0:
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
break;
|
||||
case 1:
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
break;
|
||||
default:
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public CharSequence getText() {
|
||||
try {
|
||||
Method method = getGetPrimaryClipMethod();
|
||||
ClipData clipData = getPrimaryClip(method, alternativeGetMethod, manager);
|
||||
ClipData clipData = getPrimaryClip(method, getMethodVersion, manager);
|
||||
if (clipData == null || clipData.getItemCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
@ -96,7 +123,7 @@ public class ClipboardManager {
|
||||
try {
|
||||
Method method = getSetPrimaryClipMethod();
|
||||
ClipData clipData = ClipData.newPlainText(null, text);
|
||||
setPrimaryClip(method, alternativeSetMethod, manager, clipData);
|
||||
setPrimaryClip(method, setMethodVersion, manager, clipData);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
@ -104,14 +131,23 @@ public class ClipboardManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager,
|
||||
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager,
|
||||
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
|
||||
} else if (alternativeMethod) {
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
} else {
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (methodVersion) {
|
||||
case 0:
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
break;
|
||||
case 1:
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
break;
|
||||
default:
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,10 +160,19 @@ public class ClipboardManager {
|
||||
try {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, int.class);
|
||||
alternativeAddListenerMethod = true;
|
||||
addListenerMethodVersion = 0;
|
||||
} catch (NoSuchMethodException e1) {
|
||||
try {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
|
||||
int.class);
|
||||
addListenerMethodVersion = 1;
|
||||
} catch (NoSuchMethodException e2) {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
|
||||
int.class, int.class);
|
||||
addListenerMethodVersion = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -137,7 +182,7 @@ public class ClipboardManager {
|
||||
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
|
||||
try {
|
||||
Method method = getAddPrimaryClipChangedListener();
|
||||
addPrimaryClipChangedListener(method, alternativeAddListenerMethod, manager, listener);
|
||||
addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
|
Reference in New Issue
Block a user