Compare commits

..

1 Commits

Author SHA1 Message Date
Romain Vimont
3582592d2c Split workarounds to fix audio on some devices
There were several workarounds applied in a single method. Some of them
are specific to Meizu phones, but cause issues on other devices.

Split the method to be able to only fill the app context for audio
capture without applying the Meizu workarounds.

Fixes #3801 <https://github.com/Genymobile/scrcpy/issues/3801>
2023-03-14 22:54:27 +01:00
23 changed files with 310 additions and 271 deletions

View File

@@ -3,11 +3,9 @@ _scrcpy() {
local opts=" local opts="
--always-on-top --always-on-top
--audio-bit-rate= --audio-bit-rate=
--audio-buffer=
--audio-codec= --audio-codec=
--audio-codec-options= --audio-codec-options=
--audio-encoder= --audio-encoder=
--audio-output-buffer=
-b --video-bit-rate= -b --video-bit-rate=
--crop= --crop=
-d --select-usb -d --select-usb
@@ -117,26 +115,20 @@ _scrcpy() {
COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur})) COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur}))
return return
;; ;;
--audio-bit-rate \ -b|--video-bit-rate \
|--audio-buffer \ |--codec-options \
|-b|--video-bit-rate \
|--audio-codec-options \
|--audio-encoder \
|--audio-output-buffer \
|--crop \ |--crop \
|--display \ |--display \
|--display-buffer \ |--display-buffer \
|--encoder \
|--max-fps \ |--max-fps \
|-m|--max-size \ |-m|--max-size \
|-p|--port \ |-p|--port \
|--push-target \ |--push-target \
|--rotation \
|--tunnel-host \ |--tunnel-host \
|--tunnel-port \ |--tunnel-port \
|--v4l2-buffer \ |--v4l2-buffer \
|--v4l2-sink \ |--v4l2-sink \
|--video-codec-options \
|--video-encoder \
|--tcpip \ |--tcpip \
|--window-*) |--window-*)
# Option accepting an argument, but nothing to auto-complete # Option accepting an argument, but nothing to auto-complete

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/bash --norc --noprofile -i -c "\"\\$SHELL\" -i -c scrcpy || read -p 'Press Enter to quit...'" Exec=/bin/bash --norc --noprofile -i -c '"$SHELL" -i -c scrcpy || read -p "Press any key to quit..."'
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

@@ -10,11 +10,9 @@ local arguments
arguments=( arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--always-on-top[Make scrcpy window always on top \(above other windows\)]'
'--audio-bit-rate=[Encode the audio at the given bit-rate]' '--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=[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-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-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]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
{-d,--select-usb}'[Use USB device]' {-d,--select-usb}'[Use USB device]'

View File

@@ -277,6 +277,10 @@ if get_option('buildtype') == 'debug'
'src/util/strbuf.c', 'src/util/strbuf.c',
'src/util/term.c', 'src/util/term.c',
]], ]],
['test_clock', [
'tests/test_clock.c',
'src/clock.c',
]],
['test_control_msg_serialize', [ ['test_control_msg_serialize', [
'tests/test_control_msg_serialize.c', 'tests/test_control_msg_serialize.c',
'src/control_msg.c', 'src/control_msg.c',

View File

@@ -33,14 +33,6 @@ Lower values decrease the latency, but increase the likelyhood of buffer underru
Default is 50. 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 .TP
.BI "\-\-audio\-codec " name .BI "\-\-audio\-codec " name
Select an audio codec (opus, aac or raw). Select an audio codec (opus, aac or raw).

View File

@@ -59,6 +59,8 @@
#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT #define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT
#define SC_SDL_SAMPLE_FMT AUDIO_F32 #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_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ap->buf, (SAMPLES))
#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ap->buf, (BYTES)) #define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ap->buf, (BYTES))
@@ -228,8 +230,8 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
if (played) { if (played) {
uint32_t max_buffered_samples = ap->target_buffering uint32_t max_buffered_samples = ap->target_buffering
+ 12 * ap->output_buffer + 12 * SC_AUDIO_OUTPUT_BUFFER_MS * ap->sample_rate / 1000
+ ap->target_buffering / 10; + ap->target_buffering / 10;
if (buffered_samples > max_buffered_samples) { if (buffered_samples > max_buffered_samples) {
uint32_t skip_samples = buffered_samples - max_buffered_samples; uint32_t skip_samples = buffered_samples - max_buffered_samples;
sc_audiobuf_skip(&ap->buf, skip_samples); sc_audiobuf_skip(&ap->buf, skip_samples);
@@ -244,7 +246,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
// 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 uint32_t max_initial_buffering = ap->target_buffering
+ 2 * ap->output_buffer; + 2 * SC_AUDIO_OUTPUT_BUFFER_MS * ap->sample_rate / 1000;
if (buffered_samples > max_initial_buffering) { if (buffered_samples > max_initial_buffering) {
uint32_t skip_samples = buffered_samples - max_initial_buffering; uint32_t skip_samples = buffered_samples - max_initial_buffering;
sc_audiobuf_skip(&ap->buf, skip_samples); sc_audiobuf_skip(&ap->buf, skip_samples);
@@ -331,28 +333,11 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
unsigned nb_channels = tmp; unsigned nb_channels = tmp;
#endif #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 = { SDL_AudioSpec desired = {
.freq = ctx->sample_rate, .freq = ctx->sample_rate,
.format = SC_SDL_SAMPLE_FMT, .format = SC_SDL_SAMPLE_FMT,
.channels = nb_channels, .channels = nb_channels,
.samples = aout_samples, .samples = SC_AUDIO_OUTPUT_BUFFER_MS * ctx->sample_rate / 1000,
.callback = sc_audio_player_sdl_callback, .callback = sc_audio_player_sdl_callback,
.userdata = ap, .userdata = ap,
}; };
@@ -371,6 +356,11 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
} }
ap->swr_ctx = swr_ctx; 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 #ifdef SCRCPY_LAVU_HAS_CHLAYOUT
av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0); av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0);
av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0); av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0);
@@ -393,6 +383,13 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
goto error_free_swr_ctx; 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 // 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 // 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 // the producer and the consumer will be able to access it in parallel
@@ -461,10 +458,8 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) {
} }
void 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->target_buffering_delay = target_buffering;
ap->output_buffer_duration = output_buffer_duration;
static const struct sc_frame_sink_ops ops = { static const struct sc_frame_sink_ops ops = {
.open = sc_audio_player_frame_sink_open, .open = sc_audio_player_frame_sink_open,

View File

@@ -27,10 +27,6 @@ struct sc_audio_player {
sc_tick target_buffering_delay; sc_tick target_buffering_delay;
uint32_t target_buffering; // in samples 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 // Audio buffer to communicate between the receiver and the SDL audio
// callback (protected by SDL_AudioDeviceLock()) // callback (protected by SDL_AudioDeviceLock())
struct sc_audiobuf buf; struct sc_audiobuf buf;
@@ -84,7 +80,6 @@ struct sc_audio_player_callbacks {
}; };
void 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 #endif

View File

@@ -71,7 +71,6 @@ enum {
OPT_LIST_DISPLAYS, OPT_LIST_DISPLAYS,
OPT_REQUIRE_AUDIO, OPT_REQUIRE_AUDIO,
OPT_AUDIO_BUFFER, OPT_AUDIO_BUFFER,
OPT_AUDIO_OUTPUT_BUFFER,
}; };
struct sc_option { struct sc_option {
@@ -130,16 +129,6 @@ static const struct sc_option options[] = {
"likelyhood of buffer underrun (causing audio glitches).\n" "likelyhood of buffer underrun (causing audio glitches).\n"
"Default is 50.", "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_id = OPT_AUDIO_CODEC,
.longopt = "audio-codec", .longopt = "audio-codec",
@@ -1215,19 +1204,6 @@ parse_buffering_time(const char *s, sc_tick *tick) {
return true; 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 static bool
parse_lock_video_orientation(const char *s, parse_lock_video_orientation(const char *s,
enum sc_lock_video_orientation *lock_mode) { enum sc_lock_video_orientation *lock_mode) {
@@ -1855,12 +1831,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false; return false;
} }
break; break;
case OPT_AUDIO_OUTPUT_BUFFER:
if (!parse_audio_output_buffer(optarg,
&opts->audio_output_buffer)) {
return false;
}
break;
default: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
return false; return false;

View File

@@ -1,34 +1,116 @@
#include "clock.h" #include "clock.h"
#include <assert.h>
#include "util/log.h" #include "util/log.h"
#define SC_CLOCK_NDEBUG // comment to debug #define SC_CLOCK_NDEBUG // comment to debug
void void
sc_clock_init(struct sc_clock *clock) { sc_clock_init(struct sc_clock *clock) {
clock->range = 0; clock->count = 0;
clock->offset = 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;
} }
void void
sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
if (clock->range < SC_CLOCK_RANGE) { struct sc_clock_point *point = &clock->points[clock->head];
++clock->range;
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;
} }
sc_tick offset = system - stream; if (clock->count == SC_CLOCK_RANGE) {
clock->offset = ((clock->range - 1) * clock->offset + offset) // The current point overwrites the previous value in the circular
/ clock->range; // 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);
#ifndef SC_CLOCK_NDEBUG #ifndef SC_CLOCK_NDEBUG
LOGD("Clock estimation: pts + %" PRItick, clock->offset); LOGD("Clock estimation: %f * pts + %" PRItick, clock->slope, clock->offset);
#endif #endif
} }
sc_tick sc_tick
sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) { sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) {
assert(clock->range); // sc_clock_update() must have been called assert(clock->count); // sc_clock_update() must have been called
return stream + clock->offset; return (sc_tick) (stream * clock->slope) + clock->offset;
} }

View File

@@ -3,9 +3,12 @@
#include "common.h" #include "common.h"
#include <assert.h>
#include "util/tick.h" #include "util/tick.h"
#define SC_CLOCK_RANGE 32 #define SC_CLOCK_RANGE 32
static_assert(!(SC_CLOCK_RANGE & 1), "SC_CLOCK_RANGE must be even");
struct sc_clock_point { struct sc_clock_point {
sc_tick system; sc_tick system;
@@ -18,18 +21,40 @@ struct sc_clock_point {
* *
* f(stream) = slope * stream + offset * f(stream) = slope * stream + offset
* *
* Theoretically, the slope encodes the drift between the device clock and the * To that end, it stores the SC_CLOCK_RANGE last clock points (the timestamps
* computer clock. It is expected to be very close to 1. * of a frame expressed both in stream time and system time) in a circular
* array.
* *
* Since the clock is used to estimate very close points in the future (which * To estimate the slope, it splits the last SC_CLOCK_RANGE points into two
* are reestimated on every clock update, see delay_buffer), the error caused * sets of SC_CLOCK_RANGE/2 points, and computes their centroid ("average
* by clock drift is totally negligible, so it is better to assume that the * point"). The slope of the estimated affine function is that of the line
* slope is 1 than to estimate it (the estimation error would be larger). * passing through these two points.
* *
* Therefore, only the offset is estimated. * 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.
*/ */
struct sc_clock { struct sc_clock {
unsigned range; // 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;
sc_tick offset; sc_tick offset;
}; };

View File

@@ -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_clock_update(&db->clock, sc_tick_now(), pts);
sc_cond_signal(&db->wait_cond); sc_cond_signal(&db->wait_cond);
if (db->first_frame_asap && db->clock.range == 1) { if (db->first_frame_asap && db->clock.count == 1) {
sc_mutex_unlock(&db->mutex); sc_mutex_unlock(&db->mutex);
return sc_frame_source_sinks_push(&db->frame_source, frame); return sc_frame_source_sinks_push(&db->frame_source, frame);
} }

View File

@@ -44,7 +44,6 @@ const struct scrcpy_options scrcpy_options_default = {
.display_buffer = 0, .display_buffer = 0,
.v4l2_buffer = 0, .v4l2_buffer = 0,
.audio_buffer = SC_TICK_FROM_MS(50), .audio_buffer = SC_TICK_FROM_MS(50),
.audio_output_buffer = SC_TICK_FROM_MS(5),
#ifdef HAVE_USB #ifdef HAVE_USB
.otg = false, .otg = false,
#endif #endif

View File

@@ -127,7 +127,6 @@ struct scrcpy_options {
sc_tick display_buffer; sc_tick display_buffer;
sc_tick v4l2_buffer; sc_tick v4l2_buffer;
sc_tick audio_buffer; sc_tick audio_buffer;
sc_tick audio_output_buffer;
#ifdef HAVE_USB #ifdef HAVE_USB
bool otg; bool otg;
#endif #endif

View File

@@ -688,8 +688,7 @@ aoa_hid_end:
sc_frame_source_add_sink(src, &s->screen.frame_sink); sc_frame_source_add_sink(src, &s->screen.frame_sink);
if (options->audio) { 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, sc_frame_source_add_sink(&s->audio_decoder.frame_source,
&s->audio_player.frame_sink); &s->audio_player.frame_sink);
} }

79
app/tests/test_clock.c Normal file
View File

@@ -0,0 +1,79 @@
#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;
};

View File

@@ -88,14 +88,3 @@ avoid glitches and smooth the playback:
``` ```
scrcpy --display-buffer=200 --audio-buffer=200 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

View File

@@ -59,58 +59,45 @@ public final class AudioCapture {
} }
private static void startWorkaroundAndroid11() { private static void startWorkaroundAndroid11() {
// Android 11 requires Apps to be at foreground to record audio. if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground. // Android 11 requires Apps to be at foreground to record audio.
// 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 // Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
// shell ("com.android.shell"). // 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
// If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the // shell ("com.android.shell").
// foreground. // If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the
Intent intent = new Intent(Intent.ACTION_MAIN); // foreground.
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
intent.addCategory(Intent.CATEGORY_LAUNCHER); Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent); intent.addCategory(Intent.CATEGORY_LAUNCHER);
} intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
private static void stopWorkaroundAndroid11() { // Wait for activity to start
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME); SystemClock.sleep(150);
}
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() { private static void stopWorkaroundAndroid11() {
recorder = createAudioRecord(); if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
recorder.startRecording(); ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
}
} }
public void start() throws AudioCaptureForegroundException { public void start() throws AudioCaptureForegroundException {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { startWorkaroundAndroid11();
startWorkaroundAndroid11(); try {
try { recorder = createAudioRecord();
tryStartRecording(3, 100); recorder.startRecording();
} finally { } catch (UnsupportedOperationException e) {
stopWorkaroundAndroid11(); 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();
} }
} else { throw e;
startRecording(); } finally {
stopWorkaroundAndroid11();
} }
} }

View File

@@ -271,22 +271,13 @@ public final class AudioEncoder implements AsyncProcessor {
try { try {
return MediaCodec.createByCodecName(encoderName); return MediaCodec.createByCodecName(encoderName);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage()); Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
throw new ConfigurationException("Unknown encoder: " + encoderName); 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());
try { Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'");
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType()); return mediaCodec;
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 { private class EncoderCallback extends MediaCodec.Callback {

View File

@@ -288,7 +288,10 @@ public final class Device {
boolean allOk = true; boolean allOk = true;
for (long physicalDisplayId : physicalDisplayIds) { for (long physicalDisplayId : physicalDisplayIds) {
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId); IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
allOk &= SurfaceControl.setDisplayPowerMode(binder, mode); boolean ok = SurfaceControl.setDisplayPowerMode(binder, mode);
if (!ok) {
allOk = false;
}
} }
return allOk; return allOk;
} }

View File

@@ -38,10 +38,4 @@ public final class FakeContext extends ContextWrapper {
builder.setPackageName(PACKAGE_NAME); builder.setPackageName(PACKAGE_NAME);
return builder.build(); return builder.build();
} }
// @Override to be added on SDK upgrade for Android 14
@SuppressWarnings("unused")
public int getDeviceId() {
return 0;
}
} }

View File

@@ -202,22 +202,13 @@ public class ScreenEncoder implements Device.RotationListener {
try { try {
return MediaCodec.createByCodecName(encoderName); return MediaCodec.createByCodecName(encoderName);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Ln.e("Video encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage()); Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
throw new ConfigurationException("Unknown encoder: " + encoderName); 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());
try { Ln.d("Using encoder: '" + mediaCodec.getName() + "'");
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType()); return mediaCodec;
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) { private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {

View File

@@ -16,9 +16,9 @@ public class ClipboardManager {
private Method getPrimaryClipMethod; private Method getPrimaryClipMethod;
private Method setPrimaryClipMethod; private Method setPrimaryClipMethod;
private Method addPrimaryClipChangedListener; private Method addPrimaryClipChangedListener;
private int getMethodVersion; private boolean alternativeGetMethod;
private int setMethodVersion; private boolean alternativeSetMethod;
private int addListenerMethodVersion; private boolean alternativeAddListenerMethod;
public ClipboardManager(IInterface manager) { public ClipboardManager(IInterface manager) {
this.manager = manager; this.manager = manager;
@@ -31,15 +31,9 @@ public class ClipboardManager {
} else { } else {
try { try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
getMethodVersion = 0; } catch (NoSuchMethodException e) {
} catch (NoSuchMethodException e1) { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
try { alternativeGetMethod = true;
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;
}
} }
} }
} }
@@ -53,62 +47,41 @@ public class ClipboardManager {
} else { } else {
try { try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
setMethodVersion = 0; } catch (NoSuchMethodException e) {
} catch (NoSuchMethodException e1) { setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
try { alternativeSetMethod = true;
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; return setPrimaryClipMethod;
} }
private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager)
throws InvocationTargetException, IllegalAccessException { throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME); return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
} }
if (alternativeMethod) {
switch (methodVersion) { return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
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, int methodVersion, IInterface manager, ClipData clipData) private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData)
throws InvocationTargetException, IllegalAccessException { throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME); method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
return; } else if (alternativeMethod) {
} method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
} else {
switch (methodVersion) { method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
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() { public CharSequence getText() {
try { try {
Method method = getGetPrimaryClipMethod(); Method method = getGetPrimaryClipMethod();
ClipData clipData = getPrimaryClip(method, getMethodVersion, manager); ClipData clipData = getPrimaryClip(method, alternativeGetMethod, manager);
if (clipData == null || clipData.getItemCount() == 0) { if (clipData == null || clipData.getItemCount() == 0) {
return null; return null;
} }
@@ -123,7 +96,7 @@ public class ClipboardManager {
try { try {
Method method = getSetPrimaryClipMethod(); Method method = getSetPrimaryClipMethod();
ClipData clipData = ClipData.newPlainText(null, text); ClipData clipData = ClipData.newPlainText(null, text);
setPrimaryClip(method, setMethodVersion, manager, clipData); setPrimaryClip(method, alternativeSetMethod, manager, clipData);
return true; return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
@@ -131,23 +104,14 @@ public class ClipboardManager {
} }
} }
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager,
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException { IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME); method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
return; } else if (alternativeMethod) {
} method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
} else {
switch (methodVersion) { method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
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;
} }
} }
@@ -160,19 +124,10 @@ public class ClipboardManager {
try { try {
addPrimaryClipChangedListener = manager.getClass() addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
addListenerMethodVersion = 0; } catch (NoSuchMethodException e) {
} catch (NoSuchMethodException e1) { addPrimaryClipChangedListener = manager.getClass()
try { .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, int.class);
addPrimaryClipChangedListener = manager.getClass() alternativeAddListenerMethod = true;
.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;
}
} }
} }
} }
@@ -182,7 +137,7 @@ public class ClipboardManager {
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
try { try {
Method method = getAddPrimaryClipChangedListener(); Method method = getAddPrimaryClipChangedListener();
addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener); addPrimaryClipChangedListener(method, alternativeAddListenerMethod, manager, listener);
return true; return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);