Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a373a47544 | ||
|
|
426d510ef0 | ||
|
|
f71419fd55 | ||
|
|
4e85c48592 | ||
|
|
ff82852f28 | ||
|
|
9f8f312c50 | ||
|
|
68d3a327a1 | ||
|
|
b08deb5224 | ||
|
|
2611f7f0ca | ||
|
|
81838f1678 | ||
|
|
5fff56ee6f | ||
|
|
7c0338c3e6 | ||
|
|
cdd0cdbceb | ||
|
|
aaea8e106f | ||
|
|
cc8023fcf8 | ||
|
|
a8bfb1c3ef | ||
|
|
0f957645cc | ||
|
|
55158932d2 | ||
|
|
a5ebd16d5c | ||
|
|
2dae48bc13 | ||
|
|
c39e6dda3e | ||
|
|
974230fa95 | ||
|
|
4132389686 | ||
|
|
34dae7f836 | ||
|
|
828b6e786f | ||
|
|
34b778d423 | ||
|
|
18b60c9641 | ||
|
|
2b95b5630f | ||
|
|
0e1b173213 | ||
|
|
a540301e3c | ||
|
|
29946bccc0 | ||
|
|
c20a53a17d | ||
|
|
d2d69ddfcb | ||
|
|
5a41b710f1 | ||
|
|
d8a48e871c | ||
|
|
2615143711 | ||
|
|
931bc84548 | ||
|
|
31c763fc62 | ||
|
|
20eeab921a | ||
|
|
93de9739c7 | ||
|
|
ce924d8f05 | ||
|
|
47f67412e4 | ||
|
|
51016950cb | ||
|
|
7203e58ebf | ||
|
|
25f213e4a6 | ||
|
|
cb25bed2f5 | ||
|
|
d4407f1289 | ||
|
|
1daf8e8611 | ||
|
|
4c24ea50ed | ||
|
|
eb39365737 | ||
|
|
46169618a5 | ||
|
|
1a3c61a77b | ||
|
|
fe9166a88c | ||
|
|
e4eae49eed | ||
|
|
0b24cb930c | ||
|
|
8edb1c8eef | ||
|
|
775789e50b | ||
|
|
b3641dc025 | ||
|
|
5c24f32651 | ||
|
|
7b0977c7d9 | ||
|
|
b992191572 | ||
|
|
a6de3e67b2 | ||
|
|
94a14e7fd0 | ||
|
|
bf3b1c70df | ||
|
|
4950d5c846 | ||
|
|
3f98478ad7 | ||
|
|
4e7bc84a4e | ||
|
|
ef2bb6787d | ||
|
|
7daebaadae | ||
|
|
2bc9b687db | ||
|
|
0a1451b301 | ||
|
|
d499e96d0e | ||
|
|
ef22893b7c | ||
|
|
fd655b0b79 | ||
|
|
ddef08a0f7 | ||
|
|
5be8cb79c4 | ||
|
|
0bb7f08d95 | ||
|
|
aff7b7f26a | ||
|
|
160de07e8a | ||
|
|
805d85dc3b | ||
|
|
29c04995b4 | ||
|
|
03a553f590 | ||
|
|
e2aa3cca52 | ||
|
|
de1dfae169 | ||
|
|
b6a8b6f91a | ||
|
|
2346148827 | ||
|
|
27918a501c | ||
|
|
631801ce3e | ||
|
|
884d0ec246 | ||
|
|
db0c5bc066 | ||
|
|
681fde542d | ||
|
|
fb12118190 | ||
|
|
a1d84709ca | ||
|
|
77277238b3 | ||
|
|
b9ee7c5852 | ||
|
|
ec6641b519 | ||
|
|
8fb9c28c59 | ||
|
|
836f0042af | ||
|
|
245cf146c5 | ||
|
|
adaf02957e | ||
|
|
2238a64c2f | ||
|
|
6a19814779 | ||
|
|
a2ff5750e7 | ||
|
|
f87ae8e0e7 | ||
|
|
528841f294 | ||
|
|
2517c9c31e | ||
|
|
0704441b16 | ||
|
|
f98dc5525d | ||
|
|
778c7c0a88 | ||
|
|
932e82e89b | ||
|
|
4d719da424 | ||
|
|
7d571e502a | ||
|
|
982c292794 | ||
|
|
f5447f4391 | ||
|
|
123a4d9575 | ||
|
|
c91e56ac1f | ||
|
|
d391de17c9 | ||
|
|
06ba2e9402 | ||
|
|
428cbd13ec |
@@ -78,7 +78,7 @@ _scrcpy() {
|
|||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
--audio-codec)
|
--audio-codec)
|
||||||
COMPREPLY=($(compgen -W 'raw opus aac' -- "$cur"))
|
COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur"))
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
--lock-video-orientation)
|
--lock-video-orientation)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ 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-codec=[Select the audio codec]:codec:(raw opus aac)'
|
'--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]'
|
||||||
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
|
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
|
||||||
|
|||||||
10
app/scrcpy.1
10
app/scrcpy.1
@@ -27,13 +27,15 @@ Default is 196K (196000).
|
|||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-audio\-buffer ms
|
.BI "\-\-audio\-buffer ms
|
||||||
Add a buffering delay (in milliseconds) before playing audio. This increases latency to compensate for jitter.
|
Configure the audio buffering delay (in milliseconds).
|
||||||
|
|
||||||
Default is 0 (no buffering).
|
Lower values decrease the latency, but increase the likelyhood of buffer underruns (causing audio glitches).
|
||||||
|
|
||||||
|
Default is 50.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-audio\-codec " name
|
.BI "\-\-audio\-codec " name
|
||||||
Select an audio codec (raw, opus or aac).
|
Select an audio codec (opus, aac or raw).
|
||||||
|
|
||||||
Default is opus.
|
Default is opus.
|
||||||
|
|
||||||
@@ -280,7 +282,7 @@ Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "me
|
|||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-\-require\-audio
|
.B \-\-require\-audio
|
||||||
By default, scrcpy mirrors only the video if audio capture fails on the device. This flag makes scrcpy fail if audio is enabled but does not work.
|
By default, scrcpy mirrors only the video if audio capture fails on the device. This option makes scrcpy fail if audio is enabled but does not work.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-rotation " value
|
.BI "\-\-rotation " value
|
||||||
|
|||||||
@@ -4,7 +4,54 @@
|
|||||||
|
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
//#define SC_AUDIO_PLAYER_NDEBUG // comment to debug
|
#define SC_AUDIO_PLAYER_NDEBUG // comment to debug
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Real-time audio player with configurable latency
|
||||||
|
*
|
||||||
|
* As input, the player regularly receives AVFrames of decoded audio samples.
|
||||||
|
* As output, an SDL callback regularly requests audio samples to be played.
|
||||||
|
* In the middle, an audio buffer stores the samples produced but not consumed
|
||||||
|
* yet.
|
||||||
|
*
|
||||||
|
* The goal of the player is to feed the audio output with a latency as low as
|
||||||
|
* possible while avoiding buffer underrun (i.e. not being able to provide
|
||||||
|
* samples when requested).
|
||||||
|
*
|
||||||
|
* For that purpose, it attempts to keep the average buffering (the number of
|
||||||
|
* samples present in the buffer) around a target value. If this target
|
||||||
|
* buffering is too low, then buffer underrun will occur often. If it is too
|
||||||
|
* high, then latency will be increased (this can be used on purpose to delay
|
||||||
|
* the audio playback). This value can be configured (in milliseconds) via the
|
||||||
|
* scrcpy option --audio-buffer.
|
||||||
|
*
|
||||||
|
* The player could not adjust the sample input rate (it receives samples
|
||||||
|
* produced in real-time) nor the sample output rate (it must provide samples
|
||||||
|
* as requested by the audio output callback). Therefore, it may only apply
|
||||||
|
* compensation by resampling (convert m input samples to n output samples).
|
||||||
|
*
|
||||||
|
* The compensation itself is applied by swresample (FFmpeg). It is configured
|
||||||
|
* by swr_set_compensation(). An important work for the player is thus to
|
||||||
|
* estimate regularly the compensation value to configure.
|
||||||
|
*
|
||||||
|
* Basically, the player wants to estimate the current buffering level: it is
|
||||||
|
* the result of an average of the "natural" buffering (samples are produced
|
||||||
|
* and consumed by blocks, so it must be smoothed), and by instant adjustments
|
||||||
|
* resulting of its own actions (explicit compensation and silence insertion on
|
||||||
|
* underflow), which are not smoothed.
|
||||||
|
*
|
||||||
|
* Buffer underflow events may occur when packets arrive too late. In that
|
||||||
|
* case, the player is inserting silence. Once the packets finally arrive
|
||||||
|
* (late), one strategy could be to drop the samples that were replaced by
|
||||||
|
* silence, in order to keep a minimal latency. However, dropping samples in
|
||||||
|
* case of buffer underflow is not a good strategy: it would temporarily
|
||||||
|
* increase the underflow even more, and cause very audible audio glitches.
|
||||||
|
*
|
||||||
|
* Therefore, the player don't drop any sample on underflow, the stream is just
|
||||||
|
* delayed by the number of silent samples inserted. But in return, these
|
||||||
|
* additional samples will increase buffering (latency) when the late packets
|
||||||
|
* will arrive, so they will be taken into account by compensation.
|
||||||
|
*/
|
||||||
|
|
||||||
/** Downcast frame_sink to sc_audio_player */
|
/** Downcast frame_sink to sc_audio_player */
|
||||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink)
|
#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink)
|
||||||
@@ -12,29 +59,20 @@
|
|||||||
#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_SAMPLES 480 // 10ms at 48000Hz
|
#define SC_AUDIO_OUTPUT_BUFFER_SAMPLES 240 // 5ms at 48000Hz
|
||||||
|
|
||||||
// The target number of buffered samples between the producer and the consumer.
|
static inline uint32_t
|
||||||
// This value is directly use for compensation.
|
|
||||||
#define SC_TARGET_BUFFERED_SAMPLES (3 * SC_AUDIO_OUTPUT_BUFFER_SAMPLES)
|
|
||||||
|
|
||||||
// Use a ring-buffer of 1 second (at 48000Hz) between the producer and the
|
|
||||||
// consumer. It too big, but it guarantees that the producer and the consumer
|
|
||||||
// will be able to access it in parallel without locking.
|
|
||||||
#define SC_BYTEBUF_SIZE_IN_SAMPLES 48000
|
|
||||||
|
|
||||||
static inline size_t
|
|
||||||
bytes_to_samples(struct sc_audio_player *ap, size_t bytes) {
|
bytes_to_samples(struct sc_audio_player *ap, size_t bytes) {
|
||||||
assert(bytes % (ap->nb_channels * ap->out_bytes_per_sample) == 0);
|
assert(bytes % (ap->nb_channels * ap->out_bytes_per_sample) == 0);
|
||||||
return bytes / (ap->nb_channels * ap->out_bytes_per_sample);
|
return bytes / (ap->nb_channels * ap->out_bytes_per_sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline size_t
|
static inline size_t
|
||||||
samples_to_bytes(struct sc_audio_player *ap, size_t samples) {
|
samples_to_bytes(struct sc_audio_player *ap, uint32_t samples) {
|
||||||
return samples * ap->nb_channels * ap->out_bytes_per_sample;
|
return samples * ap->nb_channels * ap->out_bytes_per_sample;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
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;
|
||||||
|
|
||||||
@@ -45,30 +83,55 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
|
|||||||
size_t len = len_int;
|
size_t len = len_int;
|
||||||
|
|
||||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
LOGD("[Audio] SDL callback requests %" SC_PRIsizet " samples",
|
LOGD("[Audio] SDL callback requests %" PRIu32 " samples",
|
||||||
bytes_to_samples(ap, len));
|
bytes_to_samples(ap, len));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
size_t read_avail = sc_bytebuf_read_available(&ap->buf);
|
size_t read_avail = sc_bytebuf_read_available(&ap->buf);
|
||||||
|
if (!ap->played) {
|
||||||
|
uint32_t buffered_samples = bytes_to_samples(ap, read_avail);
|
||||||
|
|
||||||
|
// Part of the buffering is handled by inserting initial silence. The
|
||||||
|
// remaining (margin) last samples will be handled by compensation.
|
||||||
|
uint32_t margin = 30 * ap->sample_rate / 1000; // 30ms
|
||||||
|
if (buffered_samples + margin < ap->target_buffering) {
|
||||||
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
|
LOGD("[Audio] Inserting initial buffering silence: %" PRIu32
|
||||||
|
" samples", bytes_to_samples(ap, len));
|
||||||
|
#endif
|
||||||
|
// Delay playback starting to reach the target buffering. Fill the
|
||||||
|
// whole buffer with silence (len is small compared to the
|
||||||
|
// arbitrary margin value).
|
||||||
|
memset(stream, 0, len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
size_t read = MIN(read_avail, len);
|
size_t read = MIN(read_avail, len);
|
||||||
if (read) {
|
if (read) {
|
||||||
sc_bytebuf_read(&ap->buf, stream, read);
|
sc_bytebuf_read(&ap->buf, stream, read);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (read < len) {
|
if (read < len) {
|
||||||
// Insert silence
|
size_t silence_bytes = len - read;
|
||||||
|
uint32_t silence_samples = bytes_to_samples(ap, silence_bytes);
|
||||||
|
// Insert silence. In theory, the inserted silent replaces the missing
|
||||||
|
// samples, which will arrive later, so they should be dropped to keep
|
||||||
|
// the latency minimal. However, this would cause very audible
|
||||||
|
// glitches, so let the clock compensation restore the target latency.
|
||||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
LOGD("[Audio] Buffer underflow, inserting silence: %" SC_PRIsizet
|
LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples",
|
||||||
" samples", bytes_to_samples(ap, len - read));
|
silence_samples);
|
||||||
#endif
|
#endif
|
||||||
memset(stream + read, 0, len - read);
|
memset(stream + read, 0, silence_bytes);
|
||||||
// If the first frame has not been received yet, it's not an underflow
|
|
||||||
if (ap->received) {
|
if (ap->received) {
|
||||||
ap->underflow += bytes_to_samples(ap, len - read);
|
// Inserting additional samples immediately increases buffering
|
||||||
|
ap->avg_buffering.avg += silence_samples;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ap->last_consumed = sc_tick_now();
|
ap->played = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint8_t *
|
static uint8_t *
|
||||||
@@ -89,6 +152,163 @@ sc_audio_player_get_swr_buf(struct sc_audio_player *ap, size_t min_samples) {
|
|||||||
return ap->swr_buf;
|
return ap->swr_buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
||||||
|
const AVFrame *frame) {
|
||||||
|
struct sc_audio_player *ap = DOWNCAST(sink);
|
||||||
|
|
||||||
|
SwrContext *swr_ctx = ap->swr_ctx;
|
||||||
|
|
||||||
|
int64_t swr_delay = swr_get_delay(swr_ctx, ap->sample_rate);
|
||||||
|
// No need to av_rescale_rnd(), input and output sample rates are the same
|
||||||
|
// Add more space (256) for clock compensation
|
||||||
|
int dst_nb_samples = swr_delay + frame->nb_samples + 256;
|
||||||
|
|
||||||
|
uint8_t *swr_buf = sc_audio_player_get_swr_buf(ap, dst_nb_samples);
|
||||||
|
if (!swr_buf) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples,
|
||||||
|
(const uint8_t **) frame->data, frame->nb_samples);
|
||||||
|
if (ret < 0) {
|
||||||
|
LOGE("Resampling failed: %d", ret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// swr_convert() returns the number of samples which would have been
|
||||||
|
// written if the buffer was big enough.
|
||||||
|
uint32_t samples_written = MIN(ret, dst_nb_samples);
|
||||||
|
size_t swr_buf_size = samples_to_bytes(ap, samples_written);
|
||||||
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
|
LOGD("[Audio] %" PRIu32 " samples written to buffer", samples_written);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Since this function is the only writer, the current available space is
|
||||||
|
// at least the previous available space. In practice, it should almost
|
||||||
|
// always be possible to write without lock.
|
||||||
|
bool lockless_write = swr_buf_size <= ap->previous_write_avail;
|
||||||
|
if (lockless_write) {
|
||||||
|
sc_bytebuf_prepare_write(&ap->buf, swr_buf, swr_buf_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LockAudioDevice(ap->device);
|
||||||
|
|
||||||
|
size_t read_avail = sc_bytebuf_read_available(&ap->buf);
|
||||||
|
uint32_t buffered_samples = bytes_to_samples(ap, read_avail);
|
||||||
|
|
||||||
|
if (lockless_write) {
|
||||||
|
sc_bytebuf_commit_write(&ap->buf, swr_buf_size);
|
||||||
|
} else {
|
||||||
|
// Take care to keep full samples
|
||||||
|
size_t align = ap->nb_channels * ap->out_bytes_per_sample;
|
||||||
|
size_t write_avail =
|
||||||
|
sc_bytebuf_write_available(&ap->buf) / align * align;
|
||||||
|
if (swr_buf_size > write_avail) {
|
||||||
|
// Skip old samples
|
||||||
|
size_t cap = sc_bytebuf_capacity(&ap->buf) / align * align;
|
||||||
|
if (swr_buf_size > cap) {
|
||||||
|
// Ignore the first bytes in swr_buf
|
||||||
|
swr_buf += swr_buf_size - cap;
|
||||||
|
swr_buf_size = cap;
|
||||||
|
}
|
||||||
|
assert(swr_buf_size > write_avail);
|
||||||
|
if (swr_buf_size - write_avail > 0) {
|
||||||
|
sc_bytebuf_skip(&ap->buf, swr_buf_size - write_avail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sc_bytebuf_write(&ap->buf, swr_buf, swr_buf_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffered_samples += samples_written;
|
||||||
|
assert(samples_to_bytes(ap, buffered_samples)
|
||||||
|
== sc_bytebuf_read_available(&ap->buf));
|
||||||
|
|
||||||
|
// Read with lock held, to be used after unlocking
|
||||||
|
bool played = ap->played;
|
||||||
|
if (played) {
|
||||||
|
uint32_t max_buffered_samples = ap->target_buffering
|
||||||
|
+ 12 * SC_AUDIO_OUTPUT_BUFFER_SAMPLES
|
||||||
|
+ ap->target_buffering / 10;
|
||||||
|
if (buffered_samples > max_buffered_samples) {
|
||||||
|
uint32_t skip_samples = buffered_samples - max_buffered_samples;
|
||||||
|
size_t skip_bytes = samples_to_bytes(ap, skip_samples);
|
||||||
|
sc_bytebuf_skip(&ap->buf, skip_bytes);
|
||||||
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
|
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
|
||||||
|
" samples", skip_samples);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number of samples added (or removed, if negative) for compensation
|
||||||
|
int32_t instant_compensation =
|
||||||
|
(int32_t) samples_written - frame->nb_samples;
|
||||||
|
|
||||||
|
// The compensation must apply instantly, it must not be smoothed
|
||||||
|
ap->avg_buffering.avg += instant_compensation;
|
||||||
|
|
||||||
|
// However, the buffering level must be smoothed
|
||||||
|
sc_average_push(&ap->avg_buffering, buffered_samples);
|
||||||
|
|
||||||
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
|
LOGD("[Audio] buffered_samples=%" PRIu32 " avg_buffering=%f",
|
||||||
|
buffered_samples, sc_average_get(&ap->avg_buffering));
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
// SDL playback not started yet, do not accumulate more than
|
||||||
|
// max_initial_buffering samples, this would cause unnecessary delay
|
||||||
|
// (and glitches to compensate) on start.
|
||||||
|
uint32_t max_initial_buffering = ap->target_buffering
|
||||||
|
+ 2 * SC_AUDIO_OUTPUT_BUFFER_SAMPLES;
|
||||||
|
if (buffered_samples > max_initial_buffering) {
|
||||||
|
uint32_t skip_samples = buffered_samples - max_initial_buffering;
|
||||||
|
size_t skip_bytes = samples_to_bytes(ap, skip_samples);
|
||||||
|
sc_bytebuf_skip(&ap->buf, skip_bytes);
|
||||||
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
|
LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples",
|
||||||
|
skip_samples);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ap->previous_write_avail = sc_bytebuf_write_available(&ap->buf);
|
||||||
|
ap->received = true;
|
||||||
|
|
||||||
|
SDL_UnlockAudioDevice(ap->device);
|
||||||
|
|
||||||
|
if (played) {
|
||||||
|
ap->samples_since_resync += samples_written;
|
||||||
|
if (ap->samples_since_resync >= ap->sample_rate) {
|
||||||
|
// Recompute compensation every second
|
||||||
|
ap->samples_since_resync = 0;
|
||||||
|
|
||||||
|
float avg = sc_average_get(&ap->avg_buffering);
|
||||||
|
int diff = ap->target_buffering - avg;
|
||||||
|
if (diff < 0 && buffered_samples < ap->target_buffering) {
|
||||||
|
// Do not accelerate if the instant buffering level is below
|
||||||
|
// the average, this would increase underflow
|
||||||
|
diff = 0;
|
||||||
|
}
|
||||||
|
// Compensate the diff over 4 seconds (but will be recomputed after
|
||||||
|
// 1 second)
|
||||||
|
int distance = 4 * ap->sample_rate;
|
||||||
|
// Limit compensation rate to 2%
|
||||||
|
int abs_max_diff = distance / 50;
|
||||||
|
diff = CLAMP(diff, -abs_max_diff, abs_max_diff);
|
||||||
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
|
LOGD("[Audio] Average buffering=%f, compensation %d", avg, diff);
|
||||||
|
#endif
|
||||||
|
int ret = swr_set_compensation(swr_ctx, diff, distance);
|
||||||
|
if (ret < 0) {
|
||||||
|
LOGW("Resampling compensation failed: %d", ret);
|
||||||
|
// not fatal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||||
const AVCodecContext *ctx) {
|
const AVCodecContext *ctx) {
|
||||||
@@ -127,7 +347,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
|||||||
|
|
||||||
assert(ctx->sample_rate > 0);
|
assert(ctx->sample_rate > 0);
|
||||||
assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT));
|
assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT));
|
||||||
|
|
||||||
int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT);
|
int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT);
|
||||||
assert(out_bytes_per_sample > 0);
|
assert(out_bytes_per_sample > 0);
|
||||||
|
|
||||||
@@ -157,7 +376,15 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
|||||||
ap->nb_channels = nb_channels;
|
ap->nb_channels = nb_channels;
|
||||||
ap->out_bytes_per_sample = out_bytes_per_sample;
|
ap->out_bytes_per_sample = out_bytes_per_sample;
|
||||||
|
|
||||||
size_t bytebuf_size = samples_to_bytes(ap, SC_BYTEBUF_SIZE_IN_SAMPLES);
|
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
|
||||||
|
// without locking.
|
||||||
|
size_t bytebuf_samples = ap->target_buffering + ap->sample_rate;
|
||||||
|
size_t bytebuf_size = samples_to_bytes(ap, bytebuf_samples);
|
||||||
|
|
||||||
bool ok = sc_bytebuf_init(&ap->buf, bytebuf_size);
|
bool ok = sc_bytebuf_init(&ap->buf, bytebuf_size);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
@@ -174,12 +401,21 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
|||||||
|
|
||||||
ap->previous_write_avail = sc_bytebuf_write_available(&ap->buf);
|
ap->previous_write_avail = sc_bytebuf_write_available(&ap->buf);
|
||||||
|
|
||||||
sc_average_init(&ap->avg_buffering, 8);
|
// Samples are produced and consumed by blocks, so the buffering must be
|
||||||
|
// smoothed to get a relatively stable value.
|
||||||
|
sc_average_init(&ap->avg_buffering, 32);
|
||||||
ap->samples_since_resync = 0;
|
ap->samples_since_resync = 0;
|
||||||
|
|
||||||
ap->last_consumed = 0;
|
ap->received = false;
|
||||||
ap->underflow = 0;
|
ap->played = false;
|
||||||
ap->received = 0;
|
|
||||||
|
// The thread calling open() is the thread calling push(), which fills the
|
||||||
|
// audio buffer consumed by the SDL audio thread.
|
||||||
|
ok = sc_thread_set_priority(SC_THREAD_PRIORITY_TIME_CRITICAL);
|
||||||
|
if (!ok) {
|
||||||
|
ok = sc_thread_set_priority(SC_THREAD_PRIORITY_HIGH);
|
||||||
|
(void) ok; // We don't care if it worked, at least we tried
|
||||||
|
}
|
||||||
|
|
||||||
SDL_PauseAudioDevice(ap->device, 0);
|
SDL_PauseAudioDevice(ap->device, 0);
|
||||||
|
|
||||||
@@ -208,166 +444,10 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) {
|
|||||||
swr_free(&ap->swr_ctx);
|
swr_free(&ap->swr_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
|
||||||
const AVFrame *frame) {
|
|
||||||
struct sc_audio_player *ap = DOWNCAST(sink);
|
|
||||||
|
|
||||||
SwrContext *swr_ctx = ap->swr_ctx;
|
|
||||||
|
|
||||||
int64_t delay = swr_get_delay(swr_ctx, ap->sample_rate);
|
|
||||||
// No need to av_rescale_rnd(), input and output sample rates are the same
|
|
||||||
// Add more space (256) for clock compensation
|
|
||||||
int dst_nb_samples = delay + frame->nb_samples + 256;
|
|
||||||
|
|
||||||
uint8_t *swr_buf = sc_audio_player_get_swr_buf(ap, dst_nb_samples);
|
|
||||||
if (!swr_buf) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples,
|
|
||||||
(const uint8_t **) frame->data, frame->nb_samples);
|
|
||||||
if (ret < 0) {
|
|
||||||
LOGE("Resampling failed: %d", ret);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// swr_convert() returns the number of samples which would have been
|
|
||||||
// written if the buffer was big enough.
|
|
||||||
size_t samples_written = MIN(ret, dst_nb_samples);
|
|
||||||
size_t swr_buf_size = samples_to_bytes(ap, samples_written);
|
|
||||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
|
||||||
LOGD("[Audio] %" SC_PRIsizet " samples written to buffer", samples_written);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Since this function is the only writer, the current available space is
|
|
||||||
// at least the previous available space. In practice, it should almost
|
|
||||||
// always be possible to write without lock.
|
|
||||||
bool lockless_write = swr_buf_size <= ap->previous_write_avail;
|
|
||||||
if (lockless_write) {
|
|
||||||
sc_bytebuf_prepare_write(&ap->buf, swr_buf, swr_buf_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_LockAudioDevice(ap->device);
|
|
||||||
|
|
||||||
// The consumer requests audio samples blocks (e.g. 480 samples).
|
|
||||||
// Convert the duration since the last consumption into samples.
|
|
||||||
size_t extrapolated = 0;
|
|
||||||
if (ap->last_consumed) {
|
|
||||||
sc_tick now = sc_tick_now();
|
|
||||||
assert(now >= ap->last_consumed);
|
|
||||||
extrapolated = (now - ap->last_consumed) * ap->sample_rate
|
|
||||||
/ SC_TICK_FREQ;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t read_avail = sc_bytebuf_read_available(&ap->buf);
|
|
||||||
|
|
||||||
// The consumer may not increase underflow value if there are still samples
|
|
||||||
// available
|
|
||||||
assert(read_avail == 0 || ap->underflow == 0);
|
|
||||||
|
|
||||||
size_t buffered_samples = bytes_to_samples(ap, read_avail);
|
|
||||||
// Underflow caused silence samples in excess (so it adds buffering).
|
|
||||||
// Extrapolated samples must be considered consumed for smoothing (so it
|
|
||||||
// removes buffering).
|
|
||||||
float buffering = (float) buffered_samples + ap->underflow - extrapolated;
|
|
||||||
sc_average_push(&ap->avg_buffering, buffering);
|
|
||||||
|
|
||||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
|
||||||
LOGD("[Audio] buffered_samples=%" SC_PRIsizet
|
|
||||||
" underflow=%" SC_PRIsizet
|
|
||||||
" extrapolated=%" SC_PRIsizet
|
|
||||||
" buffering=%f avg_buffering=%f",
|
|
||||||
buffered_samples, ap->underflow, extrapolated, buffering,
|
|
||||||
sc_average_get(&ap->avg_buffering));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (lockless_write) {
|
|
||||||
sc_bytebuf_commit_write(&ap->buf, swr_buf_size);
|
|
||||||
} else {
|
|
||||||
// Take care to keep full samples
|
|
||||||
size_t align = ap->nb_channels * ap->out_bytes_per_sample;
|
|
||||||
size_t write_avail =
|
|
||||||
sc_bytebuf_write_available(&ap->buf) / align * align;
|
|
||||||
if (swr_buf_size > write_avail) {
|
|
||||||
// Skip old samples
|
|
||||||
size_t cap = sc_bytebuf_capacity(&ap->buf) / align * align;
|
|
||||||
if (swr_buf_size > cap) {
|
|
||||||
// Ignore the first bytes in swr_buf
|
|
||||||
swr_buf += swr_buf_size - cap;
|
|
||||||
swr_buf_size = cap;
|
|
||||||
}
|
|
||||||
assert(swr_buf_size > write_avail);
|
|
||||||
if (swr_buf_size - write_avail > 0) {
|
|
||||||
sc_bytebuf_skip(&ap->buf, swr_buf_size - write_avail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sc_bytebuf_write(&ap->buf, swr_buf, swr_buf_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
// On buffer underflow, typically because a packet is late, silence is
|
|
||||||
// inserted. In that case, the late samples must be ignored when they
|
|
||||||
// arrive, otherwise they will delay playback.
|
|
||||||
//
|
|
||||||
// As an improvement, instead of naively skipping the silence duration, we
|
|
||||||
// can absorb it if it helps clock compensation.
|
|
||||||
if (ap->underflow) {
|
|
||||||
size_t avg = sc_average_get(&ap->avg_buffering);
|
|
||||||
if (avg > SC_TARGET_BUFFERED_SAMPLES) {
|
|
||||||
size_t diff = SC_TARGET_BUFFERED_SAMPLES - avg;
|
|
||||||
if (ap->underflow > diff) {
|
|
||||||
// Partially absorb underflow for clock compensation (only keep
|
|
||||||
// the diff with the target buffering level).
|
|
||||||
ap->underflow -= diff;
|
|
||||||
} else {
|
|
||||||
// Totally absorb underflow for clock compensation
|
|
||||||
ap->underflow = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t skip_samples = MIN(ap->underflow, buffered_samples);
|
|
||||||
if (skip_samples) {
|
|
||||||
size_t skip_bytes = samples_to_bytes(ap, skip_samples);
|
|
||||||
sc_bytebuf_skip(&ap->buf, skip_bytes);
|
|
||||||
read_avail -= skip_bytes;
|
|
||||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
|
||||||
LOGD("[Audio] Skipping %" SC_PRIsizet " samples", skip_samples);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Totally absorb underflow for clock compensation
|
|
||||||
ap->underflow = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ap->previous_write_avail = sc_bytebuf_write_available(&ap->buf);
|
|
||||||
ap->received = true;
|
|
||||||
|
|
||||||
SDL_UnlockAudioDevice(ap->device);
|
|
||||||
|
|
||||||
ap->samples_since_resync += samples_written;
|
|
||||||
if (ap->samples_since_resync >= ap->sample_rate) {
|
|
||||||
// Resync every second
|
|
||||||
ap->samples_since_resync = 0;
|
|
||||||
|
|
||||||
float avg = sc_average_get(&ap->avg_buffering);
|
|
||||||
int diff = SC_TARGET_BUFFERED_SAMPLES - avg;
|
|
||||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
|
||||||
LOGD("[Audio] Average buffering=%f, compensation %d", avg, diff);
|
|
||||||
#endif
|
|
||||||
// Compensate the diff over 3 seconds (but will be recomputed after
|
|
||||||
// 1 second)
|
|
||||||
int ret = swr_set_compensation(swr_ctx, diff, 3 * ap->sample_rate);
|
|
||||||
if (ret < 0) {
|
|
||||||
LOGW("Resampling compensation failed: %d", ret);
|
|
||||||
// not fatal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_audio_player_init(struct sc_audio_player *ap) {
|
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering) {
|
||||||
|
ap->target_buffering_delay = target_buffering;
|
||||||
|
|
||||||
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,
|
||||||
.close = sc_audio_player_frame_sink_close,
|
.close = sc_audio_player_frame_sink_close,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <util/average.h>
|
#include <util/average.h>
|
||||||
#include <util/bytebuf.h>
|
#include <util/bytebuf.h>
|
||||||
#include <util/thread.h>
|
#include <util/thread.h>
|
||||||
|
#include <util/tick.h>
|
||||||
|
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
#include <libswresample/swresample.h>
|
#include <libswresample/swresample.h>
|
||||||
@@ -18,10 +19,23 @@ struct sc_audio_player {
|
|||||||
|
|
||||||
SDL_AudioDeviceID device;
|
SDL_AudioDeviceID device;
|
||||||
|
|
||||||
// protected by SDL_AudioDeviceLock()
|
// The target buffering between the producer and the consumer. This value
|
||||||
|
// is directly use for compensation.
|
||||||
|
// Since audio capture and/or encoding on the device typically produce
|
||||||
|
// blocks of 960 samples (20ms) or 1024 samples (~21.3ms), this target
|
||||||
|
// value should be higher.
|
||||||
|
sc_tick target_buffering_delay;
|
||||||
|
uint32_t target_buffering; // in samples
|
||||||
|
|
||||||
|
// Audio buffer to communicate between the receiver and the SDL audio
|
||||||
|
// callback (protected by SDL_AudioDeviceLock())
|
||||||
struct sc_bytebuf buf;
|
struct sc_bytebuf buf;
|
||||||
|
|
||||||
|
// The previous number of bytes available in the buffer (only used by the
|
||||||
|
// receiver thread)
|
||||||
size_t previous_write_avail;
|
size_t previous_write_avail;
|
||||||
|
|
||||||
|
// Resampler (only used from the receiver thread)
|
||||||
struct SwrContext *swr_ctx;
|
struct SwrContext *swr_ctx;
|
||||||
|
|
||||||
// The sample rate is the same for input and output
|
// The sample rate is the same for input and output
|
||||||
@@ -31,22 +45,25 @@ struct sc_audio_player {
|
|||||||
// The number of bytes per sample for a single channel
|
// The number of bytes per sample for a single channel
|
||||||
unsigned out_bytes_per_sample;
|
unsigned out_bytes_per_sample;
|
||||||
|
|
||||||
// Target buffer for resampling
|
// Target buffer for resampling (only used by the receiver thread)
|
||||||
uint8_t *swr_buf;
|
uint8_t *swr_buf;
|
||||||
size_t swr_buf_alloc_size;
|
size_t swr_buf_alloc_size;
|
||||||
|
|
||||||
// Number of buffered samples (may be negative on underflow)
|
// Number of buffered samples (may be negative on underflow) (only used by
|
||||||
|
// the receiver thread)
|
||||||
struct sc_average avg_buffering;
|
struct sc_average avg_buffering;
|
||||||
// Count the number of samples to trigger a compensation update regularly
|
// Count the number of samples to trigger a compensation update regularly
|
||||||
size_t samples_since_resync;
|
// (only used by the receiver thread)
|
||||||
|
uint32_t samples_since_resync;
|
||||||
|
|
||||||
// The last date a sample has been consumed by the audio output
|
// Set to true the first time a sample is received (protected by
|
||||||
sc_tick last_consumed;
|
// SDL_AudioDeviceLock())
|
||||||
|
|
||||||
// Number of silence samples inserted to be compensated
|
|
||||||
size_t underflow;
|
|
||||||
bool received;
|
bool received;
|
||||||
|
|
||||||
|
// Set to true the first time the SDL callback is called (protected by
|
||||||
|
// SDL_AudioDeviceLock())
|
||||||
|
bool played;
|
||||||
|
|
||||||
const struct sc_audio_player_callbacks *cbs;
|
const struct sc_audio_player_callbacks *cbs;
|
||||||
void *cbs_userdata;
|
void *cbs_userdata;
|
||||||
};
|
};
|
||||||
@@ -56,6 +73,6 @@ struct sc_audio_player_callbacks {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_audio_player_init(struct sc_audio_player *ap);
|
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
enum {
|
enum {
|
||||||
OPT_RENDER_EXPIRED_FRAMES = 1000,
|
OPT_RENDER_EXPIRED_FRAMES = 1000,
|
||||||
|
OPT_BIT_RATE,
|
||||||
OPT_WINDOW_TITLE,
|
OPT_WINDOW_TITLE,
|
||||||
OPT_PUSH_TARGET,
|
OPT_PUSH_TARGET,
|
||||||
OPT_ALWAYS_ON_TOP,
|
OPT_ALWAYS_ON_TOP,
|
||||||
@@ -124,15 +125,16 @@ static const struct sc_option options[] = {
|
|||||||
.longopt_id = OPT_AUDIO_BUFFER,
|
.longopt_id = OPT_AUDIO_BUFFER,
|
||||||
.longopt = "audio-buffer",
|
.longopt = "audio-buffer",
|
||||||
.argdesc = "ms",
|
.argdesc = "ms",
|
||||||
.text = "Add a buffering delay (in milliseconds) before playing audio. "
|
.text = "Configure the audio buffering delay (in milliseconds).\n"
|
||||||
"This increases latency to compensate for jitter.\n"
|
"Lower values decrease the latency, but increase the "
|
||||||
"Default is 0 (no buffering).",
|
"likelyhood of buffer underruns (causing audio glitches).\n"
|
||||||
|
"Default is 50.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_AUDIO_CODEC,
|
.longopt_id = OPT_AUDIO_CODEC,
|
||||||
.longopt = "audio-codec",
|
.longopt = "audio-codec",
|
||||||
.argdesc = "name",
|
.argdesc = "name",
|
||||||
.text = "Select an audio codec (raw, opus or aac).\n"
|
.text = "Select an audio codec (opus, aac or raw).\n"
|
||||||
"Default is opus.",
|
"Default is opus.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -163,6 +165,12 @@ static const struct sc_option options[] = {
|
|||||||
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||||
"Default is 8M (8000000).",
|
"Default is 8M (8000000).",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// deprecated
|
||||||
|
.longopt_id = OPT_BIT_RATE,
|
||||||
|
.longopt = "bit-rate",
|
||||||
|
.argdesc = "value",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// Not really deprecated (--codec has never been released), but without
|
// Not really deprecated (--codec has never been released), but without
|
||||||
// declaring an explicit --codec option, getopt_long() partial matching
|
// declaring an explicit --codec option, getopt_long() partial matching
|
||||||
@@ -472,8 +480,8 @@ static const struct sc_option options[] = {
|
|||||||
.longopt_id = OPT_REQUIRE_AUDIO,
|
.longopt_id = OPT_REQUIRE_AUDIO,
|
||||||
.longopt = "require-audio",
|
.longopt = "require-audio",
|
||||||
.text = "By default, scrcpy mirrors only the video when audio capture "
|
.text = "By default, scrcpy mirrors only the video when audio capture "
|
||||||
"fails on the device. This flag makes scrcpy fail if audio is "
|
"fails on the device. This option makes scrcpy fail if audio "
|
||||||
"enabled but does not work."
|
"is enabled but does not work."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_ROTATION,
|
.longopt_id = OPT_ROTATION,
|
||||||
@@ -1506,10 +1514,6 @@ parse_video_codec(const char *optarg, enum sc_codec *codec) {
|
|||||||
|
|
||||||
static bool
|
static bool
|
||||||
parse_audio_codec(const char *optarg, enum sc_codec *codec) {
|
parse_audio_codec(const char *optarg, enum sc_codec *codec) {
|
||||||
if (!strcmp(optarg, "raw")) {
|
|
||||||
*codec = SC_CODEC_RAW;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!strcmp(optarg, "opus")) {
|
if (!strcmp(optarg, "opus")) {
|
||||||
*codec = SC_CODEC_OPUS;
|
*codec = SC_CODEC_OPUS;
|
||||||
return true;
|
return true;
|
||||||
@@ -1518,7 +1522,11 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) {
|
|||||||
*codec = SC_CODEC_AAC;
|
*codec = SC_CODEC_AAC;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
LOGE("Unsupported audio codec: %s (expected raw, opus or aac)", optarg);
|
if (!strcmp(optarg, "raw")) {
|
||||||
|
*codec = SC_CODEC_RAW;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
LOGE("Unsupported audio codec: %s (expected opus, aac or raw)", optarg);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1532,6 +1540,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
int c;
|
int c;
|
||||||
while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) {
|
while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
|
case OPT_BIT_RATE:
|
||||||
|
LOGW("--bit-rate is deprecated, use --video-bit-rate instead.");
|
||||||
|
// fall through
|
||||||
case 'b':
|
case 'b':
|
||||||
if (!parse_bit_rate(optarg, &opts->video_bit_rate)) {
|
if (!parse_bit_rate(optarg, &opts->video_bit_rate)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1933,18 +1944,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts->audio_codec == SC_CODEC_RAW) {
|
|
||||||
if (opts->audio_bit_rate) {
|
|
||||||
LOGW("--audio-bit-rate is ignored for raw audio codec");
|
|
||||||
}
|
|
||||||
if (opts->audio_codec_options) {
|
|
||||||
LOGW("--audio-codec-options is ignored for raw audio codec");
|
|
||||||
}
|
|
||||||
if (opts->audio_encoder) {
|
|
||||||
LOGW("--audio-encoder is ignored for raw audio codec");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!opts->control) {
|
if (!opts->control) {
|
||||||
if (opts->turn_screen_off) {
|
if (opts->turn_screen_off) {
|
||||||
LOGE("Could not request to turn screen off if control is disabled");
|
LOGE("Could not request to turn screen off if control is disabled");
|
||||||
|
|||||||
@@ -105,8 +105,7 @@ sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
|
|||||||
sc_clock_estimate(clock, &clock->slope, &clock->offset);
|
sc_clock_estimate(clock, &clock->slope, &clock->offset);
|
||||||
|
|
||||||
#ifndef SC_CLOCK_NDEBUG
|
#ifndef SC_CLOCK_NDEBUG
|
||||||
LOGD("Clock estimation: %f * pts + %" PRItick,
|
LOGD("Clock estimation: %f * pts + %" PRItick, clock->slope, clock->offset);
|
||||||
clock->slope, clock->offset);
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,10 @@
|
|||||||
# define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
|
# define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 16)
|
||||||
|
# define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifndef HAVE_STRDUP
|
#ifndef HAVE_STRDUP
|
||||||
char *strdup(const char *s);
|
char *strdup(const char *s);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ run_buffering(void *data) {
|
|||||||
|
|
||||||
sc_tick max_deadline = sc_tick_now() + db->delay;
|
sc_tick max_deadline = sc_tick_now() + db->delay;
|
||||||
// PTS (written by the server) are expressed in microseconds
|
// PTS (written by the server) are expressed in microseconds
|
||||||
sc_tick pts = SC_TICK_TO_US(dframe.frame->pts);
|
sc_tick pts = SC_TICK_FROM_US(dframe.frame->pts);
|
||||||
|
|
||||||
bool timed_out = false;
|
bool timed_out = false;
|
||||||
while (!db->stopped && !timed_out) {
|
while (!db->stopped && !timed_out) {
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
|
|||||||
#define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII
|
#define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII
|
||||||
#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII
|
#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII
|
||||||
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII
|
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII
|
||||||
#define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII
|
|
||||||
#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII
|
#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII
|
||||||
#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII"
|
#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII"
|
||||||
|
#define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII
|
||||||
switch (codec_id) {
|
switch (codec_id) {
|
||||||
case SC_CODEC_ID_H264:
|
case SC_CODEC_ID_H264:
|
||||||
return AV_CODEC_ID_H264;
|
return AV_CODEC_ID_H264;
|
||||||
@@ -33,12 +33,12 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
|
|||||||
return AV_CODEC_ID_HEVC;
|
return AV_CODEC_ID_HEVC;
|
||||||
case SC_CODEC_ID_AV1:
|
case SC_CODEC_ID_AV1:
|
||||||
return AV_CODEC_ID_AV1;
|
return AV_CODEC_ID_AV1;
|
||||||
case SC_CODEC_ID_RAW:
|
|
||||||
return AV_CODEC_ID_PCM_S16LE;
|
|
||||||
case SC_CODEC_ID_OPUS:
|
case SC_CODEC_ID_OPUS:
|
||||||
return AV_CODEC_ID_OPUS;
|
return AV_CODEC_ID_OPUS;
|
||||||
case SC_CODEC_ID_AAC:
|
case SC_CODEC_ID_AAC:
|
||||||
return AV_CODEC_ID_AAC;
|
return AV_CODEC_ID_AAC;
|
||||||
|
case SC_CODEC_ID_RAW:
|
||||||
|
return AV_CODEC_ID_PCM_S16LE;
|
||||||
default:
|
default:
|
||||||
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
|
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
|
||||||
return AV_CODEC_ID_NONE;
|
return AV_CODEC_ID_NONE;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
|||||||
.display_id = 0,
|
.display_id = 0,
|
||||||
.display_buffer = 0,
|
.display_buffer = 0,
|
||||||
.v4l2_buffer = 0,
|
.v4l2_buffer = 0,
|
||||||
.audio_buffer = 0,
|
.audio_buffer = SC_TICK_FROM_MS(50),
|
||||||
#ifdef HAVE_USB
|
#ifdef HAVE_USB
|
||||||
.otg = false,
|
.otg = false,
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ enum sc_codec {
|
|||||||
SC_CODEC_H264,
|
SC_CODEC_H264,
|
||||||
SC_CODEC_H265,
|
SC_CODEC_H265,
|
||||||
SC_CODEC_AV1,
|
SC_CODEC_AV1,
|
||||||
SC_CODEC_RAW,
|
|
||||||
SC_CODEC_OPUS,
|
SC_CODEC_OPUS,
|
||||||
SC_CODEC_AAC,
|
SC_CODEC_AAC,
|
||||||
|
SC_CODEC_RAW,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum sc_lock_video_orientation {
|
enum sc_lock_video_orientation {
|
||||||
|
|||||||
@@ -240,7 +240,8 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
|
|||||||
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
|
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recorder->stopped && sc_vecdeque_is_empty(&recorder->video_queue)) {
|
if (sc_vecdeque_is_empty(&recorder->video_queue)) {
|
||||||
|
assert(recorder->stopped);
|
||||||
// If the recorder is stopped, don't process anything if there are not
|
// If the recorder is stopped, don't process anything if there are not
|
||||||
// at least video packets
|
// at least video packets
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
sc_mutex_unlock(&recorder->mutex);
|
||||||
@@ -394,10 +395,6 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
|
|||||||
error = true;
|
error = true;
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
// If the recorder is stopped while one of the streams has no
|
|
||||||
// packets, then we must avoid a live-loop and correctly record
|
|
||||||
// the stream having packets.
|
|
||||||
pts_origin = video_pkt ? video_pkt->pts : audio_pkt->pts;
|
|
||||||
} else {
|
} else {
|
||||||
// We need both video and audio packets to initialize pts_origin
|
// We need both video and audio packets to initialize pts_origin
|
||||||
continue;
|
continue;
|
||||||
@@ -508,6 +505,10 @@ static int
|
|||||||
run_recorder(void *data) {
|
run_recorder(void *data) {
|
||||||
struct sc_recorder *recorder = data;
|
struct sc_recorder *recorder = data;
|
||||||
|
|
||||||
|
// Recording is a background task
|
||||||
|
bool ok = sc_thread_set_priority(SC_THREAD_PRIORITY_LOW);
|
||||||
|
(void) ok; // We don't care if it worked
|
||||||
|
|
||||||
bool success = sc_recorder_record(recorder);
|
bool success = sc_recorder_record(recorder);
|
||||||
|
|
||||||
sc_mutex_lock(&recorder->mutex);
|
sc_mutex_lock(&recorder->mutex);
|
||||||
@@ -583,7 +584,7 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
rec->stream_index = 0;
|
rec->stream_index = recorder->video_stream_index;
|
||||||
|
|
||||||
bool ok = sc_vecdeque_push(&recorder->video_queue, rec);
|
bool ok = sc_vecdeque_push(&recorder->video_queue, rec);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
@@ -652,7 +653,7 @@ sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
rec->stream_index = 1;
|
rec->stream_index = recorder->audio_stream_index;
|
||||||
|
|
||||||
bool ok = sc_vecdeque_push(&recorder->audio_queue, rec);
|
bool ok = sc_vecdeque_push(&recorder->audio_queue, rec);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ struct scrcpy {
|
|||||||
struct sc_server server;
|
struct sc_server server;
|
||||||
struct sc_screen screen;
|
struct sc_screen screen;
|
||||||
struct sc_audio_player audio_player;
|
struct sc_audio_player audio_player;
|
||||||
struct sc_delay_buffer audio_buffer;
|
|
||||||
struct sc_demuxer video_demuxer;
|
struct sc_demuxer video_demuxer;
|
||||||
struct sc_demuxer audio_demuxer;
|
struct sc_demuxer audio_demuxer;
|
||||||
struct sc_decoder video_decoder;
|
struct sc_decoder video_decoder;
|
||||||
@@ -246,13 +245,6 @@ sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer,
|
|||||||
|
|
||||||
// Contrary to the video demuxer, keep mirroring if only the audio fails
|
// Contrary to the video demuxer, keep mirroring if only the audio fails
|
||||||
// (unless --require-audio is set).
|
// (unless --require-audio is set).
|
||||||
// 'eos' is true on end-of-stream, including when audio capture is not
|
|
||||||
// possible on the device (so that scrcpy continue to mirror video without
|
|
||||||
// failing).
|
|
||||||
// However, if an audio configuration failure occurs (for example the user
|
|
||||||
// explicitly selected an unknown audio encoder), 'eos' is false and scrcpy
|
|
||||||
// must exit.
|
|
||||||
|
|
||||||
if (status == SC_DEMUXER_STATUS_ERROR
|
if (status == SC_DEMUXER_STATUS_ERROR
|
||||||
|| (status == SC_DEMUXER_STATUS_DISABLED
|
|| (status == SC_DEMUXER_STATUS_DISABLED
|
||||||
&& options->require_audio)) {
|
&& options->require_audio)) {
|
||||||
@@ -695,16 +687,9 @@ 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) {
|
||||||
struct sc_frame_source *src = &s->audio_decoder.frame_source;
|
sc_audio_player_init(&s->audio_player, options->audio_buffer);
|
||||||
if (options->audio_buffer) {
|
sc_frame_source_add_sink(&s->audio_decoder.frame_source,
|
||||||
sc_delay_buffer_init(&s->audio_buffer, options->audio_buffer,
|
&s->audio_player.frame_sink);
|
||||||
false);
|
|
||||||
sc_frame_source_add_sink(src, &s->audio_buffer.frame_sink);
|
|
||||||
src = &s->audio_buffer.frame_source;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_audio_player_init(&s->audio_player);
|
|
||||||
sc_frame_source_add_sink(src, &s->audio_player.frame_sink);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -169,12 +169,12 @@ sc_server_get_codec_name(enum sc_codec codec) {
|
|||||||
return "h265";
|
return "h265";
|
||||||
case SC_CODEC_AV1:
|
case SC_CODEC_AV1:
|
||||||
return "av1";
|
return "av1";
|
||||||
case SC_CODEC_RAW:
|
|
||||||
return "raw";
|
|
||||||
case SC_CODEC_OPUS:
|
case SC_CODEC_OPUS:
|
||||||
return "opus";
|
return "opus";
|
||||||
case SC_CODEC_AAC:
|
case SC_CODEC_AAC:
|
||||||
return "aac";
|
return "aac";
|
||||||
|
case SC_CODEC_RAW:
|
||||||
|
return "raw";
|
||||||
default:
|
default:
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
bool
|
bool
|
||||||
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size) {
|
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size) {
|
||||||
assert(alloc_size);
|
assert(alloc_size);
|
||||||
// sufficient, but use more for alignment.
|
|
||||||
buf->data = malloc(alloc_size);
|
buf->data = malloc(alloc_size);
|
||||||
if (!buf->data) {
|
if (!buf->data) {
|
||||||
LOG_OOM();
|
LOG_OOM();
|
||||||
@@ -64,8 +63,8 @@ sc_bytebuf_write_step0(struct sc_bytebuf *buf, const uint8_t *from,
|
|||||||
if (len < right_len) {
|
if (len < right_len) {
|
||||||
right_len = len;
|
right_len = len;
|
||||||
}
|
}
|
||||||
|
|
||||||
memcpy(buf->data + buf->head, from, right_len);
|
memcpy(buf->data + buf->head, from, right_len);
|
||||||
|
|
||||||
if (len > right_len) {
|
if (len > right_len) {
|
||||||
memcpy(buf->data, from + right_len, len - right_len);
|
memcpy(buf->data, from + right_len, len - right_len);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ struct sc_bytebuf {
|
|||||||
size_t head; // writter cursor
|
size_t head; // writter cursor
|
||||||
size_t tail; // reader cursor
|
size_t tail; // reader cursor
|
||||||
// empty: tail == head
|
// empty: tail == head
|
||||||
// full: (tail + 1) % allocated == head
|
// full: ((tail + 1) % alloc_size) == head
|
||||||
};
|
};
|
||||||
|
|
||||||
bool
|
bool
|
||||||
@@ -37,12 +37,10 @@ sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len);
|
|||||||
* The caller must check that len <= sc_bytebuf_read_available() (it is an
|
* The caller must check that len <= sc_bytebuf_read_available() (it is an
|
||||||
* error to attempt to skip more bytes than available).
|
* error to attempt to skip more bytes than available).
|
||||||
*
|
*
|
||||||
* This function is guaranteed not to change the head.
|
* This function is guaranteed not to write to buf->head.
|
||||||
*
|
|
||||||
* This function is guaranteed to not change the head.
|
|
||||||
*
|
*
|
||||||
* It is equivalent to call sc_bytebuf_read() to some array and discard the
|
* It is equivalent to call sc_bytebuf_read() to some array and discard the
|
||||||
* array (but more efficient since there is no copy).
|
* array (but this function is more efficient since there is no copy).
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len);
|
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len);
|
||||||
|
|||||||
@@ -125,8 +125,30 @@ sc_av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
|||||||
free(local_fmt);
|
free(local_fmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const char *const sc_sdl_log_priority_names[SDL_NUM_LOG_PRIORITIES] = {
|
||||||
|
[SDL_LOG_PRIORITY_VERBOSE] = "VERBOSE",
|
||||||
|
[SDL_LOG_PRIORITY_DEBUG] = "DEBUG",
|
||||||
|
[SDL_LOG_PRIORITY_INFO] = "INFO",
|
||||||
|
[SDL_LOG_PRIORITY_WARN] = "WARN",
|
||||||
|
[SDL_LOG_PRIORITY_ERROR] = "ERROR",
|
||||||
|
[SDL_LOG_PRIORITY_CRITICAL] = "CRITICAL",
|
||||||
|
};
|
||||||
|
|
||||||
|
static void SDLCALL
|
||||||
|
sc_sdl_log_print(void *userdata, int category, SDL_LogPriority priority,
|
||||||
|
const char *message) {
|
||||||
|
(void) userdata;
|
||||||
|
(void) category;
|
||||||
|
|
||||||
|
FILE *out = priority < SDL_LOG_PRIORITY_WARN ? stdout : stderr;
|
||||||
|
assert(priority < SDL_NUM_LOG_PRIORITIES);
|
||||||
|
const char *prio_name = sc_sdl_log_priority_names[priority];
|
||||||
|
fprintf(out, "%s: %s\n", prio_name, message);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_log_configure() {
|
sc_log_configure() {
|
||||||
|
SDL_LogSetOutputFunction(sc_sdl_log_print, NULL);
|
||||||
// Redirect FFmpeg logs to SDL logs
|
// Redirect FFmpeg logs to SDL logs
|
||||||
av_log_set_callback(sc_av_log_callback);
|
av_log_set_callback(sc_av_log_callback);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
/* Like calloc(), but without initialization.
|
/**
|
||||||
|
* Allocate an array of `nmemb` items of `size` bytes each
|
||||||
|
*
|
||||||
|
* Like calloc(), but without initialization.
|
||||||
* Like reallocarray(), but without reallocation.
|
* Like reallocarray(), but without reallocation.
|
||||||
*/
|
*/
|
||||||
void *
|
void *
|
||||||
|
|||||||
@@ -23,6 +23,39 @@ sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static SDL_ThreadPriority
|
||||||
|
to_sdl_thread_priority(enum sc_thread_priority priority) {
|
||||||
|
switch (priority) {
|
||||||
|
case SC_THREAD_PRIORITY_TIME_CRITICAL:
|
||||||
|
#ifdef SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL
|
||||||
|
return SDL_THREAD_PRIORITY_TIME_CRITICAL;
|
||||||
|
#else
|
||||||
|
// fall through
|
||||||
|
#endif
|
||||||
|
case SC_THREAD_PRIORITY_HIGH:
|
||||||
|
return SDL_THREAD_PRIORITY_HIGH;
|
||||||
|
case SC_THREAD_PRIORITY_NORMAL:
|
||||||
|
return SDL_THREAD_PRIORITY_NORMAL;
|
||||||
|
case SC_THREAD_PRIORITY_LOW:
|
||||||
|
return SDL_THREAD_PRIORITY_LOW;
|
||||||
|
default:
|
||||||
|
assert(!"Unknown thread priority");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_thread_set_priority(enum sc_thread_priority priority) {
|
||||||
|
SDL_ThreadPriority sdl_priority = to_sdl_thread_priority(priority);
|
||||||
|
int r = SDL_SetThreadPriority(sdl_priority);
|
||||||
|
if (r) {
|
||||||
|
LOGD("Could not set thread priority: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_thread_join(sc_thread *thread, int *status) {
|
sc_thread_join(sc_thread *thread, int *status) {
|
||||||
SDL_WaitThread(thread->thread, status);
|
SDL_WaitThread(thread->thread, status);
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ typedef struct sc_thread {
|
|||||||
SDL_Thread *thread;
|
SDL_Thread *thread;
|
||||||
} sc_thread;
|
} sc_thread;
|
||||||
|
|
||||||
|
enum sc_thread_priority {
|
||||||
|
SC_THREAD_PRIORITY_LOW,
|
||||||
|
SC_THREAD_PRIORITY_NORMAL,
|
||||||
|
SC_THREAD_PRIORITY_HIGH,
|
||||||
|
SC_THREAD_PRIORITY_TIME_CRITICAL,
|
||||||
|
};
|
||||||
|
|
||||||
typedef struct sc_mutex {
|
typedef struct sc_mutex {
|
||||||
SDL_mutex *mutex;
|
SDL_mutex *mutex;
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
@@ -39,6 +46,9 @@ sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name,
|
|||||||
void
|
void
|
||||||
sc_thread_join(sc_thread *thread, int *status);
|
sc_thread_join(sc_thread *thread, int *status);
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_thread_set_priority(enum sc_thread_priority priority);
|
||||||
|
|
||||||
bool
|
bool
|
||||||
sc_mutex_init(sc_mutex *mutex);
|
sc_mutex_init(sc_mutex *mutex);
|
||||||
|
|
||||||
|
|||||||
@@ -52,10 +52,10 @@
|
|||||||
*/
|
*/
|
||||||
#define sc_vecdeque_init(pv) \
|
#define sc_vecdeque_init(pv) \
|
||||||
({ \
|
({ \
|
||||||
(pv)->data = NULL; \
|
|
||||||
(pv)->cap = 0; \
|
(pv)->cap = 0; \
|
||||||
(pv)->origin = 0; \
|
(pv)->origin = 0; \
|
||||||
(pv)->size = 0; \
|
(pv)->size = 0; \
|
||||||
|
(pv)->data = NULL; \
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
* \param item_size the size of one item (the generic type is unknown from this
|
* \param item_size the size of one item (the generic type is unknown from this
|
||||||
* function)
|
* function)
|
||||||
* \param pcap a pointer to the `cap` field of the SC_VECDEQUE [IN/OUT]
|
* \param pcap a pointer to the `cap` field of the SC_VECDEQUE [IN/OUT]
|
||||||
* \param porigin a pointer to pv->origin (will be read and updated)
|
* \param porigin a pointer to pv->origin [IN/OUT]
|
||||||
* \param size the `size` field of the SC_VECDEQUE
|
* \param size the `size` field of the SC_VECDEQUE
|
||||||
* \return the new array to assign to the `data` field of the SC_VECDEQUE (if
|
* \return the new array to assign to the `data` field of the SC_VECDEQUE (if
|
||||||
* not NULL)
|
* not NULL)
|
||||||
@@ -312,7 +312,7 @@ sc_vecdeque_growsize_(size_t value)
|
|||||||
*
|
*
|
||||||
* If the VecDeque is full, it is resized.
|
* If the VecDeque is full, it is resized.
|
||||||
*
|
*
|
||||||
* This function returns either a valid non-nULL pointer to the uninitialized
|
* This function returns either a valid non-NULL pointer to the uninitialized
|
||||||
* item just pushed, or NULL on reallocation failure.
|
* item just pushed, or NULL on reallocation failure.
|
||||||
*/
|
*/
|
||||||
#define sc_vecdeque_push_hole(pv) \
|
#define sc_vecdeque_push_hole(pv) \
|
||||||
@@ -369,7 +369,7 @@ sc_vecdeque_growsize_(size_t value)
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pop an item and returns it
|
* Pop an item and return it
|
||||||
*
|
*
|
||||||
* It is an error to call this function if the VecDeque is empty.
|
* It is an error to call this function if the VecDeque is empty.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
public interface AsyncProcessor {
|
||||||
|
void start();
|
||||||
|
void stop();
|
||||||
|
void join() throws InterruptedException;
|
||||||
|
}
|
||||||
@@ -3,9 +3,9 @@ package com.genymobile.scrcpy;
|
|||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
|
|
||||||
public enum AudioCodec implements Codec {
|
public enum AudioCodec implements Codec {
|
||||||
RAW(0x00_72_61_77, "raw", MediaFormat.MIMETYPE_AUDIO_RAW),
|
|
||||||
OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS),
|
OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS),
|
||||||
AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC);
|
AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC),
|
||||||
|
RAW(0x00_72_61_77, "raw", MediaFormat.MIMETYPE_AUDIO_RAW);
|
||||||
|
|
||||||
private final int id; // 4-byte ASCII representation of the name
|
private final int id; // 4-byte ASCII representation of the name
|
||||||
private final String name;
|
private final String name;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import java.util.List;
|
|||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
|
||||||
public final class AudioEncoder implements AudioRecorder {
|
public final class AudioEncoder implements AsyncProcessor {
|
||||||
|
|
||||||
private static class InputTask {
|
private static class InputTask {
|
||||||
private final int index;
|
private final int index;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import android.media.MediaCodec;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
public final class AudioRawRecorder implements AudioRecorder {
|
public final class AudioRawRecorder implements AsyncProcessor {
|
||||||
|
|
||||||
private final Streamer streamer;
|
private final Streamer streamer;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.genymobile.scrcpy;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A component able to record audio asynchronously
|
|
||||||
*
|
|
||||||
* The implementation is responsible to send packets.
|
|
||||||
*/
|
|
||||||
public interface AudioRecorder {
|
|
||||||
void start();
|
|
||||||
void stop();
|
|
||||||
void join() throws InterruptedException;
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,7 @@ import java.util.concurrent.Executors;
|
|||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class Controller {
|
public class Controller implements AsyncProcessor {
|
||||||
|
|
||||||
private static final int DEFAULT_DEVICE_ID = 0;
|
private static final int DEFAULT_DEVICE_ID = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -39,28 +39,28 @@ public final class Ln {
|
|||||||
public static void v(String message) {
|
public static void v(String message) {
|
||||||
if (isEnabled(Level.VERBOSE)) {
|
if (isEnabled(Level.VERBOSE)) {
|
||||||
Log.v(TAG, message);
|
Log.v(TAG, message);
|
||||||
System.out.println(PREFIX + "VERBOSE: " + message);
|
System.out.print(PREFIX + "VERBOSE: " + message + '\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void d(String message) {
|
public static void d(String message) {
|
||||||
if (isEnabled(Level.DEBUG)) {
|
if (isEnabled(Level.DEBUG)) {
|
||||||
Log.d(TAG, message);
|
Log.d(TAG, message);
|
||||||
System.out.println(PREFIX + "DEBUG: " + message);
|
System.out.print(PREFIX + "DEBUG: " + message + '\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void i(String message) {
|
public static void i(String message) {
|
||||||
if (isEnabled(Level.INFO)) {
|
if (isEnabled(Level.INFO)) {
|
||||||
Log.i(TAG, message);
|
Log.i(TAG, message);
|
||||||
System.out.println(PREFIX + "INFO: " + message);
|
System.out.print(PREFIX + "INFO: " + message + '\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void w(String message, Throwable throwable) {
|
public static void w(String message, Throwable throwable) {
|
||||||
if (isEnabled(Level.WARN)) {
|
if (isEnabled(Level.WARN)) {
|
||||||
Log.w(TAG, message, throwable);
|
Log.w(TAG, message, throwable);
|
||||||
System.out.println(PREFIX + "WARN: " + message);
|
System.err.print(PREFIX + "WARN: " + message + '\n');
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
throwable.printStackTrace();
|
throwable.printStackTrace();
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ public final class Ln {
|
|||||||
public static void e(String message, Throwable throwable) {
|
public static void e(String message, Throwable throwable) {
|
||||||
if (isEnabled(Level.ERROR)) {
|
if (isEnabled(Level.ERROR)) {
|
||||||
Log.e(TAG, message, throwable);
|
Log.e(TAG, message, throwable);
|
||||||
System.out.println(PREFIX + "ERROR: " + message);
|
System.err.print(PREFIX + "ERROR: " + message + "\n");
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
throwable.printStackTrace();
|
throwable.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.os.BatteryManager;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
@@ -91,8 +92,7 @@ public final class Server {
|
|||||||
Workarounds.fillAppInfo();
|
Workarounds.fillAppInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
Controller controller = null;
|
List<AsyncProcessor> asyncProcessors = new ArrayList<>();
|
||||||
AudioRecorder audioRecorder = null;
|
|
||||||
|
|
||||||
try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) {
|
try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) {
|
||||||
if (options.getSendDeviceMeta()) {
|
if (options.getSendDeviceMeta()) {
|
||||||
@@ -101,29 +101,34 @@ public final class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (control) {
|
if (control) {
|
||||||
controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
|
Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
|
||||||
controller.start();
|
device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
|
||||||
|
asyncProcessors.add(controller);
|
||||||
final Controller controllerRef = controller;
|
|
||||||
device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audio) {
|
if (audio) {
|
||||||
AudioCodec audioCodec = options.getAudioCodec();
|
AudioCodec audioCodec = options.getAudioCodec();
|
||||||
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecId(), options.getSendFrameMeta());
|
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecId(),
|
||||||
|
options.getSendFrameMeta());
|
||||||
|
AsyncProcessor audioRecorder;
|
||||||
if (audioCodec == AudioCodec.RAW) {
|
if (audioCodec == AudioCodec.RAW) {
|
||||||
audioRecorder = new AudioRawRecorder(audioStreamer);
|
audioRecorder = new AudioRawRecorder(audioStreamer);
|
||||||
} else {
|
} else {
|
||||||
audioRecorder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
|
audioRecorder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
|
||||||
options.getAudioEncoder());
|
options.getAudioEncoder());
|
||||||
}
|
}
|
||||||
audioRecorder.start();
|
asyncProcessors.add(audioRecorder);
|
||||||
}
|
}
|
||||||
|
|
||||||
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecId(),
|
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecId(),
|
||||||
options.getSendFrameMeta());
|
options.getSendFrameMeta());
|
||||||
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
|
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
|
||||||
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
|
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
|
||||||
|
|
||||||
|
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
||||||
|
asyncProcessor.start();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// synchronous
|
// synchronous
|
||||||
screenEncoder.streamScreen();
|
screenEncoder.streamScreen();
|
||||||
@@ -136,20 +141,14 @@ public final class Server {
|
|||||||
} finally {
|
} finally {
|
||||||
Ln.d("Screen streaming stopped");
|
Ln.d("Screen streaming stopped");
|
||||||
initThread.interrupt();
|
initThread.interrupt();
|
||||||
if (audioRecorder != null) {
|
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
||||||
audioRecorder.stop();
|
asyncProcessor.stop();
|
||||||
}
|
|
||||||
if (controller != null) {
|
|
||||||
controller.stop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
initThread.join();
|
initThread.join();
|
||||||
if (audioRecorder != null) {
|
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
||||||
audioRecorder.join();
|
asyncProcessor.join();
|
||||||
}
|
|
||||||
if (controller != null) {
|
|
||||||
controller.join();
|
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
// ignore
|
// ignore
|
||||||
|
|||||||
Reference in New Issue
Block a user