Add option to select video codec

Introduce the selection mechanism. Alternative codecs will be added in
further commits.

PR #3713 <https://github.com/Genymobile/scrcpy/pull/3713>
This commit is contained in:
Romain Vimont 2023-02-03 12:35:37 +01:00
parent f70f6cdd3e
commit 3e517cd40e
14 changed files with 179 additions and 18 deletions

View file

@ -252,10 +252,19 @@ This affects recording orientation.
The [window may also be rotated](#rotation) independently. 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 The video codec can be selected:
crash. It is possible to select a different encoder:
```bash
scrcpy --codec=h264 # default
```
##### 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 ```bash
scrcpy --encoder=OMX.qcom.video.encoder.avc scrcpy --encoder=OMX.qcom.video.encoder.avc
@ -265,7 +274,8 @@ To list the available encoders, you can pass an invalid encoder name; the
error will give the available encoders: error will give the available encoders:
```bash ```bash
scrcpy --encoder=_ scrcpy --encoder=_ # for the default codec
scrcpy --codec=h264 --encoder=_ # for a specific codec
``` ```
### Capture ### Capture

View file

@ -25,6 +25,10 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are
Default is 8000000. Default is 8000000.
.TP
.BI "\-\-codec " name
Select a video codec (h264).
.TP .TP
.BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] .BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...]
Set a list of comma-separated key:type=value options for the device encoder. Set a list of comma-separated key:type=value options for the device encoder.

View file

@ -57,6 +57,7 @@
#define OPT_NO_CLEANUP 1037 #define OPT_NO_CLEANUP 1037
#define OPT_PRINT_FPS 1038 #define OPT_PRINT_FPS 1038
#define OPT_NO_POWER_ON 1039 #define OPT_NO_POWER_ON 1039
#define OPT_CODEC 1040
struct sc_option { struct sc_option {
char shortopt; char shortopt;
@ -105,6 +106,12 @@ static const struct sc_option options[] = {
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
"Default is " STR(DEFAULT_BIT_RATE) ".", "Default is " STR(DEFAULT_BIT_RATE) ".",
}, },
{
.longopt_id = OPT_CODEC,
.longopt = "codec",
.argdesc = "name",
.text = "Select a video codec (h264).",
},
{ {
.longopt_id = OPT_CODEC_OPTIONS, .longopt_id = OPT_CODEC_OPTIONS,
.longopt = "codec-options", .longopt = "codec-options",
@ -1377,6 +1384,16 @@ guess_record_format(const char *filename) {
return 0; return 0;
} }
static bool
parse_codec(const char *optarg, enum sc_codec *codec) {
if (!strcmp(optarg, "h264")) {
*codec = SC_CODEC_H264;
return true;
}
LOGE("Unsupported codec: %s (expected h264)", optarg);
return false;
}
static bool static bool
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
const char *optstring, const struct option *longopts) { const char *optstring, const struct option *longopts) {
@ -1610,6 +1627,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_PRINT_FPS: case OPT_PRINT_FPS:
opts->start_fps_counter = true; opts->start_fps_counter = true;
break; break;
case OPT_CODEC:
if (!parse_codec(optarg, &opts->codec)) {
return false;
}
break;
case OPT_OTG: case OPT_OTG:
#ifdef HAVE_USB #ifdef HAVE_USB
opts->otg = true; opts->otg = true;

View file

@ -17,6 +17,25 @@
#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1) #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
uint32_t codec_id = sc_read32be(data);
switch (codec_id) {
case SC_CODEC_ID_H264:
return AV_CODEC_ID_H264;
default:
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
return AV_CODEC_ID_NONE;
}
}
static bool static bool
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
// The video stream contains raw packets, without time information. When we // The video stream contains raw packets, without time information. When we
@ -171,7 +190,13 @@ static int
run_demuxer(void *data) { run_demuxer(void *data) {
struct sc_demuxer *demuxer = 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) { if (!codec) {
LOGE("H.264 decoder not found"); LOGE("H.264 decoder not found");
goto end; goto end;
@ -188,7 +213,7 @@ run_demuxer(void *data) {
goto finally_free_codec_ctx; goto finally_free_codec_ctx;
} }
demuxer->parser = av_parser_init(AV_CODEC_ID_H264); demuxer->parser = av_parser_init(codec_id);
if (!demuxer->parser) { if (!demuxer->parser) {
LOGE("Could not initialize parser"); LOGE("Could not initialize parser");
goto finally_close_sinks; goto finally_close_sinks;

View file

@ -13,6 +13,7 @@ const struct scrcpy_options scrcpy_options_default = {
.v4l2_device = NULL, .v4l2_device = NULL,
#endif #endif
.log_level = SC_LOG_LEVEL_INFO, .log_level = SC_LOG_LEVEL_INFO,
.codec = SC_CODEC_H264,
.record_format = SC_RECORD_FORMAT_AUTO, .record_format = SC_RECORD_FORMAT_AUTO,
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT,
.port_range = { .port_range = {

View file

@ -23,6 +23,10 @@ enum sc_record_format {
SC_RECORD_FORMAT_MKV, SC_RECORD_FORMAT_MKV,
}; };
enum sc_codec {
SC_CODEC_H264,
};
enum sc_lock_video_orientation { enum sc_lock_video_orientation {
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
// lock the current orientation when scrcpy starts // lock the current orientation when scrcpy starts
@ -93,6 +97,7 @@ struct scrcpy_options {
const char *v4l2_device; const char *v4l2_device;
#endif #endif
enum sc_log_level log_level; enum sc_log_level log_level;
enum sc_codec codec;
enum sc_record_format record_format; enum sc_record_format record_format;
enum sc_keyboard_input_mode keyboard_input_mode; enum sc_keyboard_input_mode keyboard_input_mode;
enum sc_mouse_input_mode mouse_input_mode; enum sc_mouse_input_mode mouse_input_mode;

View file

@ -315,6 +315,7 @@ scrcpy(struct scrcpy_options *options) {
.select_usb = options->select_usb, .select_usb = options->select_usb,
.select_tcpip = options->select_tcpip, .select_tcpip = options->select_tcpip,
.log_level = options->log_level, .log_level = options->log_level,
.codec = options->codec,
.crop = options->crop, .crop = options->crop,
.port_range = options->port_range, .port_range = options->port_range,
.tunnel_host = options->tunnel_host, .tunnel_host = options->tunnel_host,

View file

@ -156,6 +156,16 @@ sc_server_sleep(struct sc_server *server, sc_tick deadline) {
return !stopped; return !stopped;
} }
static const char *
sc_server_get_codec_name(enum sc_codec codec) {
switch (codec) {
case SC_CODEC_H264:
return "h264";
default:
return NULL;
}
}
static sc_pid static sc_pid
execute_server(struct sc_server *server, execute_server(struct sc_server *server,
const struct sc_server_params *params) { const struct sc_server_params *params) {
@ -203,6 +213,9 @@ execute_server(struct sc_server *server,
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate); 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) { if (params->max_size) {
ADD_PARAM("max_size=%" PRIu16, params->max_size); ADD_PARAM("max_size=%" PRIu16, params->max_size);
} }

View file

@ -25,6 +25,7 @@ struct sc_server_params {
uint32_t uid; uint32_t uid;
const char *req_serial; const char *req_serial;
enum sc_log_level log_level; enum sc_log_level log_level;
enum sc_codec codec;
const char *crop; const char *crop;
const char *codec_options; const char *codec_options;
const char *encoder_name; const char *encoder_name;

View file

@ -5,9 +5,12 @@ import android.graphics.Rect;
import java.util.List; import java.util.List;
public class Options { public class Options {
private static final String VIDEO_CODEC_H264 = "h264";
private Ln.Level logLevel = Ln.Level.DEBUG; private Ln.Level logLevel = Ln.Level.DEBUG;
private int uid = -1; // 31-bit non-negative value, or -1 private int uid = -1; // 31-bit non-negative value, or -1
private int maxSize; private int maxSize;
private VideoCodec codec = VideoCodec.H264;
private int bitRate = 8000000; private int bitRate = 8000000;
private int maxFps; private int maxFps;
private int lockVideoOrientation = -1; private int lockVideoOrientation = -1;
@ -29,6 +32,7 @@ public class Options {
private boolean sendDeviceMeta = true; // send device name and size private boolean sendDeviceMeta = true; // send device name and size
private boolean sendFrameMeta = true; // send PTS so that the client may record properly 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 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() { public Ln.Level getLogLevel() {
return logLevel; return logLevel;
@ -54,6 +58,14 @@ public class Options {
this.maxSize = maxSize; this.maxSize = maxSize;
} }
public VideoCodec getCodec() {
return codec;
}
public void setCodec(VideoCodec codec) {
this.codec = codec;
}
public int getBitRate() { public int getBitRate() {
return bitRate; return bitRate;
} }
@ -205,4 +217,12 @@ public class Options {
public void setSendDummyByte(boolean sendDummyByte) { public void setSendDummyByte(boolean sendDummyByte) {
this.sendDummyByte = sendDummyByte; this.sendDummyByte = sendDummyByte;
} }
public boolean getSendCodecId() {
return sendCodecId;
}
public void setSendCodecId(boolean sendCodecId) {
this.sendCodecId = sendCodecId;
}
} }

View file

@ -35,6 +35,7 @@ public class ScreenEncoder implements Device.RotationListener {
private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final String videoMimeType;
private final String encoderName; private final String encoderName;
private final List<CodecOption> codecOptions; private final List<CodecOption> codecOptions;
private final int bitRate; private final int bitRate;
@ -44,7 +45,8 @@ public class ScreenEncoder implements Device.RotationListener {
private boolean firstFrameSent; private boolean firstFrameSent;
private int consecutiveErrors; 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.bitRate = bitRate;
this.maxFps = maxFps; this.maxFps = maxFps;
this.codecOptions = codecOptions; this.codecOptions = codecOptions;
@ -62,8 +64,8 @@ public class ScreenEncoder implements Device.RotationListener {
} }
public void streamScreen(Device device, Callbacks callbacks) throws IOException { public void streamScreen(Device device, Callbacks callbacks) throws IOException {
MediaCodec codec = createCodec(encoderName); MediaCodec codec = createCodec(videoMimeType, encoderName);
MediaFormat format = createFormat(bitRate, maxFps, codecOptions); MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions);
IBinder display = createDisplay(); IBinder display = createDisplay();
device.setRotationListener(this); device.setRotationListener(this);
boolean alive; boolean alive;
@ -194,28 +196,28 @@ public class ScreenEncoder implements Device.RotationListener {
return !eof; return !eof;
} }
private static MediaCodecInfo[] listEncoders() { private static MediaCodecInfo[] listEncoders(String videoMimeType) {
List<MediaCodecInfo> result = new ArrayList<>(); List<MediaCodecInfo> result = new ArrayList<>();
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
for (MediaCodecInfo codecInfo : list.getCodecInfos()) { 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); result.add(codecInfo);
} }
} }
return result.toArray(new MediaCodecInfo[result.size()]); 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) { if (encoderName != null) {
Ln.d("Creating encoder by name: '" + encoderName + "'"); Ln.d("Creating encoder by name: '" + encoderName + "'");
try { try {
return MediaCodec.createByCodecName(encoderName); return MediaCodec.createByCodecName(encoderName);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
MediaCodecInfo[] encoders = listEncoders(); MediaCodecInfo[] encoders = listEncoders(videoMimeType);
throw new InvalidEncoderException(encoderName, encoders); throw new InvalidEncoderException(encoderName, encoders);
} }
} }
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); MediaCodec codec = MediaCodec.createEncoderByType(videoMimeType);
Ln.d("Using encoder: '" + codec.getName() + "'"); Ln.d("Using encoder: '" + codec.getName() + "'");
return codec; return codec;
} }
@ -237,9 +239,9 @@ public class ScreenEncoder implements Device.RotationListener {
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value); 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(); 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); 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 // 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); format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);

View file

@ -85,12 +85,13 @@ public final class Server {
} }
try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) { try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) {
VideoCodec codec = options.getCodec();
if (options.getSendDeviceMeta()) { if (options.getSendDeviceMeta()) {
Size videoSize = device.getScreenInfo().getVideoSize(); Size videoSize = device.getScreenInfo().getVideoSize();
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
} }
ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName(), ScreenEncoder screenEncoder = new ScreenEncoder(codec.getMimeType(), options.getBitRate(), options.getMaxFps(), codecOptions,
options.getDownsizeOnError()); options.getEncoderName(), options.getDownsizeOnError());
Controller controller = null; Controller controller = null;
if (control) { if (control) {
@ -104,6 +105,9 @@ public final class Server {
try { try {
// synchronous // synchronous
VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), options.getSendFrameMeta()); VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), options.getSendFrameMeta());
if (options.getSendCodecId()) {
videoStreamer.writeHeader(codec.getId());
}
screenEncoder.streamScreen(device, videoStreamer); screenEncoder.streamScreen(device, videoStreamer);
} catch (IOException e) { } catch (IOException e) {
// this is expected on close // this is expected on close
@ -156,6 +160,13 @@ public final class Server {
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
options.setLogLevel(level); options.setLogLevel(level);
break; 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": case "max_size":
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8 int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
options.setMaxSize(maxSize); options.setMaxSize(maxSize);
@ -237,12 +248,17 @@ public final class Server {
boolean sendDummyByte = Boolean.parseBoolean(value); boolean sendDummyByte = Boolean.parseBoolean(value);
options.setSendDummyByte(sendDummyByte); options.setSendDummyByte(sendDummyByte);
break; break;
case "send_codec_id":
boolean sendCodecId = Boolean.parseBoolean(value);
options.setSendCodecId(sendCodecId);
break;
case "raw_video_stream": case "raw_video_stream":
boolean rawVideoStream = Boolean.parseBoolean(value); boolean rawVideoStream = Boolean.parseBoolean(value);
if (rawVideoStream) { if (rawVideoStream) {
options.setSendDeviceMeta(false); options.setSendDeviceMeta(false);
options.setSendFrameMeta(false); options.setSendFrameMeta(false);
options.setSendDummyByte(false); options.setSendDummyByte(false);
options.setSendCodecId(false);
} }
break; break;
default: default:

View file

@ -0,0 +1,34 @@
package com.genymobile.scrcpy;
import android.media.MediaFormat;
public enum VideoCodec {
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC);
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;
}
}

View file

@ -21,6 +21,13 @@ public final class VideoStreamer implements ScreenEncoder.Callbacks {
this.sendFrameMeta = sendFrameMeta; 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 @Override
public void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException { public void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
if (sendFrameMeta) { if (sendFrameMeta) {