Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for H265 and AV1 #3713

Merged
merged 3 commits into from
Feb 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions app/scrcpy.1
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
37 changes: 37 additions & 0 deletions app/src/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down
35 changes: 33 additions & 2 deletions app/src/demuxer.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,31 @@

#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;
}

#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);
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_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
// The video stream contains raw packets, without time information. When we
Expand Down Expand Up @@ -171,7 +196,13 @@ static int
run_demuxer(void *data) {
struct sc_demuxer *demuxer = data;

const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
enum AVCodecID codec_id = sc_demuxer_recv_codec_id(demuxer);
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;
Expand All @@ -188,7 +219,7 @@ run_demuxer(void *data) {
goto finally_free_codec_ctx;
}

demuxer->parser = av_parser_init(AV_CODEC_ID_H264);
demuxer->parser = av_parser_init(codec_id);
if (!demuxer->parser) {
LOGE("Could not initialize parser");
goto finally_close_sinks;
Expand Down
1 change: 1 addition & 0 deletions app/src/options.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
7 changes: 7 additions & 0 deletions app/src/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions app/src/scrcpy.c
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,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,
Expand Down
17 changes: 17 additions & 0 deletions app/src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions app/src/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 20 additions & 0 deletions server/src/main/java/com/genymobile/scrcpy/Options.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import java.util.List;

public class Options {
private static final String VIDEO_CODEC_H264 = "h264";

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;
Expand All @@ -29,6 +32,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;
Expand All @@ -54,6 +58,14 @@ public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
}

public VideoCodec getCodec() {
return codec;
}

public void setCodec(VideoCodec codec) {
this.codec = codec;
}

public int getBitRate() {
return bitRate;
}
Expand Down Expand Up @@ -205,4 +217,12 @@ public boolean getSendDummyByte() {
public void setSendDummyByte(boolean sendDummyByte) {
this.sendDummyByte = sendDummyByte;
}

public boolean getSendCodecId() {
return sendCodecId;
}

public void setSendCodecId(boolean sendCodecId) {
this.sendCodecId = sendCodecId;
}
}
22 changes: 12 additions & 10 deletions server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public interface Callbacks {

private final AtomicBoolean rotationChanged = new AtomicBoolean();

private final String videoMimeType;
private final String encoderName;
private final List<CodecOption> codecOptions;
private final int bitRate;
Expand All @@ -44,7 +45,8 @@ public interface Callbacks {
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;
Expand All @@ -62,8 +64,8 @@ public boolean consumeRotationChange() {
}

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;
Expand Down Expand Up @@ -194,28 +196,28 @@ private boolean encode(MediaCodec codec, Callbacks callbacks) throws IOException
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;
}
Expand All @@ -237,9 +239,9 @@ private static void setCodecOption(MediaFormat format, CodecOption codecOption)
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);
Expand Down
Loading