Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4108a311a | ||
|
|
430ecc36c1 | ||
|
|
1ad2abec1d | ||
|
|
c61f86af0b | ||
|
|
8fe6d78070 | ||
|
|
3f532ab5b5 | ||
|
|
b006c6f3ef | ||
|
|
662615baa8 | ||
|
|
0f92c17b62 | ||
|
|
9f4c16bc77 | ||
|
|
5e964c7cb9 | ||
|
|
90c2ce8885 | ||
|
|
98d4e64a0f | ||
|
|
f9cbd8726f | ||
|
|
101d7908b2 | ||
|
|
c1a4c91c51 | ||
|
|
5865b5f9b4 | ||
|
|
6ff4c92f3e | ||
|
|
d9e5356586 | ||
|
|
60b39cdda5 | ||
|
|
b581fd78c1 | ||
|
|
97d773524c | ||
|
|
6b396f8297 | ||
|
|
6b9365e71b | ||
|
|
be6ec050b3 | ||
|
|
5782bb8743 | ||
|
|
159516b1d7 | ||
|
|
398e477738 | ||
|
|
db142b7c65 | ||
|
|
4533f186c0 | ||
|
|
b52741583c | ||
|
|
561492b199 | ||
|
|
a9d0a01a85 | ||
|
|
78bf9f4e27 | ||
|
|
d71ddc06db | ||
|
|
9d207456af | ||
|
|
00fdfcc4c7 | ||
|
|
2a1661672c | ||
|
|
7ca65b7452 | ||
|
|
ad9bf02300 | ||
|
|
e02f30f895 | ||
|
|
25e2eb7d7c | ||
|
|
280a9afda8 | ||
|
|
e91618586c | ||
|
|
680ddf64be | ||
|
|
f4e7085c34 | ||
|
|
439a1fd4ed | ||
|
|
49eb326ce9 | ||
|
|
f03f32267e | ||
|
|
45b2e6db5c | ||
|
|
400a1c69b1 | ||
|
|
730eb1086a | ||
|
|
4f9e9c6619 | ||
|
|
953edfd1df | ||
|
|
230b8274b9 | ||
|
|
40866ddc10 | ||
|
|
bd56c0abf7 |
@@ -21,6 +21,7 @@ src = [
|
||||
'src/mouse_inject.c',
|
||||
'src/opengl.c',
|
||||
'src/options.c',
|
||||
'src/packet_merger.c',
|
||||
'src/receiver.c',
|
||||
'src/recorder.c',
|
||||
'src/scrcpy.c',
|
||||
@@ -200,10 +201,6 @@ conf.set('PORTABLE', get_option('portable'))
|
||||
conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183')
|
||||
conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
|
||||
|
||||
# the default video bitrate, in bits/second
|
||||
# overridden by option --bit-rate
|
||||
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
|
||||
|
||||
# run a server debugger and wait for a client to be attached
|
||||
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
|
||||
|
||||
|
||||
16
app/scrcpy.1
16
app/scrcpy.1
@@ -19,16 +19,30 @@ provides display and control of Android devices connected on USB (or over TCP/IP
|
||||
.B \-\-always\-on\-top
|
||||
Make scrcpy window always on top (above other windows).
|
||||
|
||||
.TP
|
||||
.BI "\-\-audio\-bit\-rate " value
|
||||
Encode the audio at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
||||
|
||||
Default is 196K (196000).
|
||||
|
||||
.TP
|
||||
.BI "\-\-audio\-codec " name
|
||||
Select an audio codec (opus or aac).
|
||||
|
||||
Default is opus.
|
||||
|
||||
.TP
|
||||
.BI "\-b, \-\-bit\-rate " value
|
||||
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
||||
|
||||
Default is 8000000.
|
||||
Default is 8M (8000000).
|
||||
|
||||
.TP
|
||||
.BI "\-\-codec " name
|
||||
Select a video codec (h264, h265 or av1).
|
||||
|
||||
Default is h264.
|
||||
|
||||
.TP
|
||||
.BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...]
|
||||
Set a list of comma-separated key:type=value options for the device encoder.
|
||||
|
||||
@@ -59,6 +59,8 @@
|
||||
#define OPT_NO_POWER_ON 1039
|
||||
#define OPT_CODEC 1040
|
||||
#define OPT_NO_AUDIO 1041
|
||||
#define OPT_AUDIO_BIT_RATE 1042
|
||||
#define OPT_AUDIO_CODEC 1043
|
||||
|
||||
struct sc_option {
|
||||
char shortopt;
|
||||
@@ -99,19 +101,35 @@ static const struct sc_option options[] = {
|
||||
.longopt = "always-on-top",
|
||||
.text = "Make scrcpy window always on top (above other windows).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_AUDIO_BIT_RATE,
|
||||
.longopt = "audio-bit-rate",
|
||||
.argdesc = "value",
|
||||
.text = "Encode the audio at the given bit-rate, expressed in bits/s. "
|
||||
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||
"Default is 196K (196000).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_AUDIO_CODEC,
|
||||
.longopt = "audio-codec",
|
||||
.argdesc = "name",
|
||||
.text = "Select an audio codec (opus or aac).\n"
|
||||
"Default is opus.",
|
||||
},
|
||||
{
|
||||
.shortopt = 'b',
|
||||
.longopt = "bit-rate",
|
||||
.argdesc = "value",
|
||||
.text = "Encode the video at the gitven bit-rate, expressed in bits/s. "
|
||||
.text = "Encode the video at the given bit-rate, expressed in bits/s. "
|
||||
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||
"Default is " STR(DEFAULT_BIT_RATE) ".",
|
||||
"Default is 8M (8000000).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CODEC,
|
||||
.longopt = "codec",
|
||||
.argdesc = "name",
|
||||
.text = "Select a video codec (h264, h265 or av1).",
|
||||
.text = "Select a video codec (h264, h265 or av1).\n"
|
||||
"Default is h264.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CODEC_OPTIONS,
|
||||
@@ -1408,6 +1426,20 @@ parse_codec(const char *optarg, enum sc_codec *codec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_audio_codec(const char *optarg, enum sc_codec *codec) {
|
||||
if (!strcmp(optarg, "opus")) {
|
||||
*codec = SC_CODEC_OPUS;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(optarg, "aac")) {
|
||||
*codec = SC_CODEC_AAC;
|
||||
return true;
|
||||
}
|
||||
LOGE("Unsupported audio codec: %s (expected opus)", optarg);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
const char *optstring, const struct option *longopts) {
|
||||
@@ -1423,6 +1455,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_AUDIO_BIT_RATE:
|
||||
if (!parse_bit_rate(optarg, &opts->audio_bit_rate)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_CROP:
|
||||
opts->crop = optarg;
|
||||
break;
|
||||
@@ -1649,6 +1686,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_AUDIO_CODEC:
|
||||
if (!parse_audio_codec(optarg, &opts->audio_codec)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_OTG:
|
||||
#ifdef HAVE_USB
|
||||
opts->otg = true;
|
||||
|
||||
@@ -9,20 +9,20 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
|
||||
struct sc_acksync *acksync) {
|
||||
cbuf_init(&controller->queue);
|
||||
|
||||
bool ok = receiver_init(&controller->receiver, control_socket, acksync);
|
||||
bool ok = sc_receiver_init(&controller->receiver, control_socket, acksync);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_mutex_init(&controller->mutex);
|
||||
if (!ok) {
|
||||
receiver_destroy(&controller->receiver);
|
||||
sc_receiver_destroy(&controller->receiver);
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&controller->msg_cond);
|
||||
if (!ok) {
|
||||
receiver_destroy(&controller->receiver);
|
||||
sc_receiver_destroy(&controller->receiver);
|
||||
sc_mutex_destroy(&controller->mutex);
|
||||
return false;
|
||||
}
|
||||
@@ -43,7 +43,7 @@ sc_controller_destroy(struct sc_controller *controller) {
|
||||
sc_control_msg_destroy(&msg);
|
||||
}
|
||||
|
||||
receiver_destroy(&controller->receiver);
|
||||
sc_receiver_destroy(&controller->receiver);
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -117,7 +117,7 @@ sc_controller_start(struct sc_controller *controller) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!receiver_start(&controller->receiver)) {
|
||||
if (!sc_receiver_start(&controller->receiver)) {
|
||||
sc_controller_stop(controller);
|
||||
sc_thread_join(&controller->thread, NULL);
|
||||
return false;
|
||||
@@ -137,5 +137,5 @@ sc_controller_stop(struct sc_controller *controller) {
|
||||
void
|
||||
sc_controller_join(struct sc_controller *controller) {
|
||||
sc_thread_join(&controller->thread, NULL);
|
||||
receiver_join(&controller->receiver);
|
||||
sc_receiver_join(&controller->receiver);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ struct sc_controller {
|
||||
sc_cond msg_cond;
|
||||
bool stopped;
|
||||
struct sc_control_msg_queue queue;
|
||||
struct receiver receiver;
|
||||
struct sc_receiver receiver;
|
||||
};
|
||||
|
||||
bool
|
||||
|
||||
@@ -29,7 +29,6 @@ sc_decoder_open_sinks(struct sc_decoder *decoder) {
|
||||
for (unsigned i = 0; i < decoder->sink_count; ++i) {
|
||||
struct sc_frame_sink *sink = decoder->sinks[i];
|
||||
if (!sink->ops->open(sink)) {
|
||||
LOGE("Could not open frame sink %d", i);
|
||||
sc_decoder_close_first_sinks(decoder, i);
|
||||
return false;
|
||||
}
|
||||
@@ -63,7 +62,6 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) {
|
||||
}
|
||||
|
||||
if (!sc_decoder_open_sinks(decoder)) {
|
||||
LOGE("Could not open decoder sinks");
|
||||
av_frame_free(&decoder->frame);
|
||||
avcodec_close(decoder->codec_ctx);
|
||||
avcodec_free_context(&decoder->codec_ctx);
|
||||
@@ -86,7 +84,6 @@ push_frame_to_sinks(struct sc_decoder *decoder, const AVFrame *frame) {
|
||||
for (unsigned i = 0; i < decoder->sink_count; ++i) {
|
||||
struct sc_frame_sink *sink = decoder->sinks[i];
|
||||
if (!sink->ops->push(sink, frame)) {
|
||||
LOGE("Could not send frame to sink %d", i);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "decoder.h"
|
||||
#include "events.h"
|
||||
#include "packet_merger.h"
|
||||
#include "recorder.h"
|
||||
#include "util/binary.h"
|
||||
#include "util/log.h"
|
||||
@@ -18,17 +19,12 @@
|
||||
#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1)
|
||||
|
||||
static enum AVCodecID
|
||||
sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer) {
|
||||
uint8_t data[4];
|
||||
ssize_t r = net_recv_all(demuxer->socket, data, 4);
|
||||
if (r < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sc_demuxer_to_avcodec_id(uint32_t codec_id) {
|
||||
#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_AV1 UINT32_C(0x00617631) // "av1" in ASCII
|
||||
uint32_t codec_id = sc_read32be(data);
|
||||
#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII
|
||||
#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII"
|
||||
switch (codec_id) {
|
||||
case SC_CODEC_ID_H264:
|
||||
return AV_CODEC_ID_H264;
|
||||
@@ -36,12 +32,28 @@ sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer) {
|
||||
return AV_CODEC_ID_HEVC;
|
||||
case SC_CODEC_ID_AV1:
|
||||
return AV_CODEC_ID_AV1;
|
||||
case SC_CODEC_ID_OPUS:
|
||||
return AV_CODEC_ID_OPUS;
|
||||
case SC_CODEC_ID_AAC:
|
||||
return AV_CODEC_ID_AAC;
|
||||
default:
|
||||
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
|
||||
return AV_CODEC_ID_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) {
|
||||
uint8_t data[4];
|
||||
ssize_t r = net_recv_all(demuxer->socket, data, 4);
|
||||
if (r < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*codec_id = sc_read32be(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
||||
// The video stream contains raw packets, without time information. When we
|
||||
@@ -105,7 +117,6 @@ push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) {
|
||||
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
|
||||
struct sc_packet_sink *sink = demuxer->sinks[i];
|
||||
if (!sink->ops->push(sink, packet)) {
|
||||
LOGE("Could not send config packet to sink %d", i);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -115,50 +126,9 @@ push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) {
|
||||
|
||||
static bool
|
||||
sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
||||
bool is_config = packet->pts == AV_NOPTS_VALUE;
|
||||
|
||||
// A config packet must not be decoded immediately (it contains no
|
||||
// frame); instead, it must be concatenated with the future data packet.
|
||||
if (demuxer->pending || is_config) {
|
||||
if (demuxer->pending) {
|
||||
size_t offset = demuxer->pending->size;
|
||||
if (av_grow_packet(demuxer->pending, packet->size)) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(demuxer->pending->data + offset, packet->data, packet->size);
|
||||
} else {
|
||||
demuxer->pending = av_packet_alloc();
|
||||
if (!demuxer->pending) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
if (av_packet_ref(demuxer->pending, packet)) {
|
||||
LOG_OOM();
|
||||
av_packet_free(&demuxer->pending);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_config) {
|
||||
// prepare the concat packet to send to the decoder
|
||||
demuxer->pending->pts = packet->pts;
|
||||
demuxer->pending->dts = packet->dts;
|
||||
demuxer->pending->flags = packet->flags;
|
||||
packet = demuxer->pending;
|
||||
}
|
||||
}
|
||||
|
||||
bool ok = push_packet_to_sinks(demuxer, packet);
|
||||
|
||||
if (!is_config && demuxer->pending) {
|
||||
// the pending packet must be discarded (consumed or error)
|
||||
av_packet_free(&demuxer->pending);
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
LOGE("Could not process packet");
|
||||
LOGE("Demuxer '%s': could not process packet", demuxer->name);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -183,7 +153,6 @@ sc_demuxer_open_sinks(struct sc_demuxer *demuxer, const AVCodec *codec) {
|
||||
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
|
||||
struct sc_packet_sink *sink = demuxer->sinks[i];
|
||||
if (!sink->ops->open(sink, codec)) {
|
||||
LOGE("Could not open packet sink %d", i);
|
||||
sc_demuxer_close_first_sinks(demuxer, i);
|
||||
return false;
|
||||
}
|
||||
@@ -192,56 +161,93 @@ sc_demuxer_open_sinks(struct sc_demuxer *demuxer, const AVCodec *codec) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_demuxer_disable_sinks(struct sc_demuxer *demuxer) {
|
||||
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
|
||||
struct sc_packet_sink *sink = demuxer->sinks[i];
|
||||
if (sink->ops->disable) {
|
||||
sink->ops->disable(sink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
run_demuxer(void *data) {
|
||||
struct sc_demuxer *demuxer = data;
|
||||
|
||||
enum AVCodecID codec_id = sc_demuxer_recv_codec_id(demuxer);
|
||||
// Flag to report end-of-stream (i.e. device disconnected)
|
||||
bool eos = false;
|
||||
|
||||
uint32_t raw_codec_id;
|
||||
bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id);
|
||||
if (!ok) {
|
||||
LOGE("Demuxer '%s': stream disabled due to connection error",
|
||||
demuxer->name);
|
||||
eos = true;
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (raw_codec_id == 0) {
|
||||
LOGW("Demuxer '%s': stream explicitly disabled by the device",
|
||||
demuxer->name);
|
||||
sc_demuxer_disable_sinks(demuxer);
|
||||
eos = true;
|
||||
goto end;
|
||||
}
|
||||
|
||||
enum AVCodecID codec_id = sc_demuxer_to_avcodec_id(raw_codec_id);
|
||||
if (codec_id == AV_CODEC_ID_NONE) {
|
||||
// Error already logged
|
||||
LOGE("Demuxer '%s': stream disabled due to unsupported codec",
|
||||
demuxer->name);
|
||||
sc_demuxer_disable_sinks(demuxer);
|
||||
goto end;
|
||||
}
|
||||
|
||||
const AVCodec *codec = avcodec_find_decoder(codec_id);
|
||||
if (!codec) {
|
||||
LOGE("H.264 decoder not found");
|
||||
goto end;
|
||||
}
|
||||
|
||||
demuxer->codec_ctx = avcodec_alloc_context3(codec);
|
||||
if (!demuxer->codec_ctx) {
|
||||
LOG_OOM();
|
||||
LOGE("Demuxer '%s': stream disabled due to missing decoder",
|
||||
demuxer->name);
|
||||
sc_demuxer_disable_sinks(demuxer);
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (!sc_demuxer_open_sinks(demuxer, codec)) {
|
||||
LOGE("Could not open demuxer sinks");
|
||||
goto finally_free_codec_ctx;
|
||||
goto end;
|
||||
}
|
||||
|
||||
demuxer->parser = av_parser_init(codec_id);
|
||||
if (!demuxer->parser) {
|
||||
LOGE("Could not initialize parser");
|
||||
goto finally_close_sinks;
|
||||
}
|
||||
// Config packets must be merged with the next non-config packet only for
|
||||
// video streams
|
||||
bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO;
|
||||
|
||||
// We must only pass complete frames to av_parser_parse2()!
|
||||
// It's more complicated, but this allows to reduce the latency by 1 frame!
|
||||
demuxer->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
|
||||
struct sc_packet_merger merger;
|
||||
|
||||
if (must_merge_config_packet) {
|
||||
sc_packet_merger_init(&merger);
|
||||
}
|
||||
|
||||
AVPacket *packet = av_packet_alloc();
|
||||
if (!packet) {
|
||||
LOG_OOM();
|
||||
goto finally_close_parser;
|
||||
goto finally_close_sinks;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
bool ok = sc_demuxer_recv_packet(demuxer, packet);
|
||||
if (!ok) {
|
||||
// end of stream
|
||||
eos = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (must_merge_config_packet) {
|
||||
// Prepend any config packet to the next media packet
|
||||
ok = sc_packet_merger_merge(&merger, packet);
|
||||
if (!ok) {
|
||||
av_packet_unref(packet);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ok = sc_demuxer_push_packet(demuxer, packet);
|
||||
av_packet_unref(packet);
|
||||
if (!ok) {
|
||||
@@ -250,35 +256,31 @@ run_demuxer(void *data) {
|
||||
}
|
||||
}
|
||||
|
||||
LOGD("End of frames");
|
||||
LOGD("Demuxer '%s': end of frames", demuxer->name);
|
||||
|
||||
if (demuxer->pending) {
|
||||
av_packet_free(&demuxer->pending);
|
||||
if (must_merge_config_packet) {
|
||||
sc_packet_merger_destroy(&merger);
|
||||
}
|
||||
|
||||
av_packet_free(&packet);
|
||||
finally_close_parser:
|
||||
av_parser_close(demuxer->parser);
|
||||
finally_close_sinks:
|
||||
sc_demuxer_close_sinks(demuxer);
|
||||
finally_free_codec_ctx:
|
||||
avcodec_free_context(&demuxer->codec_ctx);
|
||||
end:
|
||||
demuxer->cbs->on_eos(demuxer, demuxer->cbs_userdata);
|
||||
demuxer->cbs->on_ended(demuxer, eos, demuxer->cbs_userdata);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) {
|
||||
assert(socket != SC_SOCKET_NONE);
|
||||
|
||||
demuxer->name = name; // statically allocated
|
||||
demuxer->socket = socket;
|
||||
demuxer->pending = NULL;
|
||||
demuxer->sink_count = 0;
|
||||
|
||||
assert(cbs && cbs->on_eos);
|
||||
assert(cbs && cbs->on_ended);
|
||||
|
||||
demuxer->cbs = cbs;
|
||||
demuxer->cbs_userdata = cbs_userdata;
|
||||
@@ -294,12 +296,12 @@ sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink) {
|
||||
|
||||
bool
|
||||
sc_demuxer_start(struct sc_demuxer *demuxer) {
|
||||
LOGD("Starting demuxer thread");
|
||||
LOGD("Demuxer '%s': starting thread", demuxer->name);
|
||||
|
||||
bool ok = sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer",
|
||||
demuxer);
|
||||
if (!ok) {
|
||||
LOGE("Could not start demuxer thread");
|
||||
LOGE("Demuxer '%s': could not start thread", demuxer->name);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -15,28 +15,25 @@
|
||||
#define SC_DEMUXER_MAX_SINKS 2
|
||||
|
||||
struct sc_demuxer {
|
||||
const char *name; // must be statically allocated (e.g. a string literal)
|
||||
|
||||
sc_socket socket;
|
||||
sc_thread thread;
|
||||
|
||||
struct sc_packet_sink *sinks[SC_DEMUXER_MAX_SINKS];
|
||||
unsigned sink_count;
|
||||
|
||||
AVCodecContext *codec_ctx;
|
||||
AVCodecParserContext *parser;
|
||||
// successive packets may need to be concatenated, until a non-config
|
||||
// packet is available
|
||||
AVPacket *pending;
|
||||
|
||||
const struct sc_demuxer_callbacks *cbs;
|
||||
void *cbs_userdata;
|
||||
};
|
||||
|
||||
struct sc_demuxer_callbacks {
|
||||
void (*on_eos)(struct sc_demuxer *demuxer, void *userdata);
|
||||
void (*on_ended)(struct sc_demuxer *demuxer, bool eos, void *userdata);
|
||||
};
|
||||
|
||||
// The name must be statically allocated (e.g. a string literal)
|
||||
void
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata);
|
||||
|
||||
void
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#define EVENT_NEW_FRAME SDL_USEREVENT
|
||||
#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1)
|
||||
#define EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2)
|
||||
#define EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3)
|
||||
#define EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
|
||||
#define SC_EVENT_NEW_FRAME SDL_USEREVENT
|
||||
#define SC_EVENT_DEVICE_DISCONNECTED (SDL_USEREVENT + 1)
|
||||
#define SC_EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2)
|
||||
#define SC_EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3)
|
||||
#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
|
||||
#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5)
|
||||
#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6)
|
||||
|
||||
@@ -14,6 +14,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
#endif
|
||||
.log_level = SC_LOG_LEVEL_INFO,
|
||||
.codec = SC_CODEC_H264,
|
||||
.audio_codec = SC_CODEC_OPUS,
|
||||
.record_format = SC_RECORD_FORMAT_AUTO,
|
||||
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT,
|
||||
.port_range = {
|
||||
@@ -27,7 +28,8 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.count = 2,
|
||||
},
|
||||
.max_size = 0,
|
||||
.bit_rate = DEFAULT_BIT_RATE,
|
||||
.bit_rate = 0,
|
||||
.audio_bit_rate = 0,
|
||||
.max_fps = 0,
|
||||
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
|
||||
.rotation = 0,
|
||||
|
||||
@@ -27,6 +27,8 @@ enum sc_codec {
|
||||
SC_CODEC_H264,
|
||||
SC_CODEC_H265,
|
||||
SC_CODEC_AV1,
|
||||
SC_CODEC_OPUS,
|
||||
SC_CODEC_AAC,
|
||||
};
|
||||
|
||||
enum sc_lock_video_orientation {
|
||||
@@ -100,6 +102,7 @@ struct scrcpy_options {
|
||||
#endif
|
||||
enum sc_log_level log_level;
|
||||
enum sc_codec codec;
|
||||
enum sc_codec audio_codec;
|
||||
enum sc_record_format record_format;
|
||||
enum sc_keyboard_input_mode keyboard_input_mode;
|
||||
enum sc_mouse_input_mode mouse_input_mode;
|
||||
@@ -109,6 +112,7 @@ struct scrcpy_options {
|
||||
struct sc_shortcut_mods shortcut_mods;
|
||||
uint16_t max_size;
|
||||
uint32_t bit_rate;
|
||||
uint32_t audio_bit_rate;
|
||||
uint16_t max_fps;
|
||||
enum sc_lock_video_orientation lock_video_orientation;
|
||||
uint8_t rotation;
|
||||
|
||||
48
app/src/packet_merger.c
Normal file
48
app/src/packet_merger.c
Normal file
@@ -0,0 +1,48 @@
|
||||
#include "packet_merger.h"
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
void
|
||||
sc_packet_merger_init(struct sc_packet_merger *merger) {
|
||||
merger->config = NULL;
|
||||
}
|
||||
|
||||
void
|
||||
sc_packet_merger_destroy(struct sc_packet_merger *merger) {
|
||||
free(merger->config);
|
||||
}
|
||||
|
||||
bool
|
||||
sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet) {
|
||||
bool is_config = packet->pts == AV_NOPTS_VALUE;
|
||||
|
||||
if (is_config) {
|
||||
free(merger->config);
|
||||
|
||||
merger->config = malloc(packet->size);
|
||||
if (!merger->config) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(merger->config, packet->data, packet->size);
|
||||
merger->config_size = packet->size;
|
||||
} else if (merger->config) {
|
||||
size_t config_size = merger->config_size;
|
||||
size_t media_size = packet->size;
|
||||
|
||||
if (av_grow_packet(packet, config_size)) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
memmove(packet->data + config_size, packet->data, media_size);
|
||||
memcpy(packet->data, merger->config, config_size);
|
||||
|
||||
free(merger->config);
|
||||
merger->config = NULL;
|
||||
// merger->size is meaningless when merger->config is NULL
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
43
app/src/packet_merger.h
Normal file
43
app/src/packet_merger.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#ifndef SC_PACKET_MERGER_H
|
||||
#define SC_PACKET_MERGER_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
|
||||
/**
|
||||
* Config packets (containing the SPS/PPS) are sent in-band. A new config
|
||||
* packet is sent whenever a new encoding session is started (on start and on
|
||||
* device orientation change).
|
||||
*
|
||||
* Every time a config packet is received, it must be sent alone (for recorder
|
||||
* extradata), then concatenated to the next media packet (for correct decoding
|
||||
* and recording).
|
||||
*
|
||||
* This helper reads every input packet and modifies each media packet which
|
||||
* immediately follows a config packet to prepend the config packet payload.
|
||||
*/
|
||||
|
||||
struct sc_packet_merger {
|
||||
uint8_t *config;
|
||||
size_t config_size;
|
||||
};
|
||||
|
||||
void
|
||||
sc_packet_merger_init(struct sc_packet_merger *merger);
|
||||
|
||||
void
|
||||
sc_packet_merger_destroy(struct sc_packet_merger *merger);
|
||||
|
||||
/**
|
||||
* If the packet is a config packet, then keep its data for later.
|
||||
* Otherwise (if the packet is a media packet), then if a config packet is
|
||||
* pending, prepend the config packet to this packet (so the packet is
|
||||
* modified!).
|
||||
*/
|
||||
bool
|
||||
sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet);
|
||||
|
||||
#endif
|
||||
@@ -7,7 +7,7 @@
|
||||
#include "util/log.h"
|
||||
|
||||
bool
|
||||
receiver_init(struct receiver *receiver, sc_socket control_socket,
|
||||
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
|
||||
struct sc_acksync *acksync) {
|
||||
bool ok = sc_mutex_init(&receiver->mutex);
|
||||
if (!ok) {
|
||||
@@ -21,12 +21,12 @@ receiver_init(struct receiver *receiver, sc_socket control_socket,
|
||||
}
|
||||
|
||||
void
|
||||
receiver_destroy(struct receiver *receiver) {
|
||||
sc_receiver_destroy(struct sc_receiver *receiver) {
|
||||
sc_mutex_destroy(&receiver->mutex);
|
||||
}
|
||||
|
||||
static void
|
||||
process_msg(struct receiver *receiver, struct device_msg *msg) {
|
||||
process_msg(struct sc_receiver *receiver, struct device_msg *msg) {
|
||||
switch (msg->type) {
|
||||
case DEVICE_MSG_TYPE_CLIPBOARD: {
|
||||
char *current = SDL_GetClipboardText();
|
||||
@@ -51,7 +51,7 @@ process_msg(struct receiver *receiver, struct device_msg *msg) {
|
||||
}
|
||||
|
||||
static ssize_t
|
||||
process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) {
|
||||
process_msgs(struct sc_receiver *receiver, const unsigned char *buf, size_t len) {
|
||||
size_t head = 0;
|
||||
for (;;) {
|
||||
struct device_msg msg;
|
||||
@@ -76,7 +76,7 @@ process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) {
|
||||
|
||||
static int
|
||||
run_receiver(void *data) {
|
||||
struct receiver *receiver = data;
|
||||
struct sc_receiver *receiver = data;
|
||||
|
||||
static unsigned char buf[DEVICE_MSG_MAX_SIZE];
|
||||
size_t head = 0;
|
||||
@@ -108,7 +108,7 @@ run_receiver(void *data) {
|
||||
}
|
||||
|
||||
bool
|
||||
receiver_start(struct receiver *receiver) {
|
||||
sc_receiver_start(struct sc_receiver *receiver) {
|
||||
LOGD("Starting receiver thread");
|
||||
|
||||
bool ok = sc_thread_create(&receiver->thread, run_receiver,
|
||||
@@ -122,6 +122,6 @@ receiver_start(struct receiver *receiver) {
|
||||
}
|
||||
|
||||
void
|
||||
receiver_join(struct receiver *receiver) {
|
||||
sc_receiver_join(struct sc_receiver *receiver) {
|
||||
sc_thread_join(&receiver->thread, NULL);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
// receive events from the device
|
||||
// managed by the controller
|
||||
struct receiver {
|
||||
struct sc_receiver {
|
||||
sc_socket control_socket;
|
||||
sc_thread thread;
|
||||
sc_mutex mutex;
|
||||
@@ -20,18 +20,18 @@ struct receiver {
|
||||
};
|
||||
|
||||
bool
|
||||
receiver_init(struct receiver *receiver, sc_socket control_socket,
|
||||
struct sc_acksync *acksync);
|
||||
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
|
||||
struct sc_acksync *acksync);
|
||||
|
||||
void
|
||||
receiver_destroy(struct receiver *receiver);
|
||||
sc_receiver_destroy(struct sc_receiver *receiver);
|
||||
|
||||
bool
|
||||
receiver_start(struct receiver *receiver);
|
||||
sc_receiver_start(struct sc_receiver *receiver);
|
||||
|
||||
// no receiver_stop(), it will automatically stop on control_socket shutdown
|
||||
// no sc_receiver_stop(), it will automatically stop on control_socket shutdown
|
||||
|
||||
void
|
||||
receiver_join(struct receiver *receiver);
|
||||
sc_receiver_join(struct sc_receiver *receiver);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
#include "util/log.h"
|
||||
#include "util/str.h"
|
||||
|
||||
/** Downcast packet_sink to recorder */
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink)
|
||||
|
||||
#define SC_PTS_ORIGIN_NONE UINT64_C(-1)
|
||||
/** Downcast packet sinks to recorder */
|
||||
#define DOWNCAST_VIDEO(SINK) \
|
||||
container_of(SINK, struct sc_recorder, video_packet_sink)
|
||||
#define DOWNCAST_AUDIO(SINK) \
|
||||
container_of(SINK, struct sc_recorder, audio_packet_sink)
|
||||
|
||||
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
|
||||
|
||||
@@ -80,9 +81,7 @@ sc_recorder_get_format_name(enum sc_record_format format) {
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_write_header(struct sc_recorder *recorder, const AVPacket *packet) {
|
||||
AVStream *ostream = recorder->ctx->streams[0];
|
||||
|
||||
sc_recorder_set_extradata(AVStream *ostream, const AVPacket *packet) {
|
||||
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
|
||||
if (!extradata) {
|
||||
LOG_OOM();
|
||||
@@ -94,183 +93,56 @@ sc_recorder_write_header(struct sc_recorder *recorder, const AVPacket *packet) {
|
||||
|
||||
ostream->codecpar->extradata = extradata;
|
||||
ostream->codecpar->extradata_size = packet->size;
|
||||
|
||||
int ret = avformat_write_header(recorder->ctx, NULL);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to write header to %s", recorder->filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_rescale_packet(struct sc_recorder *recorder, AVPacket *packet) {
|
||||
AVStream *ostream = recorder->ctx->streams[0];
|
||||
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
|
||||
static inline void
|
||||
sc_recorder_rescale_packet(AVStream *stream, AVPacket *packet) {
|
||||
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, stream->time_base);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_write(struct sc_recorder *recorder, AVPacket *packet) {
|
||||
if (!recorder->header_written) {
|
||||
if (packet->pts != AV_NOPTS_VALUE) {
|
||||
LOGE("The first packet is not a config packet");
|
||||
return false;
|
||||
}
|
||||
bool ok = sc_recorder_write_header(recorder, packet);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
recorder->header_written = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (packet->pts == AV_NOPTS_VALUE) {
|
||||
// ignore config packets
|
||||
return true;
|
||||
}
|
||||
|
||||
sc_recorder_rescale_packet(recorder, packet);
|
||||
return av_write_frame(recorder->ctx, packet) >= 0;
|
||||
sc_recorder_write_stream(struct sc_recorder *recorder, int stream_index,
|
||||
AVPacket *packet) {
|
||||
AVStream *stream = recorder->ctx->streams[stream_index];
|
||||
sc_recorder_rescale_packet(stream, packet);
|
||||
return av_interleaved_write_frame(recorder->ctx, packet) >= 0;
|
||||
}
|
||||
|
||||
static int
|
||||
run_recorder(void *data) {
|
||||
struct sc_recorder *recorder = data;
|
||||
static inline bool
|
||||
sc_recorder_write_video(struct sc_recorder *recorder, AVPacket *packet) {
|
||||
return sc_recorder_write_stream(recorder, recorder->video_stream_index,
|
||||
packet);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
while (!recorder->stopped && sc_queue_is_empty(&recorder->queue)) {
|
||||
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
|
||||
}
|
||||
|
||||
// if stopped is set, continue to process the remaining events (to
|
||||
// finish the recording) before actually stopping
|
||||
|
||||
if (recorder->stopped && sc_queue_is_empty(&recorder->queue)) {
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
struct sc_record_packet *last = recorder->previous;
|
||||
if (last) {
|
||||
// assign an arbitrary duration to the last packet
|
||||
last->packet->duration = 100000;
|
||||
bool ok = sc_recorder_write(recorder, last->packet);
|
||||
if (!ok) {
|
||||
// failing to write the last frame is not very serious, no
|
||||
// future frame may depend on it, so the resulting file
|
||||
// will still be valid
|
||||
LOGW("Could not record last packet");
|
||||
}
|
||||
sc_record_packet_delete(last);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
struct sc_record_packet *rec;
|
||||
sc_queue_take(&recorder->queue, next, &rec);
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
if (recorder->pts_origin == SC_PTS_ORIGIN_NONE
|
||||
&& rec->packet->pts != AV_NOPTS_VALUE) {
|
||||
// First PTS received
|
||||
recorder->pts_origin = rec->packet->pts;
|
||||
}
|
||||
|
||||
if (rec->packet->pts != AV_NOPTS_VALUE) {
|
||||
// Set PTS relatve to the origin
|
||||
rec->packet->pts -= recorder->pts_origin;
|
||||
rec->packet->dts = rec->packet->pts;
|
||||
}
|
||||
|
||||
// recorder->previous is only written from this thread, no need to lock
|
||||
struct sc_record_packet *previous = recorder->previous;
|
||||
recorder->previous = rec;
|
||||
|
||||
if (!previous) {
|
||||
// we just received the first packet
|
||||
continue;
|
||||
}
|
||||
|
||||
// config packets have no PTS, we must ignore them
|
||||
if (rec->packet->pts != AV_NOPTS_VALUE
|
||||
&& previous->packet->pts != AV_NOPTS_VALUE) {
|
||||
// we now know the duration of the previous packet
|
||||
previous->packet->duration =
|
||||
rec->packet->pts - previous->packet->pts;
|
||||
}
|
||||
|
||||
bool ok = sc_recorder_write(recorder, previous->packet);
|
||||
sc_record_packet_delete(previous);
|
||||
if (!ok) {
|
||||
LOGE("Could not record packet");
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
recorder->failed = true;
|
||||
// discard pending packets
|
||||
sc_recorder_queue_clear(&recorder->queue);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!recorder->failed) {
|
||||
if (recorder->header_written) {
|
||||
int ret = av_write_trailer(recorder->ctx);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to write trailer to %s", recorder->filename);
|
||||
recorder->failed = true;
|
||||
}
|
||||
} else {
|
||||
// the recorded file is empty
|
||||
recorder->failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (recorder->failed) {
|
||||
LOGE("Recording failed to %s", recorder->filename);
|
||||
} else {
|
||||
const char *format_name = sc_recorder_get_format_name(recorder->format);
|
||||
LOGI("Recording complete to %s file: %s", format_name,
|
||||
recorder->filename);
|
||||
}
|
||||
|
||||
LOGD("Recorder thread ended");
|
||||
|
||||
return 0;
|
||||
static inline bool
|
||||
sc_recorder_write_audio(struct sc_recorder *recorder, AVPacket *packet) {
|
||||
return sc_recorder_write_stream(recorder, recorder->audio_stream_index,
|
||||
packet);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) {
|
||||
bool ok = sc_mutex_init(&recorder->mutex);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&recorder->queue_cond);
|
||||
if (!ok) {
|
||||
goto error_mutex_destroy;
|
||||
}
|
||||
|
||||
sc_queue_init(&recorder->queue);
|
||||
recorder->stopped = false;
|
||||
recorder->failed = false;
|
||||
recorder->header_written = false;
|
||||
recorder->previous = NULL;
|
||||
recorder->pts_origin = SC_PTS_ORIGIN_NONE;
|
||||
|
||||
sc_recorder_open_output_file(struct sc_recorder *recorder) {
|
||||
const char *format_name = sc_recorder_get_format_name(recorder->format);
|
||||
assert(format_name);
|
||||
const AVOutputFormat *format = find_muxer(format_name);
|
||||
if (!format) {
|
||||
LOGE("Could not find muxer");
|
||||
goto error_cond_destroy;
|
||||
return false;
|
||||
}
|
||||
|
||||
recorder->ctx = avformat_alloc_context();
|
||||
if (!recorder->ctx) {
|
||||
LOG_OOM();
|
||||
goto error_cond_destroy;
|
||||
return false;
|
||||
}
|
||||
|
||||
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
|
||||
AVIO_FLAG_WRITE);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to open output file: %s", recorder->filename);
|
||||
avformat_free_context(recorder->ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
|
||||
@@ -282,71 +154,436 @@ sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) {
|
||||
av_dict_set(&recorder->ctx->metadata, "comment",
|
||||
"Recorded by scrcpy " SCRCPY_VERSION, 0);
|
||||
|
||||
AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec);
|
||||
if (!ostream) {
|
||||
goto error_avformat_free_context;
|
||||
}
|
||||
|
||||
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||
ostream->codecpar->codec_id = input_codec->id;
|
||||
ostream->codecpar->format = AV_PIX_FMT_YUV420P;
|
||||
ostream->codecpar->width = recorder->declared_frame_size.width;
|
||||
ostream->codecpar->height = recorder->declared_frame_size.height;
|
||||
|
||||
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
|
||||
AVIO_FLAG_WRITE);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to open output file: %s", recorder->filename);
|
||||
// ostream will be cleaned up during context cleaning
|
||||
goto error_avformat_free_context;
|
||||
}
|
||||
|
||||
LOGD("Starting recorder thread");
|
||||
ok = sc_thread_create(&recorder->thread, run_recorder, "scrcpy-recorder",
|
||||
recorder);
|
||||
if (!ok) {
|
||||
LOGE("Could not start recorder thread");
|
||||
goto error_avio_close;
|
||||
}
|
||||
|
||||
LOGI("Recording started to %s file: %s", format_name, recorder->filename);
|
||||
|
||||
return true;
|
||||
|
||||
error_avio_close:
|
||||
avio_close(recorder->ctx->pb);
|
||||
error_avformat_free_context:
|
||||
avformat_free_context(recorder->ctx);
|
||||
error_cond_destroy:
|
||||
sc_cond_destroy(&recorder->queue_cond);
|
||||
error_mutex_destroy:
|
||||
sc_mutex_destroy(&recorder->mutex);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_close(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
recorder->stopped = true;
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
sc_thread_join(&recorder->thread, NULL);
|
||||
|
||||
sc_recorder_close_output_file(struct sc_recorder *recorder) {
|
||||
avio_close(recorder->ctx->pb);
|
||||
avformat_free_context(recorder->ctx);
|
||||
sc_cond_destroy(&recorder->queue_cond);
|
||||
sc_mutex_destroy(&recorder->mutex);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_push(struct sc_recorder *recorder, const AVPacket *packet) {
|
||||
sc_recorder_wait_video_stream(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
assert(!recorder->stopped);
|
||||
while (!recorder->video_codec && !recorder->stopped) {
|
||||
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
|
||||
}
|
||||
const AVCodec *codec = recorder->video_codec;
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
if (recorder->failed) {
|
||||
// reject any new packet (this will stop the stream)
|
||||
if (codec) {
|
||||
AVStream *stream = avformat_new_stream(recorder->ctx, codec);
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||
stream->codecpar->codec_id = codec->id;
|
||||
stream->codecpar->format = AV_PIX_FMT_YUV420P;
|
||||
stream->codecpar->width = recorder->declared_frame_size.width;
|
||||
stream->codecpar->height = recorder->declared_frame_size.height;
|
||||
|
||||
recorder->video_stream_index = stream->index;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_wait_audio_stream(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
while (!recorder->audio_codec && !recorder->audio_disabled
|
||||
&& !recorder->stopped) {
|
||||
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
|
||||
}
|
||||
|
||||
if (recorder->audio_disabled) {
|
||||
// Reset audio flag. From there, the recorder thread may access this
|
||||
// flag without any mutex.
|
||||
recorder->audio = false;
|
||||
}
|
||||
|
||||
const AVCodec *codec = recorder->audio_codec;
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
if (codec) {
|
||||
AVStream *stream = avformat_new_stream(recorder->ctx, codec);
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
|
||||
stream->codecpar->codec_id = codec->id;
|
||||
stream->codecpar->ch_layout.nb_channels = 2;
|
||||
stream->codecpar->sample_rate = 48000;
|
||||
|
||||
recorder->audio_stream_index = stream->index;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
sc_recorder_has_empty_queues(struct sc_recorder *recorder) {
|
||||
if (sc_queue_is_empty(&recorder->video_queue)) {
|
||||
// The video queue is empty
|
||||
return true;
|
||||
}
|
||||
|
||||
if (recorder->audio && sc_queue_is_empty(&recorder->audio_queue)) {
|
||||
// The audio queue is empty (when audio is enabled)
|
||||
return true;
|
||||
}
|
||||
|
||||
// No queue is empty
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_process_header(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
while (!recorder->stopped && sc_recorder_has_empty_queues(recorder)) {
|
||||
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
|
||||
}
|
||||
|
||||
if (recorder->stopped && sc_queue_is_empty(&recorder->video_queue)) {
|
||||
// If the recorder is stopped, don't process anything if there are not
|
||||
// at least video packets
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sc_record_packet *video_pkt;
|
||||
sc_queue_take(&recorder->video_queue, next, &video_pkt);
|
||||
|
||||
struct sc_record_packet *audio_pkt = NULL;
|
||||
if (!sc_queue_is_empty(&recorder->audio_queue)) {
|
||||
assert(recorder->audio);
|
||||
sc_queue_take(&recorder->audio_queue, next, &audio_pkt);
|
||||
}
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
int ret = false;
|
||||
|
||||
if (video_pkt->packet->pts != AV_NOPTS_VALUE) {
|
||||
LOGE("The first video packet is not a config packet");
|
||||
goto end;
|
||||
}
|
||||
|
||||
assert(recorder->video_stream_index >= 0);
|
||||
AVStream *video_stream =
|
||||
recorder->ctx->streams[recorder->video_stream_index];
|
||||
bool ok = sc_recorder_set_extradata(video_stream, video_pkt->packet);
|
||||
if (!ok) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (audio_pkt) {
|
||||
if (audio_pkt->packet->pts != AV_NOPTS_VALUE) {
|
||||
LOGE("The first audio packet is not a config packet");
|
||||
goto end;
|
||||
}
|
||||
|
||||
assert(recorder->audio_stream_index >= 0);
|
||||
AVStream *audio_stream =
|
||||
recorder->ctx->streams[recorder->audio_stream_index];
|
||||
ok = sc_recorder_set_extradata(audio_stream, audio_pkt->packet);
|
||||
if (!ok) {
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
|
||||
ok = avformat_write_header(recorder->ctx, NULL) >= 0;
|
||||
if (!ok) {
|
||||
LOGE("Failed to write header to %s", recorder->filename);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ret = true;
|
||||
|
||||
end:
|
||||
sc_record_packet_delete(video_pkt);
|
||||
if (audio_pkt) {
|
||||
sc_record_packet_delete(audio_pkt);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_process_packets(struct sc_recorder *recorder) {
|
||||
int64_t pts_origin = AV_NOPTS_VALUE;
|
||||
|
||||
bool header_written = sc_recorder_process_header(recorder);
|
||||
if (!header_written) {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sc_record_packet *video_pkt = NULL;
|
||||
struct sc_record_packet *audio_pkt = NULL;
|
||||
|
||||
// We can write a video packet only once we received the next one so that
|
||||
// we can set its duration (next_pts - current_pts)
|
||||
struct sc_record_packet *video_pkt_previous = NULL;
|
||||
|
||||
bool error = false;
|
||||
|
||||
for (;;) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
while (!recorder->stopped) {
|
||||
if (!video_pkt && !sc_queue_is_empty(&recorder->video_queue)) {
|
||||
// A new packet may be assigned to video_pkt and be processed
|
||||
break;
|
||||
}
|
||||
if (recorder->audio && !audio_pkt
|
||||
&& !sc_queue_is_empty(&recorder->audio_queue)) {
|
||||
// A new packet may be assigned to audio_pkt and be processed
|
||||
break;
|
||||
}
|
||||
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
|
||||
}
|
||||
|
||||
// If stopped is set, continue to process the remaining events (to
|
||||
// finish the recording) before actually stopping.
|
||||
|
||||
// If there is no audio, then the audio_queue will remain empty forever
|
||||
// and audio_pkt will always be NULL.
|
||||
assert(recorder->audio
|
||||
|| (!audio_pkt && sc_queue_is_empty(&recorder->audio_queue)));
|
||||
|
||||
if (!video_pkt && !sc_queue_is_empty(&recorder->video_queue)) {
|
||||
sc_queue_take(&recorder->video_queue, next, &video_pkt);
|
||||
}
|
||||
|
||||
if (!audio_pkt && !sc_queue_is_empty(&recorder->audio_queue)) {
|
||||
sc_queue_take(&recorder->audio_queue, next, &audio_pkt);
|
||||
}
|
||||
|
||||
if (recorder->stopped && !video_pkt && !audio_pkt) {
|
||||
assert(sc_queue_is_empty(&recorder->video_queue));
|
||||
assert(sc_queue_is_empty(&recorder->audio_queue));
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
break;
|
||||
}
|
||||
|
||||
assert(video_pkt || audio_pkt); // at least one
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
// Ignore further config packets (e.g. on device orientation
|
||||
// change). The next non-config packet will have the config packet
|
||||
// data prepended.
|
||||
if (video_pkt && video_pkt->packet->pts == AV_NOPTS_VALUE) {
|
||||
sc_record_packet_delete(video_pkt);
|
||||
video_pkt = NULL;
|
||||
}
|
||||
|
||||
if (audio_pkt && audio_pkt->packet->pts == AV_NOPTS_VALUE) {
|
||||
sc_record_packet_delete(audio_pkt);
|
||||
audio_pkt= NULL;
|
||||
}
|
||||
|
||||
if (pts_origin == AV_NOPTS_VALUE) {
|
||||
if (!recorder->audio) {
|
||||
assert(video_pkt);
|
||||
pts_origin = video_pkt->packet->pts;
|
||||
} else if (video_pkt && audio_pkt) {
|
||||
pts_origin =
|
||||
MIN(video_pkt->packet->pts, audio_pkt->packet->pts);
|
||||
} else if (recorder->stopped) {
|
||||
if (video_pkt) {
|
||||
// The recorder is stopped without audio, record the video
|
||||
// packets
|
||||
pts_origin = video_pkt->packet->pts;
|
||||
} else {
|
||||
// Fail if there is no video
|
||||
error = true;
|
||||
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->packet->pts
|
||||
: audio_pkt->packet->pts;
|
||||
} else {
|
||||
// We need both video and audio packets to initialize pts_origin
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
assert(pts_origin != AV_NOPTS_VALUE);
|
||||
|
||||
if (video_pkt) {
|
||||
video_pkt->packet->pts -= pts_origin;
|
||||
video_pkt->packet->dts = video_pkt->packet->pts;
|
||||
|
||||
if (video_pkt_previous) {
|
||||
// we now know the duration of the previous packet
|
||||
video_pkt_previous->packet->duration =
|
||||
video_pkt->packet->pts - video_pkt_previous->packet->pts;
|
||||
|
||||
bool ok = sc_recorder_write_video(recorder,
|
||||
video_pkt_previous->packet);
|
||||
sc_record_packet_delete(video_pkt_previous);
|
||||
if (!ok) {
|
||||
LOGE("Could not record video packet");
|
||||
error = true;
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
|
||||
video_pkt_previous = video_pkt;
|
||||
video_pkt = NULL;
|
||||
}
|
||||
|
||||
if (audio_pkt) {
|
||||
audio_pkt->packet->pts -= pts_origin;
|
||||
audio_pkt->packet->dts = audio_pkt->packet->pts;
|
||||
|
||||
bool ok = sc_recorder_write_audio(recorder, audio_pkt->packet);
|
||||
if (!ok) {
|
||||
LOGE("Could not record audio packet");
|
||||
error = true;
|
||||
goto end;
|
||||
}
|
||||
|
||||
sc_record_packet_delete(audio_pkt);
|
||||
audio_pkt = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the last video packet
|
||||
struct sc_record_packet *last = video_pkt_previous;
|
||||
if (last) {
|
||||
// assign an arbitrary duration to the last packet
|
||||
last->packet->duration = 100000;
|
||||
bool ok = sc_recorder_write_video(recorder, last->packet);
|
||||
if (!ok) {
|
||||
// failing to write the last frame is not very serious, no
|
||||
// future frame may depend on it, so the resulting file
|
||||
// will still be valid
|
||||
LOGW("Could not record last packet");
|
||||
}
|
||||
sc_record_packet_delete(last);
|
||||
}
|
||||
|
||||
int ret = av_write_trailer(recorder->ctx);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to write trailer to %s", recorder->filename);
|
||||
error = false;
|
||||
}
|
||||
|
||||
end:
|
||||
if (video_pkt) {
|
||||
sc_record_packet_delete(video_pkt);
|
||||
}
|
||||
if (audio_pkt) {
|
||||
sc_record_packet_delete(audio_pkt);
|
||||
}
|
||||
|
||||
return !error;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_record(struct sc_recorder *recorder) {
|
||||
bool ok = sc_recorder_open_output_file(recorder);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_recorder_wait_video_stream(recorder);
|
||||
if (!ok) {
|
||||
sc_recorder_close_output_file(recorder);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (recorder->audio) {
|
||||
ok = sc_recorder_wait_audio_stream(recorder);
|
||||
if (!ok) {
|
||||
sc_recorder_close_output_file(recorder);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If recorder->stopped, process any queued packet anyway
|
||||
|
||||
ok = sc_recorder_process_packets(recorder);
|
||||
sc_recorder_close_output_file(recorder);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int
|
||||
run_recorder(void *data) {
|
||||
struct sc_recorder *recorder = data;
|
||||
|
||||
bool success = sc_recorder_record(recorder);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
// Prevent the producer to push any new packet
|
||||
recorder->stopped = true;
|
||||
// Discard pending packets
|
||||
sc_recorder_queue_clear(&recorder->video_queue);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
if (success) {
|
||||
const char *format_name = sc_recorder_get_format_name(recorder->format);
|
||||
LOGI("Recording complete to %s file: %s", format_name,
|
||||
recorder->filename);
|
||||
} else {
|
||||
LOGE("Recording failed to %s", recorder->filename);
|
||||
}
|
||||
|
||||
LOGD("Recorder thread ended");
|
||||
|
||||
recorder->cbs->on_ended(recorder, success, recorder->cbs_userdata);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
|
||||
const AVCodec *codec) {
|
||||
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
||||
assert(codec);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
if (recorder->stopped) {
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
recorder->video_codec = codec;
|
||||
sc_cond_signal(&recorder->stream_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_video_packet_sink_close(struct sc_packet_sink *sink) {
|
||||
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
// EOS also stops the recorder
|
||||
recorder->stopped = true;
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
|
||||
const AVPacket *packet) {
|
||||
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
if (recorder->stopped) {
|
||||
// reject any new packet
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
@@ -358,7 +595,9 @@ sc_recorder_push(struct sc_recorder *recorder, const AVPacket *packet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sc_queue_push(&recorder->queue, next, rec);
|
||||
rec->packet->stream_index = 0;
|
||||
|
||||
sc_queue_push(&recorder->video_queue, next, rec);
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
@@ -366,51 +605,190 @@ sc_recorder_push(struct sc_recorder *recorder, const AVPacket *packet) {
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_packet_sink_open(struct sc_packet_sink *sink,
|
||||
const AVCodec *codec) {
|
||||
struct sc_recorder *recorder = DOWNCAST(sink);
|
||||
return sc_recorder_open(recorder, codec);
|
||||
sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
|
||||
const AVCodec *codec) {
|
||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||
assert(recorder->audio);
|
||||
// only written from this thread, no need to lock
|
||||
assert(!recorder->audio_disabled);
|
||||
assert(codec);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
recorder->audio_codec = codec;
|
||||
sc_cond_signal(&recorder->stream_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_packet_sink_close(struct sc_packet_sink *sink) {
|
||||
struct sc_recorder *recorder = DOWNCAST(sink);
|
||||
sc_recorder_close(recorder);
|
||||
sc_recorder_audio_packet_sink_close(struct sc_packet_sink *sink) {
|
||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||
assert(recorder->audio);
|
||||
// only written from this thread, no need to lock
|
||||
assert(!recorder->audio_disabled);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
// EOS also stops the recorder
|
||||
// TODO must stop only once both video and audio stream are complete
|
||||
recorder->stopped = true;
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_packet_sink_push(struct sc_packet_sink *sink,
|
||||
const AVPacket *packet) {
|
||||
struct sc_recorder *recorder = DOWNCAST(sink);
|
||||
return sc_recorder_push(recorder, packet);
|
||||
sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink,
|
||||
const AVPacket *packet) {
|
||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||
assert(recorder->audio);
|
||||
// only written from this thread, no need to lock
|
||||
assert(!recorder->audio_disabled);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
if (recorder->stopped) {
|
||||
// reject any new packet
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sc_record_packet *rec = sc_record_packet_new(packet);
|
||||
if (!rec) {
|
||||
LOG_OOM();
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
rec->packet->stream_index = 1;
|
||||
|
||||
sc_queue_push(&recorder->audio_queue, next, rec);
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) {
|
||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||
assert(recorder->audio);
|
||||
// only written from this thread, no need to lock
|
||||
assert(!recorder->audio_disabled);
|
||||
assert(!recorder->audio_codec);
|
||||
|
||||
LOGW("Audio stream recording disabled");
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
recorder->audio_disabled = true;
|
||||
sc_cond_signal(&recorder->stream_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
}
|
||||
|
||||
bool
|
||||
sc_recorder_init(struct sc_recorder *recorder,
|
||||
const char *filename,
|
||||
enum sc_record_format format,
|
||||
struct sc_size declared_frame_size) {
|
||||
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||
enum sc_record_format format, bool audio,
|
||||
struct sc_size declared_frame_size,
|
||||
const struct sc_recorder_callbacks *cbs, void *cbs_userdata) {
|
||||
recorder->filename = strdup(filename);
|
||||
if (!recorder->filename) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = sc_mutex_init(&recorder->mutex);
|
||||
if (!ok) {
|
||||
goto error_free_filename;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&recorder->queue_cond);
|
||||
if (!ok) {
|
||||
goto error_mutex_destroy;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&recorder->stream_cond);
|
||||
if (!ok) {
|
||||
goto error_queue_cond_destroy;
|
||||
}
|
||||
|
||||
recorder->audio = audio;
|
||||
|
||||
sc_queue_init(&recorder->video_queue);
|
||||
sc_queue_init(&recorder->audio_queue);
|
||||
recorder->stopped = false;
|
||||
|
||||
recorder->video_codec = NULL;
|
||||
recorder->audio_codec = NULL;
|
||||
recorder->audio_disabled = false;
|
||||
|
||||
recorder->video_stream_index = -1;
|
||||
recorder->audio_stream_index = -1;
|
||||
|
||||
recorder->format = format;
|
||||
recorder->declared_frame_size = declared_frame_size;
|
||||
|
||||
static const struct sc_packet_sink_ops ops = {
|
||||
.open = sc_recorder_packet_sink_open,
|
||||
.close = sc_recorder_packet_sink_close,
|
||||
.push = sc_recorder_packet_sink_push,
|
||||
assert(cbs && cbs->on_ended);
|
||||
|
||||
recorder->cbs = cbs;
|
||||
recorder->cbs_userdata = cbs_userdata;
|
||||
|
||||
static const struct sc_packet_sink_ops video_ops = {
|
||||
.open = sc_recorder_video_packet_sink_open,
|
||||
.close = sc_recorder_video_packet_sink_close,
|
||||
.push = sc_recorder_video_packet_sink_push,
|
||||
};
|
||||
|
||||
recorder->packet_sink.ops = &ops;
|
||||
recorder->video_packet_sink.ops = &video_ops;
|
||||
|
||||
if (audio) {
|
||||
static const struct sc_packet_sink_ops audio_ops = {
|
||||
.open = sc_recorder_audio_packet_sink_open,
|
||||
.close = sc_recorder_audio_packet_sink_close,
|
||||
.push = sc_recorder_audio_packet_sink_push,
|
||||
.disable = sc_recorder_audio_packet_sink_disable,
|
||||
};
|
||||
|
||||
recorder->audio_packet_sink.ops = &audio_ops;
|
||||
}
|
||||
|
||||
ok = sc_thread_create(&recorder->thread, run_recorder, "scrcpy-recorder",
|
||||
recorder);
|
||||
if (!ok) {
|
||||
LOGE("Could not start recorder thread");
|
||||
goto error_stream_cond_destroy;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
error_stream_cond_destroy:
|
||||
sc_cond_destroy(&recorder->stream_cond);
|
||||
error_queue_cond_destroy:
|
||||
sc_cond_destroy(&recorder->queue_cond);
|
||||
error_mutex_destroy:
|
||||
sc_mutex_destroy(&recorder->mutex);
|
||||
error_free_filename:
|
||||
free(recorder->filename);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
sc_recorder_stop(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
recorder->stopped = true;
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
sc_cond_signal(&recorder->stream_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
}
|
||||
|
||||
void
|
||||
sc_recorder_join(struct sc_recorder *recorder) {
|
||||
sc_thread_join(&recorder->thread, NULL);
|
||||
}
|
||||
|
||||
void
|
||||
sc_recorder_destroy(struct sc_recorder *recorder) {
|
||||
sc_cond_destroy(&recorder->stream_cond);
|
||||
sc_cond_destroy(&recorder->queue_cond);
|
||||
sc_mutex_destroy(&recorder->mutex);
|
||||
free(recorder->filename);
|
||||
}
|
||||
|
||||
@@ -20,34 +20,63 @@ struct sc_record_packet {
|
||||
struct sc_recorder_queue SC_QUEUE(struct sc_record_packet);
|
||||
|
||||
struct sc_recorder {
|
||||
struct sc_packet_sink packet_sink; // packet sink trait
|
||||
struct sc_packet_sink video_packet_sink;
|
||||
struct sc_packet_sink audio_packet_sink;
|
||||
|
||||
/* The audio flag is unprotected:
|
||||
* - it is initialized from sc_recorder_init() from the main thread;
|
||||
* - it may be reset once from the recorder thread if the audio is
|
||||
* disabled dynamically.
|
||||
*
|
||||
* Therefore, once the recorder thread is started, only the recorder thread
|
||||
* may access it without data races.
|
||||
*/
|
||||
bool audio;
|
||||
|
||||
char *filename;
|
||||
enum sc_record_format format;
|
||||
AVFormatContext *ctx;
|
||||
struct sc_size declared_frame_size;
|
||||
bool header_written;
|
||||
|
||||
uint64_t pts_origin;
|
||||
|
||||
sc_thread thread;
|
||||
sc_mutex mutex;
|
||||
sc_cond queue_cond;
|
||||
bool stopped; // set on recorder_close()
|
||||
bool failed; // set on packet write failure
|
||||
struct sc_recorder_queue queue;
|
||||
// set on sc_recorder_stop(), packet_sink close or recording failure
|
||||
bool stopped;
|
||||
struct sc_recorder_queue video_queue;
|
||||
struct sc_recorder_queue audio_queue;
|
||||
|
||||
// we can write a packet only once we received the next one so that we can
|
||||
// set its duration (next_pts - current_pts)
|
||||
// "previous" is only accessed from the recorder thread, so it does not
|
||||
// need to be protected by the mutex
|
||||
struct sc_record_packet *previous;
|
||||
// wake up the recorder thread once the video or audio codec is known
|
||||
sc_cond stream_cond;
|
||||
const AVCodec *video_codec;
|
||||
const AVCodec *audio_codec;
|
||||
// Instead of providing an audio_codec, the demuxer may notify that the
|
||||
// stream is disabled if the device could not capture audio
|
||||
bool audio_disabled;
|
||||
|
||||
int video_stream_index;
|
||||
int audio_stream_index;
|
||||
|
||||
const struct sc_recorder_callbacks *cbs;
|
||||
void *cbs_userdata;
|
||||
};
|
||||
|
||||
struct sc_recorder_callbacks {
|
||||
void (*on_ended)(struct sc_recorder *recorder, bool success,
|
||||
void *userdata);
|
||||
};
|
||||
|
||||
bool
|
||||
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||
enum sc_record_format format,
|
||||
struct sc_size declared_frame_size);
|
||||
enum sc_record_format format, bool audio,
|
||||
struct sc_size declared_frame_size,
|
||||
const struct sc_recorder_callbacks *cbs, void *cbs_userdata);
|
||||
|
||||
void
|
||||
sc_recorder_stop(struct sc_recorder *recorder);
|
||||
|
||||
void
|
||||
sc_recorder_join(struct sc_recorder *recorder);
|
||||
|
||||
void
|
||||
sc_recorder_destroy(struct sc_recorder *recorder);
|
||||
|
||||
@@ -156,9 +156,15 @@ event_loop(struct scrcpy *s) {
|
||||
SDL_Event event;
|
||||
while (SDL_WaitEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case EVENT_STREAM_STOPPED:
|
||||
case SC_EVENT_DEVICE_DISCONNECTED:
|
||||
LOGW("Device disconnected");
|
||||
return SCRCPY_EXIT_DISCONNECTED;
|
||||
case SC_EVENT_DEMUXER_ERROR:
|
||||
LOGE("Demuxer error");
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
case SC_EVENT_RECORDER_ERROR:
|
||||
LOGE("Recorder error");
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
case SDL_QUIT:
|
||||
LOGD("User requested to quit");
|
||||
return SCRCPY_EXIT_SUCCESS;
|
||||
@@ -180,10 +186,10 @@ await_for_server(bool *connected) {
|
||||
LOGD("User requested to quit");
|
||||
*connected = false;
|
||||
return true;
|
||||
case EVENT_SERVER_CONNECTION_FAILED:
|
||||
case SC_EVENT_SERVER_CONNECTION_FAILED:
|
||||
LOGE("Server connection failed");
|
||||
return false;
|
||||
case EVENT_SERVER_CONNECTED:
|
||||
case SC_EVENT_SERVER_CONNECTED:
|
||||
LOGD("Server connected");
|
||||
*connected = true;
|
||||
return true;
|
||||
@@ -234,19 +240,37 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
||||
}
|
||||
|
||||
static void
|
||||
sc_video_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) {
|
||||
(void) demuxer;
|
||||
sc_recorder_on_ended(struct sc_recorder *recorder, bool success,
|
||||
void *userdata) {
|
||||
(void) recorder;
|
||||
(void) userdata;
|
||||
|
||||
PUSH_EVENT(EVENT_STREAM_STOPPED);
|
||||
if (!success) {
|
||||
PUSH_EVENT(SC_EVENT_RECORDER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
sc_audio_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) {
|
||||
sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos,
|
||||
void *userdata) {
|
||||
(void) demuxer;
|
||||
(void) userdata;
|
||||
|
||||
// TODO
|
||||
if (eos) {
|
||||
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
|
||||
} else {
|
||||
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos,
|
||||
void *userdata) {
|
||||
(void) demuxer;
|
||||
(void) eos;
|
||||
(void) userdata;
|
||||
|
||||
// Contrary to the video demuxer, keep mirroring if only the audio fails
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -254,7 +278,7 @@ sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
|
||||
(void) server;
|
||||
(void) userdata;
|
||||
|
||||
PUSH_EVENT(EVENT_SERVER_CONNECTION_FAILED);
|
||||
PUSH_EVENT(SC_EVENT_SERVER_CONNECTION_FAILED);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -262,7 +286,7 @@ sc_server_on_connected(struct sc_server *server, void *userdata) {
|
||||
(void) server;
|
||||
(void) userdata;
|
||||
|
||||
PUSH_EVENT(EVENT_SERVER_CONNECTED);
|
||||
PUSH_EVENT(SC_EVENT_SERVER_CONNECTED);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -275,8 +299,9 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) {
|
||||
// event
|
||||
}
|
||||
|
||||
// Generate a scrcpy id to differentiate multiple running scrcpy instances
|
||||
static uint32_t
|
||||
scrcpy_generate_uid() {
|
||||
scrcpy_generate_scid() {
|
||||
struct sc_rand rand;
|
||||
sc_rand_init(&rand);
|
||||
// Only use 31 bits to avoid issues with signed values on the Java-side
|
||||
@@ -317,21 +342,23 @@ scrcpy(struct scrcpy_options *options) {
|
||||
|
||||
struct sc_acksync *acksync = NULL;
|
||||
|
||||
uint32_t uid = scrcpy_generate_uid();
|
||||
uint32_t scid = scrcpy_generate_scid();
|
||||
|
||||
struct sc_server_params params = {
|
||||
.uid = uid,
|
||||
.scid = scid,
|
||||
.req_serial = options->serial,
|
||||
.select_usb = options->select_usb,
|
||||
.select_tcpip = options->select_tcpip,
|
||||
.log_level = options->log_level,
|
||||
.codec = options->codec,
|
||||
.audio_codec = options->audio_codec,
|
||||
.crop = options->crop,
|
||||
.port_range = options->port_range,
|
||||
.tunnel_host = options->tunnel_host,
|
||||
.tunnel_port = options->tunnel_port,
|
||||
.max_size = options->max_size,
|
||||
.bit_rate = options->bit_rate,
|
||||
.audio_bit_rate = options->audio_bit_rate,
|
||||
.max_fps = options->max_fps,
|
||||
.lock_video_orientation = options->lock_video_orientation,
|
||||
.control = options->control,
|
||||
@@ -419,10 +446,12 @@ scrcpy(struct scrcpy_options *options) {
|
||||
|
||||
struct sc_recorder *rec = NULL;
|
||||
if (options->record_filename) {
|
||||
if (!sc_recorder_init(&s->recorder,
|
||||
options->record_filename,
|
||||
options->record_format,
|
||||
info->frame_size)) {
|
||||
static const struct sc_recorder_callbacks recorder_cbs = {
|
||||
.on_ended = sc_recorder_on_ended,
|
||||
};
|
||||
if (!sc_recorder_init(&s->recorder, options->record_filename,
|
||||
options->record_format, options->audio,
|
||||
info->frame_size, &recorder_cbs, NULL)) {
|
||||
goto end;
|
||||
}
|
||||
rec = &s->recorder;
|
||||
@@ -432,16 +461,16 @@ scrcpy(struct scrcpy_options *options) {
|
||||
av_log_set_callback(av_log_callback);
|
||||
|
||||
static const struct sc_demuxer_callbacks video_demuxer_cbs = {
|
||||
.on_eos = sc_video_demuxer_on_eos,
|
||||
.on_ended = sc_video_demuxer_on_ended,
|
||||
};
|
||||
sc_demuxer_init(&s->video_demuxer, s->server.video_socket,
|
||||
sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket,
|
||||
&video_demuxer_cbs, NULL);
|
||||
|
||||
if (options->audio) {
|
||||
static const struct sc_demuxer_callbacks audio_demuxer_cbs = {
|
||||
.on_eos = sc_audio_demuxer_on_eos,
|
||||
.on_ended = sc_audio_demuxer_on_ended,
|
||||
};
|
||||
sc_demuxer_init(&s->audio_demuxer, s->server.audio_socket,
|
||||
sc_demuxer_init(&s->audio_demuxer, "audio", s->server.audio_socket,
|
||||
&audio_demuxer_cbs, NULL);
|
||||
}
|
||||
|
||||
@@ -450,7 +479,10 @@ scrcpy(struct scrcpy_options *options) {
|
||||
}
|
||||
|
||||
if (rec) {
|
||||
sc_demuxer_add_sink(&s->video_demuxer, &rec->packet_sink);
|
||||
sc_demuxer_add_sink(&s->video_demuxer, &rec->video_packet_sink);
|
||||
if (options->audio) {
|
||||
sc_demuxer_add_sink(&s->audio_demuxer, &rec->audio_packet_sink);
|
||||
}
|
||||
}
|
||||
|
||||
struct sc_controller *controller = NULL;
|
||||
@@ -702,6 +734,9 @@ end:
|
||||
if (file_pusher_initialized) {
|
||||
sc_file_pusher_stop(&s->file_pusher);
|
||||
}
|
||||
if (recorder_initialized) {
|
||||
sc_recorder_stop(&s->recorder);
|
||||
}
|
||||
if (screen_initialized) {
|
||||
sc_screen_interrupt(&s->screen);
|
||||
}
|
||||
@@ -753,6 +788,7 @@ end:
|
||||
}
|
||||
|
||||
if (recorder_initialized) {
|
||||
sc_recorder_join(&s->recorder);
|
||||
sc_recorder_destroy(&s->recorder);
|
||||
}
|
||||
|
||||
|
||||
@@ -371,7 +371,7 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
|
||||
bool need_new_event;
|
||||
if (previous_skipped) {
|
||||
sc_fps_counter_add_skipped_frame(&screen->fps_counter);
|
||||
// The EVENT_NEW_FRAME triggered for the previous frame will consume
|
||||
// The SC_EVENT_NEW_FRAME triggered for the previous frame will consume
|
||||
// this new frame instead, unless the previous event failed
|
||||
need_new_event = screen->event_failed;
|
||||
} else {
|
||||
@@ -380,7 +380,7 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
|
||||
|
||||
if (need_new_event) {
|
||||
static SDL_Event new_frame_event = {
|
||||
.type = EVENT_NEW_FRAME,
|
||||
.type = SC_EVENT_NEW_FRAME,
|
||||
};
|
||||
|
||||
// Post the event on the UI thread
|
||||
@@ -820,7 +820,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
|
||||
bool relative_mode = sc_screen_is_relative_mode(screen);
|
||||
|
||||
switch (event->type) {
|
||||
case EVENT_NEW_FRAME: {
|
||||
case SC_EVENT_NEW_FRAME: {
|
||||
bool ok = sc_screen_update_frame(screen);
|
||||
if (!ok) {
|
||||
LOGW("Frame update failed\n");
|
||||
|
||||
@@ -165,6 +165,10 @@ sc_server_get_codec_name(enum sc_codec codec) {
|
||||
return "h265";
|
||||
case SC_CODEC_AV1:
|
||||
return "av1";
|
||||
case SC_CODEC_OPUS:
|
||||
return "opus";
|
||||
case SC_CODEC_AAC:
|
||||
return "aac";
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
@@ -213,16 +217,24 @@ execute_server(struct sc_server *server,
|
||||
cmd[count++] = p; \
|
||||
}
|
||||
|
||||
ADD_PARAM("uid=%08x", params->uid);
|
||||
ADD_PARAM("scid=%08x", params->scid);
|
||||
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
|
||||
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
|
||||
|
||||
if (params->bit_rate) {
|
||||
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
|
||||
}
|
||||
if (!params->audio) {
|
||||
ADD_PARAM("audio=false");
|
||||
} else if (params->audio_bit_rate) {
|
||||
ADD_PARAM("audio_bit_rate=%" PRIu32, params->audio_bit_rate);
|
||||
}
|
||||
if (params->codec != SC_CODEC_H264) {
|
||||
ADD_PARAM("codec=%s", sc_server_get_codec_name(params->codec));
|
||||
}
|
||||
if (params->audio_codec != SC_CODEC_OPUS) {
|
||||
ADD_PARAM("audio_codec=%s",
|
||||
sc_server_get_codec_name(params->audio_codec));
|
||||
}
|
||||
if (params->max_size) {
|
||||
ADD_PARAM("max_size=%" PRIu16, params->max_size);
|
||||
}
|
||||
@@ -821,7 +833,7 @@ run_server(void *data) {
|
||||
LOGD("Device serial: %s", serial);
|
||||
|
||||
int r = asprintf(&server->device_socket_name, SC_SOCKET_NAME_PREFIX "%08x",
|
||||
params->uid);
|
||||
params->scid);
|
||||
if (r == -1) {
|
||||
LOG_OOM();
|
||||
goto error_connection_failed;
|
||||
|
||||
@@ -22,10 +22,11 @@ struct sc_server_info {
|
||||
};
|
||||
|
||||
struct sc_server_params {
|
||||
uint32_t uid;
|
||||
uint32_t scid;
|
||||
const char *req_serial;
|
||||
enum sc_log_level log_level;
|
||||
enum sc_codec codec;
|
||||
enum sc_codec audio_codec;
|
||||
const char *crop;
|
||||
const char *codec_options;
|
||||
const char *encoder_name;
|
||||
@@ -34,6 +35,7 @@ struct sc_server_params {
|
||||
uint16_t tunnel_port;
|
||||
uint16_t max_size;
|
||||
uint32_t bit_rate;
|
||||
uint32_t audio_bit_rate;
|
||||
uint16_t max_fps;
|
||||
int8_t lock_video_orientation;
|
||||
bool control;
|
||||
|
||||
@@ -19,9 +19,20 @@ struct sc_packet_sink {
|
||||
};
|
||||
|
||||
struct sc_packet_sink_ops {
|
||||
/* The codec instance is static, it is valid until the end of the program */
|
||||
bool (*open)(struct sc_packet_sink *sink, const AVCodec *codec);
|
||||
void (*close)(struct sc_packet_sink *sink);
|
||||
bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet);
|
||||
|
||||
/*/
|
||||
* Called when the input stream has been disabled at runtime.
|
||||
*
|
||||
* If it is called, then open(), close() and push() will never be called.
|
||||
*
|
||||
* It is useful to notify the recorder that the requested audio stream has
|
||||
* finally been disabled because the device could not capture it.
|
||||
*/
|
||||
void (*disable)(struct sc_packet_sink *sink);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -22,7 +22,7 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) {
|
||||
(void) userdata;
|
||||
|
||||
SDL_Event event;
|
||||
event.type = EVENT_USB_DEVICE_DISCONNECTED;
|
||||
event.type = SC_EVENT_USB_DEVICE_DISCONNECTED;
|
||||
int ret = SDL_PushEvent(&event);
|
||||
if (ret < 0) {
|
||||
LOGE("Could not post USB disconnection event: %s", SDL_GetError());
|
||||
@@ -34,7 +34,7 @@ event_loop(struct scrcpy_otg *s) {
|
||||
SDL_Event event;
|
||||
while (SDL_WaitEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case EVENT_USB_DEVICE_DISCONNECTED:
|
||||
case SC_EVENT_USB_DEVICE_DISCONNECTED:
|
||||
LOGW("Device disconnected");
|
||||
return SCRCPY_EXIT_DISCONNECTED;
|
||||
case SDL_QUIT:
|
||||
|
||||
@@ -3,7 +3,8 @@ package com.genymobile.scrcpy;
|
||||
import android.media.MediaFormat;
|
||||
|
||||
public enum AudioCodec implements Codec {
|
||||
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);
|
||||
|
||||
private final int id; // 4-byte ASCII representation of the name
|
||||
private final String name;
|
||||
@@ -17,7 +18,7 @@ public enum AudioCodec implements Codec {
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.VIDEO;
|
||||
return Type.AUDIO;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -11,18 +11,16 @@ import android.media.MediaRecorder;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class AudioEncoder {
|
||||
|
||||
private static class InputTask {
|
||||
final int index;
|
||||
private final int index;
|
||||
|
||||
InputTask(int index) {
|
||||
this.index = index;
|
||||
@@ -30,8 +28,8 @@ public final class AudioEncoder {
|
||||
}
|
||||
|
||||
private static class OutputTask {
|
||||
final int index;
|
||||
final MediaCodec.BufferInfo bufferInfo;
|
||||
private final int index;
|
||||
private final MediaCodec.BufferInfo bufferInfo;
|
||||
|
||||
OutputTask(int index, MediaCodec.BufferInfo bufferInfo) {
|
||||
this.index = index;
|
||||
@@ -39,19 +37,18 @@ public final class AudioEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
private static final String MIMETYPE = MediaFormat.MIMETYPE_AUDIO_OPUS;
|
||||
private static final int SAMPLE_RATE = 48000;
|
||||
private static final int CHANNELS = 2;
|
||||
private static final int BIT_RATE = 128000;
|
||||
|
||||
private static int BUFFER_MS = 15; // milliseconds
|
||||
private static final int BUFFER_MS = 15; // milliseconds
|
||||
private static final int BUFFER_SIZE = SAMPLE_RATE * CHANNELS * BUFFER_MS / 1000;
|
||||
|
||||
private final Streamer streamer;
|
||||
private final int bitRate;
|
||||
|
||||
private AudioRecord recorder;
|
||||
private MediaCodec mediaCodec;
|
||||
|
||||
private final AtomicBoolean cleanUpDone = new AtomicBoolean(false);
|
||||
|
||||
// Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4).
|
||||
// So many pending tasks would lead to an unacceptable delay anyway.
|
||||
private final BlockingQueue<InputTask> inputTasks = new ArrayBlockingQueue<>(64);
|
||||
@@ -65,6 +62,11 @@ public final class AudioEncoder {
|
||||
|
||||
private boolean ended;
|
||||
|
||||
public AudioEncoder(Streamer streamer, int bitRate) {
|
||||
this.streamer = streamer;
|
||||
this.bitRate = bitRate;
|
||||
}
|
||||
|
||||
private static AudioFormat createAudioFormat() {
|
||||
AudioFormat.Builder builder = new AudioFormat.Builder();
|
||||
builder.setEncoding(AudioFormat.ENCODING_PCM_16BIT);
|
||||
@@ -87,10 +89,10 @@ public final class AudioEncoder {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static MediaFormat createFormat() {
|
||||
private static MediaFormat createFormat(String mimeType, int bitRate) {
|
||||
MediaFormat format = new MediaFormat();
|
||||
format.setString(MediaFormat.KEY_MIME, MIMETYPE);
|
||||
format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
|
||||
format.setString(MediaFormat.KEY_MIME, mimeType);
|
||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
||||
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS);
|
||||
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE);
|
||||
return format;
|
||||
@@ -142,11 +144,13 @@ public final class AudioEncoder {
|
||||
}
|
||||
|
||||
private void outputThread() throws IOException, InterruptedException {
|
||||
streamer.writeHeader();
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
OutputTask task = outputTasks.take();
|
||||
ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index);
|
||||
try {
|
||||
Ln.i("Audio packet [pts=" + task.bufferInfo.presentationTimeUs + "] " + buffer.remaining() + " bytes");
|
||||
streamer.writePacket(buffer, task.bufferInfo);
|
||||
} finally {
|
||||
mediaCodec.releaseOutputBuffer(task.index, false);
|
||||
}
|
||||
@@ -158,7 +162,7 @@ public final class AudioEncoder {
|
||||
try {
|
||||
encode();
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
Ln.e("Audio encoding error", e);
|
||||
} finally {
|
||||
Ln.d("Audio encoder stopped");
|
||||
}
|
||||
@@ -196,25 +200,32 @@ public final class AudioEncoder {
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public void encode() throws IOException {
|
||||
mediaCodec = MediaCodec.createEncoderByType(MIMETYPE); // may throw IOException
|
||||
|
||||
try {
|
||||
recorder = createAudioRecord();
|
||||
try {
|
||||
String mimeType = streamer.getCodec().getMimeType();
|
||||
|
||||
mediaCodecThread = new HandlerThread("AudioEncoder");
|
||||
mediaCodecThread.start();
|
||||
mediaCodec = MediaCodec.createEncoderByType(mimeType); // may throw IOException
|
||||
recorder = createAudioRecord();
|
||||
|
||||
MediaFormat format = createFormat();
|
||||
mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper()));
|
||||
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||
mediaCodecThread = new HandlerThread("AudioEncoder");
|
||||
mediaCodecThread.start();
|
||||
|
||||
recorder.startRecording();
|
||||
MediaFormat format = createFormat(mimeType, bitRate);
|
||||
mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper()));
|
||||
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||
|
||||
recorder.startRecording();
|
||||
} catch (Throwable e) {
|
||||
// Notify the client that the audio could not be captured
|
||||
streamer.writeDisableStream();
|
||||
throw e;
|
||||
}
|
||||
|
||||
inputThread = new Thread(() -> {
|
||||
try {
|
||||
inputThread();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// this is expected on close
|
||||
Ln.e("Audio capture error", e);
|
||||
} finally {
|
||||
end();
|
||||
}
|
||||
@@ -223,8 +234,13 @@ public final class AudioEncoder {
|
||||
outputThread = new Thread(() -> {
|
||||
try {
|
||||
outputThread();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
} catch (InterruptedException e) {
|
||||
// this is expected on close
|
||||
} catch (IOException e) {
|
||||
// Broken pipe is expected on close, because the socket is closed by the client
|
||||
if (!IO.isBrokenPipe(e)) {
|
||||
Ln.e("Audio encoding error", e);
|
||||
}
|
||||
} finally {
|
||||
end();
|
||||
}
|
||||
@@ -234,7 +250,9 @@ public final class AudioEncoder {
|
||||
inputThread.start();
|
||||
outputThread.start();
|
||||
} catch (Throwable e) {
|
||||
mediaCodec.release();
|
||||
if (mediaCodec != null) {
|
||||
mediaCodec.release();
|
||||
}
|
||||
if (recorder != null) {
|
||||
recorder.release();
|
||||
}
|
||||
|
||||
@@ -2,10 +2,16 @@ package com.genymobile.scrcpy;
|
||||
|
||||
public interface Codec {
|
||||
|
||||
enum Type {VIDEO}
|
||||
enum Type {
|
||||
VIDEO,
|
||||
AUDIO,
|
||||
}
|
||||
|
||||
Type getType();
|
||||
|
||||
int getId();
|
||||
|
||||
String getName();
|
||||
|
||||
String getMimeType();
|
||||
}
|
||||
|
||||
@@ -51,17 +51,17 @@ public final class DesktopConnection implements Closeable {
|
||||
return localSocket;
|
||||
}
|
||||
|
||||
private static String getSocketName(int uid) {
|
||||
if (uid == -1) {
|
||||
// If no UID is set, use "scrcpy" to simplify using scrcpy-server alone
|
||||
private static String getSocketName(int scid) {
|
||||
if (scid == -1) {
|
||||
// If no SCID is set, use "scrcpy" to simplify using scrcpy-server alone
|
||||
return SOCKET_NAME_PREFIX;
|
||||
}
|
||||
|
||||
return SOCKET_NAME_PREFIX + String.format("_%08x", uid);
|
||||
return SOCKET_NAME_PREFIX + String.format("_%08x", scid);
|
||||
}
|
||||
|
||||
public static DesktopConnection open(int uid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException {
|
||||
String socketName = getSocketName(uid);
|
||||
public static DesktopConnection open(int scid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException {
|
||||
String socketName = getSocketName(scid);
|
||||
|
||||
LocalSocket videoSocket = null;
|
||||
LocalSocket audioSocket = null;
|
||||
|
||||
@@ -48,4 +48,9 @@ public final class IO {
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static boolean isBrokenPipe(IOException e) {
|
||||
Throwable cause = e.getCause();
|
||||
return cause instanceof ErrnoException && ((ErrnoException) cause).errno == OsConstants.EPIPE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@ import java.util.List;
|
||||
public class Options {
|
||||
|
||||
private Ln.Level logLevel = Ln.Level.DEBUG;
|
||||
private int uid = -1; // 31-bit non-negative value, or -1
|
||||
private int scid = -1; // 31-bit non-negative value, or -1
|
||||
private boolean audio = true;
|
||||
private int maxSize;
|
||||
private VideoCodec codec = VideoCodec.H264;
|
||||
private AudioCodec audioCodec = AudioCodec.OPUS;
|
||||
private int bitRate = 8000000;
|
||||
private int audioBitRate = 196000;
|
||||
private int maxFps;
|
||||
private int lockVideoOrientation = -1;
|
||||
private boolean tunnelForward;
|
||||
@@ -42,12 +44,12 @@ public class Options {
|
||||
this.logLevel = logLevel;
|
||||
}
|
||||
|
||||
public int getUid() {
|
||||
return uid;
|
||||
public int getScid() {
|
||||
return scid;
|
||||
}
|
||||
|
||||
public void setUid(int uid) {
|
||||
this.uid = uid;
|
||||
public void setScid(int scid) {
|
||||
this.scid = scid;
|
||||
}
|
||||
|
||||
public boolean getAudio() {
|
||||
@@ -74,6 +76,14 @@ public class Options {
|
||||
this.codec = codec;
|
||||
}
|
||||
|
||||
public AudioCodec getAudioCodec() {
|
||||
return audioCodec;
|
||||
}
|
||||
|
||||
public void setAudioCodec(AudioCodec audioCodec) {
|
||||
this.audioCodec = audioCodec;
|
||||
}
|
||||
|
||||
public int getBitRate() {
|
||||
return bitRate;
|
||||
}
|
||||
@@ -82,6 +92,14 @@ public class Options {
|
||||
this.bitRate = bitRate;
|
||||
}
|
||||
|
||||
public int getAudioBitRate() {
|
||||
return audioBitRate;
|
||||
}
|
||||
|
||||
public void setAudioBitRate(int audioBitRate) {
|
||||
this.audioBitRate = audioBitRate;
|
||||
}
|
||||
|
||||
public int getMaxFps() {
|
||||
return maxFps;
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
private boolean firstFrameSent;
|
||||
private int consecutiveErrors;
|
||||
|
||||
public ScreenEncoder(Device device, Streamer streamer, int bitRate, int maxFps, List<CodecOption> codecOptions,
|
||||
String encoderName, boolean downsizeOnError) {
|
||||
public ScreenEncoder(Device device, Streamer streamer, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
|
||||
boolean downsizeOnError) {
|
||||
this.device = device;
|
||||
this.streamer = streamer;
|
||||
this.bitRate = bitRate;
|
||||
|
||||
@@ -66,7 +66,7 @@ public final class Server {
|
||||
|
||||
Thread initThread = startInitThread(options);
|
||||
|
||||
int uid = options.getUid();
|
||||
int scid = options.getScid();
|
||||
boolean tunnelForward = options.isTunnelForward();
|
||||
boolean control = options.getControl();
|
||||
boolean audio = options.getAudio();
|
||||
@@ -93,7 +93,7 @@ public final class Server {
|
||||
Workarounds.fillAppInfo();
|
||||
}
|
||||
|
||||
try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, audio, control, sendDummyByte)) {
|
||||
try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) {
|
||||
VideoCodec codec = options.getCodec();
|
||||
if (options.getSendDeviceMeta()) {
|
||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||
@@ -111,7 +111,9 @@ public final class Server {
|
||||
|
||||
AudioEncoder audioEncoder = null;
|
||||
if (audio) {
|
||||
audioEncoder = new AudioEncoder();
|
||||
Streamer audioStreamer = new Streamer(connection.getAudioFd(), options.getAudioCodec(), options.getSendCodecId(),
|
||||
options.getSendFrameMeta());
|
||||
audioEncoder = new AudioEncoder(audioStreamer, options.getAudioBitRate());
|
||||
audioEncoder.start();
|
||||
}
|
||||
|
||||
@@ -122,7 +124,10 @@ public final class Server {
|
||||
// synchronous
|
||||
screenEncoder.streamScreen();
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
// Broken pipe is expected on close, because the socket is closed by the client
|
||||
if (!IO.isBrokenPipe(e)) {
|
||||
Ln.e("Video encoding error", e);
|
||||
}
|
||||
} finally {
|
||||
Ln.d("Screen streaming stopped");
|
||||
initThread.interrupt();
|
||||
@@ -154,6 +159,7 @@ public final class Server {
|
||||
return thread;
|
||||
}
|
||||
|
||||
@SuppressWarnings("MethodLength")
|
||||
private static Options createOptions(String... args) {
|
||||
if (args.length < 1) {
|
||||
throw new IllegalArgumentException("Missing client version");
|
||||
@@ -176,12 +182,12 @@ public final class Server {
|
||||
String key = arg.substring(0, equalIndex);
|
||||
String value = arg.substring(equalIndex + 1);
|
||||
switch (key) {
|
||||
case "uid":
|
||||
int uid = Integer.parseInt(value, 0x10);
|
||||
if (uid < -1) {
|
||||
throw new IllegalArgumentException("uid may not be negative (except -1 for 'none'): " + uid);
|
||||
case "scid":
|
||||
int scid = Integer.parseInt(value, 0x10);
|
||||
if (scid < -1) {
|
||||
throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid);
|
||||
}
|
||||
options.setUid(uid);
|
||||
options.setScid(scid);
|
||||
break;
|
||||
case "log_level":
|
||||
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
|
||||
@@ -198,6 +204,13 @@ public final class Server {
|
||||
}
|
||||
options.setCodec(codec);
|
||||
break;
|
||||
case "audio_codec":
|
||||
AudioCodec audioCodec = AudioCodec.findByName(value);
|
||||
if (audioCodec == null) {
|
||||
throw new IllegalArgumentException("Audio codec " + value + " not supported");
|
||||
}
|
||||
options.setAudioCodec(audioCodec);
|
||||
break;
|
||||
case "max_size":
|
||||
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
|
||||
options.setMaxSize(maxSize);
|
||||
@@ -206,6 +219,10 @@ public final class Server {
|
||||
int bitRate = Integer.parseInt(value);
|
||||
options.setBitRate(bitRate);
|
||||
break;
|
||||
case "audio_bit_rate":
|
||||
int audioBitRate = Integer.parseInt(value);
|
||||
options.setAudioBitRate(audioBitRate);
|
||||
break;
|
||||
case "max_fps":
|
||||
int maxFps = Integer.parseInt(value);
|
||||
options.setMaxFps(maxFps);
|
||||
|
||||
@@ -11,6 +11,8 @@ public final class Streamer {
|
||||
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
||||
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
||||
|
||||
private static final long AOPUSHDR = 0x5244485355504F41L; // "AOPUSHDR" in ASCII (little-endian)
|
||||
|
||||
private final FileDescriptor fd;
|
||||
private final Codec codec;
|
||||
private final boolean sendCodecId;
|
||||
@@ -38,7 +40,17 @@ public final class Streamer {
|
||||
}
|
||||
}
|
||||
|
||||
public void writeDisableStream() throws IOException {
|
||||
// Writing 0 (32-bit) as codec-id means that the device disables the stream (because it could not capture)
|
||||
byte[] zeros = new byte[4];
|
||||
IO.writeFully(fd, zeros, 0, zeros.length);
|
||||
}
|
||||
|
||||
public void writePacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
||||
if (codec == AudioCodec.OPUS) {
|
||||
fixOpusConfigPacket(codecBuffer, bufferInfo);
|
||||
}
|
||||
|
||||
if (sendFrameMeta) {
|
||||
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
||||
}
|
||||
@@ -64,4 +76,46 @@ public final class Streamer {
|
||||
headerBuffer.flip();
|
||||
IO.writeFully(fd, headerBuffer);
|
||||
}
|
||||
|
||||
private static void fixOpusConfigPacket(ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
||||
// Here is an example of the config packet received for an OPUS stream:
|
||||
//
|
||||
// 00000000 41 4f 50 55 53 48 44 52 13 00 00 00 00 00 00 00 |AOPUSHDR........|
|
||||
// -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA -------------------
|
||||
// 00000010 4f 70 75 73 48 65 61 64 01 01 38 01 80 bb 00 00 |OpusHead..8.....|
|
||||
// 00000020 00 00 00 |... |
|
||||
// ------------------------------------------------------------------------------
|
||||
// 00000020 41 4f 50 55 53 44 4c 59 08 00 00 00 00 | AOPUSDLY.....|
|
||||
// 00000030 00 00 00 a0 2e 63 00 00 00 00 00 41 4f 50 55 53 |.....c.....AOPUS|
|
||||
// 00000040 50 52 4c 08 00 00 00 00 00 00 00 00 b4 c4 04 00 |PRL.............|
|
||||
// 00000050 00 00 00 |...|
|
||||
//
|
||||
// Each "section" is prefixed by a 64-bit ID and a 64-bit length.
|
||||
|
||||
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
|
||||
if (!isConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (buffer.remaining() >= 16) {
|
||||
long id = buffer.getLong();
|
||||
long sizeLong = buffer.getLong();
|
||||
if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) {
|
||||
throw new IOException("Invalid block size in OPUS header: " + sizeLong);
|
||||
}
|
||||
int size = (int) sizeLong;
|
||||
if (id == AOPUSHDR) {
|
||||
if (buffer.remaining() < size) {
|
||||
throw new IOException("Not enough data in OPUS header (invalid size: " + size + ")");
|
||||
}
|
||||
// Set the buffer to point to the OPUS header slice
|
||||
buffer.limit(buffer.position() + size);
|
||||
return;
|
||||
}
|
||||
|
||||
buffer.position(buffer.position() + size);
|
||||
}
|
||||
|
||||
throw new IOException("OPUS header not found");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.genymobile.scrcpy;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.os.Looper;
|
||||
|
||||
Reference in New Issue
Block a user