From 3e517cd40eb50f8afb5137f39b0676fead58902a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Feb 2023 12:35:37 +0100 Subject: [PATCH] Add option to select video codec Introduce the selection mechanism. Alternative codecs will be added in further commits. PR #3713 --- README.md | 18 +++++++--- app/scrcpy.1 | 4 +++ app/src/cli.c | 22 ++++++++++++ app/src/demuxer.c | 29 ++++++++++++++-- app/src/options.c | 1 + app/src/options.h | 5 +++ app/src/scrcpy.c | 1 + app/src/server.c | 13 +++++++ app/src/server.h | 1 + .../java/com/genymobile/scrcpy/Options.java | 20 +++++++++++ .../com/genymobile/scrcpy/ScreenEncoder.java | 22 ++++++------ .../java/com/genymobile/scrcpy/Server.java | 20 +++++++++-- .../com/genymobile/scrcpy/VideoCodec.java | 34 +++++++++++++++++++ .../com/genymobile/scrcpy/VideoStreamer.java | 7 ++++ 14 files changed, 179 insertions(+), 18 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/VideoCodec.java diff --git a/README.md b/README.md index b2a767fd..facdfc81 100644 --- a/README.md +++ b/README.md @@ -252,10 +252,19 @@ 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: + +```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 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: ```bash -scrcpy --encoder=_ +scrcpy --encoder=_ # for the default codec +scrcpy --codec=h264 --encoder=_ # for a specific codec ``` ### Capture diff --git a/app/scrcpy.1 b/app/scrcpy.1 index a828a239..c7ddeefb 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -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). + .TP .BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] Set a list of comma-separated key:type=value options for the device encoder. diff --git a/app/src/cli.c b/app/src/cli.c index 538dd3e7..ee65e4c0 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -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).", + }, { .longopt_id = OPT_CODEC_OPTIONS, .longopt = "codec-options", @@ -1377,6 +1384,16 @@ 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; + } + LOGE("Unsupported codec: %s (expected h264)", 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 +1627,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; diff --git a/app/src/demuxer.c b/app/src/demuxer.c index c88af220..ad0b40a2 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -17,6 +17,25 @@ #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 sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { // The video stream contains raw packets, without time information. When we @@ -171,7 +190,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; @@ -188,7 +213,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; diff --git a/app/src/options.c b/app/src/options.c index 8b2624d9..525795ae 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -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 = { diff --git a/app/src/options.h b/app/src/options.h index 7e542c06..18006e42 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -23,6 +23,10 @@ enum sc_record_format { SC_RECORD_FORMAT_MKV, }; +enum sc_codec { + SC_CODEC_H264, +}; + enum sc_lock_video_orientation { SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, // lock the current orientation when scrcpy starts @@ -93,6 +97,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; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 14688471..e85536e6 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -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, diff --git a/app/src/server.c b/app/src/server.c index 05e040cd..aaa0ffb1 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -156,6 +156,16 @@ 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"; + default: + return NULL; + } +} + static sc_pid execute_server(struct sc_server *server, 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("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); } diff --git a/app/src/server.h b/app/src/server.h index e0f2c225..950ad532 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -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; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 171d6661..d43a60a2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -5,9 +5,12 @@ import android.graphics.Rect; 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; @@ -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; @@ -54,6 +58,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 +217,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; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 1d66c0d1..fed6f6c3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -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 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 codecOptions, String encoderName, boolean downsizeOnError) { + public ScreenEncoder(String videoMimeType, int bitRate, int maxFps, List 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 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 codecOptions) { + private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List 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); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 46612069..81c3e813 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -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,6 +105,9 @@ 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 @@ -156,6 +160,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 +248,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: diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java new file mode 100644 index 00000000..e5edfcf2 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java @@ -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; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java b/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java index 77e51499..943c641d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java @@ -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) {