Compare commits
15 Commits
audio.10
...
packet_mer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b4534371c | ||
|
|
f03f32267e | ||
|
|
45b2e6db5c | ||
|
|
400a1c69b1 | ||
|
|
730eb1086a | ||
|
|
4f9e9c6619 | ||
|
|
953edfd1df | ||
|
|
230b8274b9 | ||
|
|
40866ddc10 | ||
|
|
bd56c0abf7 | ||
|
|
6524e90c68 | ||
|
|
f2dee20a20 | ||
|
|
d2dce51038 | ||
|
|
4342c5637d | ||
|
|
3e517cd40e |
21
README.md
21
README.md
@@ -252,10 +252,22 @@ This affects recording orientation.
|
||||
The [window may also be rotated](#rotation) independently.
|
||||
|
||||
|
||||
#### Encoder
|
||||
#### Codec
|
||||
|
||||
Some devices have more than one encoder, and some of them may cause issues or
|
||||
crash. It is possible to select a different encoder:
|
||||
The video codec can be selected. The possible values are `h264` (default),
|
||||
`h265` and `av1`:
|
||||
|
||||
```bash
|
||||
scrcpy --codec=h264 # default
|
||||
scrcpy --codec=h265
|
||||
scrcpy --codec=av1
|
||||
```
|
||||
|
||||
|
||||
##### Encoder
|
||||
|
||||
Some devices have more than one encoder for a specific codec, and some of them
|
||||
may cause issues or crash. It is possible to select a different encoder:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder=OMX.qcom.video.encoder.avc
|
||||
@@ -265,7 +277,8 @@ To list the available encoders, you can pass an invalid encoder name; the
|
||||
error will give the available encoders:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder=_
|
||||
scrcpy --encoder=_ # for the default codec
|
||||
scrcpy --codec=h265 --encoder=_ # for a specific codec
|
||||
```
|
||||
|
||||
### Capture
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -25,6 +25,10 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are
|
||||
|
||||
Default is 8000000.
|
||||
|
||||
.TP
|
||||
.BI "\-\-codec " name
|
||||
Select a video codec (h264, h265 or av1).
|
||||
|
||||
.TP
|
||||
.BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...]
|
||||
Set a list of comma-separated key:type=value options for the device encoder.
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
#define OPT_NO_CLEANUP 1037
|
||||
#define OPT_PRINT_FPS 1038
|
||||
#define OPT_NO_POWER_ON 1039
|
||||
#define OPT_CODEC 1040
|
||||
|
||||
struct sc_option {
|
||||
char shortopt;
|
||||
@@ -105,6 +106,12 @@ static const struct sc_option options[] = {
|
||||
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||
"Default is " STR(DEFAULT_BIT_RATE) ".",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CODEC,
|
||||
.longopt = "codec",
|
||||
.argdesc = "name",
|
||||
.text = "Select a video codec (h264, h265 or av1).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CODEC_OPTIONS,
|
||||
.longopt = "codec-options",
|
||||
@@ -1377,6 +1384,24 @@ guess_record_format(const char *filename) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_codec(const char *optarg, enum sc_codec *codec) {
|
||||
if (!strcmp(optarg, "h264")) {
|
||||
*codec = SC_CODEC_H264;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(optarg, "h265")) {
|
||||
*codec = SC_CODEC_H265;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(optarg, "av1")) {
|
||||
*codec = SC_CODEC_AV1;
|
||||
return true;
|
||||
}
|
||||
LOGE("Unsupported codec: %s (expected h264, h265 or av1)", 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) {
|
||||
@@ -1610,6 +1635,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
case OPT_PRINT_FPS:
|
||||
opts->start_fps_counter = true;
|
||||
break;
|
||||
case OPT_CODEC:
|
||||
if (!parse_codec(optarg, &opts->codec)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_OTG:
|
||||
#ifdef HAVE_USB
|
||||
opts->otg = true;
|
||||
@@ -1718,6 +1748,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
}
|
||||
|
||||
if (opts->record_format == SC_RECORD_FORMAT_MP4
|
||||
&& opts->codec == SC_CODEC_AV1) {
|
||||
LOGE("Could not mux AV1 stream into MP4 container "
|
||||
"(record to mkv or select another video codec)");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!opts->control) {
|
||||
if (opts->turn_screen_off) {
|
||||
LOGE("Could not request to turn screen off if control is disabled");
|
||||
|
||||
@@ -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"
|
||||
@@ -17,6 +18,36 @@
|
||||
|
||||
#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1)
|
||||
|
||||
static enum AVCodecID
|
||||
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
|
||||
switch (codec_id) {
|
||||
case SC_CODEC_ID_H264:
|
||||
return AV_CODEC_ID_H264;
|
||||
case SC_CODEC_ID_H265:
|
||||
return AV_CODEC_ID_HEVC;
|
||||
case SC_CODEC_ID_AV1:
|
||||
return AV_CODEC_ID_AV1;
|
||||
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
|
||||
@@ -80,7 +111,7 @@ 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);
|
||||
LOGE("Could not send packet to sink %d", i);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -90,48 +121,7 @@ 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");
|
||||
return false;
|
||||
@@ -171,43 +161,53 @@ static int
|
||||
run_demuxer(void *data) {
|
||||
struct sc_demuxer *demuxer = data;
|
||||
|
||||
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
|
||||
// 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) {
|
||||
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
|
||||
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();
|
||||
if (!sc_demuxer_open_sinks(demuxer, codec)) {
|
||||
LOGE("Could not open demuxer sinks");
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (!sc_demuxer_open_sinks(demuxer, codec)) {
|
||||
LOGE("Could not open demuxer sinks");
|
||||
goto finally_free_codec_ctx;
|
||||
}
|
||||
|
||||
demuxer->parser = av_parser_init(AV_CODEC_ID_H264);
|
||||
if (!demuxer->parser) {
|
||||
LOGE("Could not initialize parser");
|
||||
goto finally_close_sinks;
|
||||
}
|
||||
|
||||
// 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;
|
||||
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;
|
||||
}
|
||||
|
||||
// Prepend any config packet to the next media packet
|
||||
ok = sc_packet_merger_merge(&merger, packet);
|
||||
if (!ok) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -221,19 +221,13 @@ run_demuxer(void *data) {
|
||||
|
||||
LOGD("End of frames");
|
||||
|
||||
if (demuxer->pending) {
|
||||
av_packet_free(&demuxer->pending);
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -242,10 +236,9 @@ void
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
|
||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) {
|
||||
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;
|
||||
|
||||
@@ -21,18 +21,12 @@ struct sc_demuxer {
|
||||
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);
|
||||
};
|
||||
|
||||
void
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#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)
|
||||
|
||||
@@ -13,6 +13,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.v4l2_device = NULL,
|
||||
#endif
|
||||
.log_level = SC_LOG_LEVEL_INFO,
|
||||
.codec = SC_CODEC_H264,
|
||||
.record_format = SC_RECORD_FORMAT_AUTO,
|
||||
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT,
|
||||
.port_range = {
|
||||
|
||||
@@ -23,6 +23,12 @@ enum sc_record_format {
|
||||
SC_RECORD_FORMAT_MKV,
|
||||
};
|
||||
|
||||
enum sc_codec {
|
||||
SC_CODEC_H264,
|
||||
SC_CODEC_H265,
|
||||
SC_CODEC_AV1,
|
||||
};
|
||||
|
||||
enum sc_lock_video_orientation {
|
||||
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
|
||||
// lock the current orientation when scrcpy starts
|
||||
@@ -93,6 +99,7 @@ struct scrcpy_options {
|
||||
const char *v4l2_device;
|
||||
#endif
|
||||
enum sc_log_level log_level;
|
||||
enum sc_codec codec;
|
||||
enum sc_record_format record_format;
|
||||
enum sc_keyboard_input_mode keyboard_input_mode;
|
||||
enum sc_mouse_input_mode mouse_input_mode;
|
||||
|
||||
52
app/src/packet_merger.c
Normal file
52
app/src/packet_merger.c
Normal file
@@ -0,0 +1,52 @@
|
||||
#include "packet_merger.h"
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
void
|
||||
sc_packet_merger_init(struct sc_packet_merger *merger) {
|
||||
merger->pending_config = NULL;
|
||||
}
|
||||
|
||||
void
|
||||
sc_packet_merger_destroy(struct sc_packet_merger *merger) {
|
||||
av_packet_free(&merger->pending_config);
|
||||
}
|
||||
|
||||
bool
|
||||
sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet) {
|
||||
bool is_config = packet->pts == AV_NOPTS_VALUE;
|
||||
|
||||
if (is_config) {
|
||||
av_packet_free(&merger->pending_config);
|
||||
|
||||
merger->pending_config = av_packet_alloc();
|
||||
if (!merger->pending_config) {
|
||||
LOG_OOM();
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (av_packet_ref(merger->pending_config, packet)) {
|
||||
LOG_OOM();
|
||||
av_packet_free(&merger->pending_config);
|
||||
goto error;
|
||||
}
|
||||
} else if (merger->pending_config) {
|
||||
size_t config_size = merger->pending_config->size;
|
||||
size_t media_size = packet->size;
|
||||
if (av_grow_packet(packet, config_size)) {
|
||||
LOG_OOM();
|
||||
goto error;
|
||||
}
|
||||
|
||||
memmove(packet->data + config_size, packet->data, media_size);
|
||||
memcpy(packet->data, merger->pending_config->data, config_size);
|
||||
|
||||
av_packet_free(&merger->pending_config);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
error:
|
||||
av_packet_unref(packet);
|
||||
return false;
|
||||
}
|
||||
41
app/src/packet_merger.h
Normal file
41
app/src/packet_merger.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#ifndef SC_PACKET_MERGER_H
|
||||
#define SC_PACKET_MERGER_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.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 {
|
||||
AVPacket *pending_config;
|
||||
};
|
||||
|
||||
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 reference it 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
|
||||
@@ -155,9 +155,12 @@ 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 SDL_QUIT:
|
||||
LOGD("User requested to quit");
|
||||
return SCRCPY_EXIT_SUCCESS;
|
||||
@@ -179,10 +182,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;
|
||||
@@ -233,11 +236,15 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
||||
}
|
||||
|
||||
static void
|
||||
sc_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) {
|
||||
sc_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos, void *userdata) {
|
||||
(void) demuxer;
|
||||
(void) userdata;
|
||||
|
||||
PUSH_EVENT(EVENT_STREAM_STOPPED);
|
||||
if (eos) {
|
||||
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
|
||||
} else {
|
||||
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -245,7 +252,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
|
||||
@@ -253,7 +260,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
|
||||
@@ -315,6 +322,7 @@ scrcpy(struct scrcpy_options *options) {
|
||||
.select_usb = options->select_usb,
|
||||
.select_tcpip = options->select_tcpip,
|
||||
.log_level = options->log_level,
|
||||
.codec = options->codec,
|
||||
.crop = options->crop,
|
||||
.port_range = options->port_range,
|
||||
.tunnel_host = options->tunnel_host,
|
||||
@@ -420,7 +428,7 @@ scrcpy(struct scrcpy_options *options) {
|
||||
av_log_set_callback(av_log_callback);
|
||||
|
||||
static const struct sc_demuxer_callbacks demuxer_cbs = {
|
||||
.on_eos = sc_demuxer_on_eos,
|
||||
.on_ended = sc_demuxer_on_ended,
|
||||
};
|
||||
sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL);
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -156,6 +156,20 @@ sc_server_sleep(struct sc_server *server, sc_tick deadline) {
|
||||
return !stopped;
|
||||
}
|
||||
|
||||
static const char *
|
||||
sc_server_get_codec_name(enum sc_codec codec) {
|
||||
switch (codec) {
|
||||
case SC_CODEC_H264:
|
||||
return "h264";
|
||||
case SC_CODEC_H265:
|
||||
return "h265";
|
||||
case SC_CODEC_AV1:
|
||||
return "av1";
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static sc_pid
|
||||
execute_server(struct sc_server *server,
|
||||
const struct sc_server_params *params) {
|
||||
@@ -203,6 +217,9 @@ execute_server(struct sc_server *server,
|
||||
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
|
||||
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
|
||||
|
||||
if (params->codec != SC_CODEC_H264) {
|
||||
ADD_PARAM("codec=%s", sc_server_get_codec_name(params->codec));
|
||||
}
|
||||
if (params->max_size) {
|
||||
ADD_PARAM("max_size=%" PRIu16, params->max_size);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ struct sc_server_params {
|
||||
uint32_t uid;
|
||||
const char *req_serial;
|
||||
enum sc_log_level log_level;
|
||||
enum sc_codec codec;
|
||||
const char *crop;
|
||||
const char *codec_options;
|
||||
const char *encoder_name;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -90,6 +90,7 @@ public class Controller {
|
||||
control();
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
} finally {
|
||||
Ln.d("Controller stopped");
|
||||
}
|
||||
});
|
||||
@@ -100,11 +101,17 @@ public class Controller {
|
||||
public void stop() {
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
sender.stop();
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException {
|
||||
if (thread != null) {
|
||||
thread.join();
|
||||
}
|
||||
sender.join();
|
||||
}
|
||||
|
||||
public DeviceMessageSender getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
@@ -277,6 +277,26 @@ public final class Device {
|
||||
* @param mode one of the {@code POWER_MODE_*} constants
|
||||
*/
|
||||
public static boolean setScreenPowerMode(int mode) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// Change the power mode for all physical displays
|
||||
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
|
||||
if (physicalDisplayIds == null) {
|
||||
Ln.e("Could not get physical display ids");
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean allOk = true;
|
||||
for (long physicalDisplayId : physicalDisplayIds) {
|
||||
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
|
||||
boolean ok = SurfaceControl.setDisplayPowerMode(binder, mode);
|
||||
if (!ok) {
|
||||
allOk = false;
|
||||
}
|
||||
}
|
||||
return allOk;
|
||||
}
|
||||
|
||||
// Older Android versions, only 1 display
|
||||
IBinder d = SurfaceControl.getBuiltInDisplay();
|
||||
if (d == null) {
|
||||
Ln.e("Could not get built-in display");
|
||||
|
||||
@@ -57,6 +57,7 @@ public final class DeviceMessageSender {
|
||||
loop();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// this is expected on close
|
||||
} finally {
|
||||
Ln.d("Device message sender stopped");
|
||||
}
|
||||
});
|
||||
@@ -66,7 +67,12 @@ public final class DeviceMessageSender {
|
||||
public void stop() {
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException {
|
||||
if (thread != null) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import android.graphics.Rect;
|
||||
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 maxSize;
|
||||
private VideoCodec codec = VideoCodec.H264;
|
||||
private int bitRate = 8000000;
|
||||
private int maxFps;
|
||||
private int lockVideoOrientation = -1;
|
||||
@@ -29,6 +31,7 @@ public class Options {
|
||||
private boolean sendDeviceMeta = true; // send device name and size
|
||||
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
|
||||
private boolean sendDummyByte = true; // write a byte on start to detect connection issues
|
||||
private boolean sendCodecId = true; // write the codec ID (4 bytes) before the stream
|
||||
|
||||
public Ln.Level getLogLevel() {
|
||||
return logLevel;
|
||||
@@ -54,6 +57,14 @@ public class Options {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
public VideoCodec getCodec() {
|
||||
return codec;
|
||||
}
|
||||
|
||||
public void setCodec(VideoCodec codec) {
|
||||
this.codec = codec;
|
||||
}
|
||||
|
||||
public int getBitRate() {
|
||||
return bitRate;
|
||||
}
|
||||
@@ -205,4 +216,12 @@ public class Options {
|
||||
public void setSendDummyByte(boolean sendDummyByte) {
|
||||
this.sendDummyByte = sendDummyByte;
|
||||
}
|
||||
|
||||
public boolean getSendCodecId() {
|
||||
return sendCodecId;
|
||||
}
|
||||
|
||||
public void setSendCodecId(boolean sendCodecId) {
|
||||
this.sendCodecId = sendCodecId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
|
||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||
|
||||
private final String videoMimeType;
|
||||
private final String encoderName;
|
||||
private final List<CodecOption> codecOptions;
|
||||
private final int bitRate;
|
||||
@@ -44,7 +45,8 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
private boolean firstFrameSent;
|
||||
private int consecutiveErrors;
|
||||
|
||||
public ScreenEncoder(int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName, boolean downsizeOnError) {
|
||||
public ScreenEncoder(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName, boolean downsizeOnError) {
|
||||
this.videoMimeType = videoMimeType;
|
||||
this.bitRate = bitRate;
|
||||
this.maxFps = maxFps;
|
||||
this.codecOptions = codecOptions;
|
||||
@@ -62,8 +64,8 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
}
|
||||
|
||||
public void streamScreen(Device device, Callbacks callbacks) throws IOException {
|
||||
MediaCodec codec = createCodec(encoderName);
|
||||
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
|
||||
MediaCodec codec = createCodec(videoMimeType, encoderName);
|
||||
MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions);
|
||||
IBinder display = createDisplay();
|
||||
device.setRotationListener(this);
|
||||
boolean alive;
|
||||
@@ -194,28 +196,28 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
return !eof;
|
||||
}
|
||||
|
||||
private static MediaCodecInfo[] listEncoders() {
|
||||
private static MediaCodecInfo[] listEncoders(String videoMimeType) {
|
||||
List<MediaCodecInfo> result = new ArrayList<>();
|
||||
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||
for (MediaCodecInfo codecInfo : list.getCodecInfos()) {
|
||||
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
|
||||
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(videoMimeType)) {
|
||||
result.add(codecInfo);
|
||||
}
|
||||
}
|
||||
return result.toArray(new MediaCodecInfo[result.size()]);
|
||||
}
|
||||
|
||||
private static MediaCodec createCodec(String encoderName) throws IOException {
|
||||
private static MediaCodec createCodec(String videoMimeType, String encoderName) throws IOException {
|
||||
if (encoderName != null) {
|
||||
Ln.d("Creating encoder by name: '" + encoderName + "'");
|
||||
try {
|
||||
return MediaCodec.createByCodecName(encoderName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
MediaCodecInfo[] encoders = listEncoders();
|
||||
MediaCodecInfo[] encoders = listEncoders(videoMimeType);
|
||||
throw new InvalidEncoderException(encoderName, encoders);
|
||||
}
|
||||
}
|
||||
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||
MediaCodec codec = MediaCodec.createEncoderByType(videoMimeType);
|
||||
Ln.d("Using encoder: '" + codec.getName() + "'");
|
||||
return codec;
|
||||
}
|
||||
@@ -237,9 +239,9 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
|
||||
}
|
||||
|
||||
private static MediaFormat createFormat(int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||
MediaFormat format = new MediaFormat();
|
||||
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||
format.setString(MediaFormat.KEY_MIME, videoMimeType);
|
||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
||||
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
|
||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
|
||||
|
||||
@@ -85,12 +85,13 @@ public final class Server {
|
||||
}
|
||||
|
||||
try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) {
|
||||
VideoCodec codec = options.getCodec();
|
||||
if (options.getSendDeviceMeta()) {
|
||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||
}
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName(),
|
||||
options.getDownsizeOnError());
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(codec.getMimeType(), options.getBitRate(), options.getMaxFps(), codecOptions,
|
||||
options.getEncoderName(), options.getDownsizeOnError());
|
||||
|
||||
Controller controller = null;
|
||||
if (control) {
|
||||
@@ -104,15 +105,27 @@ public final class Server {
|
||||
try {
|
||||
// synchronous
|
||||
VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), options.getSendFrameMeta());
|
||||
if (options.getSendCodecId()) {
|
||||
videoStreamer.writeHeader(codec.getId());
|
||||
}
|
||||
screenEncoder.streamScreen(device, videoStreamer);
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Screen streaming stopped");
|
||||
} finally {
|
||||
Ln.d("Screen streaming stopped");
|
||||
initThread.interrupt();
|
||||
if (controller != null) {
|
||||
controller.stop();
|
||||
}
|
||||
|
||||
try {
|
||||
initThread.join();
|
||||
if (controller != null) {
|
||||
controller.join();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,6 +169,13 @@ public final class Server {
|
||||
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
|
||||
options.setLogLevel(level);
|
||||
break;
|
||||
case "codec":
|
||||
VideoCodec codec = VideoCodec.findByName(value);
|
||||
if (codec == null) {
|
||||
throw new IllegalArgumentException("Video codec " + value + " not supported");
|
||||
}
|
||||
options.setCodec(codec);
|
||||
break;
|
||||
case "max_size":
|
||||
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
|
||||
options.setMaxSize(maxSize);
|
||||
@@ -237,12 +257,17 @@ public final class Server {
|
||||
boolean sendDummyByte = Boolean.parseBoolean(value);
|
||||
options.setSendDummyByte(sendDummyByte);
|
||||
break;
|
||||
case "send_codec_id":
|
||||
boolean sendCodecId = Boolean.parseBoolean(value);
|
||||
options.setSendCodecId(sendCodecId);
|
||||
break;
|
||||
case "raw_video_stream":
|
||||
boolean rawVideoStream = Boolean.parseBoolean(value);
|
||||
if (rawVideoStream) {
|
||||
options.setSendDeviceMeta(false);
|
||||
options.setSendFrameMeta(false);
|
||||
options.setSendDummyByte(false);
|
||||
options.setSendCodecId(false);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
36
server/src/main/java/com/genymobile/scrcpy/VideoCodec.java
Normal file
36
server/src/main/java/com/genymobile/scrcpy/VideoCodec.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.media.MediaFormat;
|
||||
|
||||
public enum VideoCodec {
|
||||
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC),
|
||||
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC),
|
||||
AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1);
|
||||
|
||||
private final int id; // 4-byte ASCII representation of the name
|
||||
private final String name;
|
||||
private final String mimeType;
|
||||
|
||||
VideoCodec(int id, String name, String mimeType) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public static VideoCodec findByName(String name) {
|
||||
for (VideoCodec codec : values()) {
|
||||
if (codec.name.equals(name)) {
|
||||
return codec;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,13 @@ public final class VideoStreamer implements ScreenEncoder.Callbacks {
|
||||
this.sendFrameMeta = sendFrameMeta;
|
||||
}
|
||||
|
||||
public void writeHeader(int codecId) throws IOException {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
buffer.putInt(codecId);
|
||||
buffer.flip();
|
||||
IO.writeFully(fd, buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
||||
if (sendFrameMeta) {
|
||||
|
||||
@@ -30,6 +30,8 @@ public final class SurfaceControl {
|
||||
|
||||
private static Method getBuiltInDisplayMethod;
|
||||
private static Method setDisplayPowerModeMethod;
|
||||
private static Method getPhysicalDisplayTokenMethod;
|
||||
private static Method getPhysicalDisplayIdsMethod;
|
||||
|
||||
private SurfaceControl() {
|
||||
// only static methods
|
||||
@@ -98,7 +100,6 @@ public final class SurfaceControl {
|
||||
}
|
||||
|
||||
public static IBinder getBuiltInDisplay() {
|
||||
|
||||
try {
|
||||
Method method = getGetBuiltInDisplayMethod();
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
@@ -114,6 +115,40 @@ public final class SurfaceControl {
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException {
|
||||
if (getPhysicalDisplayTokenMethod == null) {
|
||||
getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class);
|
||||
}
|
||||
return getPhysicalDisplayTokenMethod;
|
||||
}
|
||||
|
||||
public static IBinder getPhysicalDisplayToken(long physicalDisplayId) {
|
||||
try {
|
||||
Method method = getGetPhysicalDisplayTokenMethod();
|
||||
return (IBinder) method.invoke(null, physicalDisplayId);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException {
|
||||
if (getPhysicalDisplayIdsMethod == null) {
|
||||
getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds");
|
||||
}
|
||||
return getPhysicalDisplayIdsMethod;
|
||||
}
|
||||
|
||||
public static long[] getPhysicalDisplayIds() {
|
||||
try {
|
||||
Method method = getGetPhysicalDisplayIdsMethod();
|
||||
return (long[]) method.invoke(null);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException {
|
||||
if (setDisplayPowerModeMethod == null) {
|
||||
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);
|
||||
|
||||
Reference in New Issue
Block a user