diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 02389159..6920e9a4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -25,6 +25,16 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are Default is 8000000. +.TP +.BI "\-\-codec\-options " key[:type]=value[,...] +Set a list of comma-separated key:type=value options for the device encoder. + +The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'. + +The list of possible codec options is available in the Android documentation +.UR https://d.android.com/reference/android/media/MediaFormat +.UE . + .TP .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy Crop the device screen on the server. diff --git a/app/src/cli.c b/app/src/cli.c index 0c68279f..30272828 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -30,6 +30,15 @@ scrcpy_print_usage(const char *arg0) { " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" " Default is %d.\n" "\n" + " --codec-options key[:type]=value[,...]\n" + " Set a list of comma-separated key:type=value options for the\n" + " device encoder.\n" + " The possible values for 'type' are 'int' (default), 'long',\n" + " 'float' and 'string'.\n" + " The list of possible codec options is available in the\n" + " Android documentation:\n" + " \n" + "\n" " --crop width:height:x:y\n" " Crop the device screen on the server.\n" " The values are expressed in the device natural orientation\n" @@ -472,12 +481,14 @@ guess_record_format(const char *filename) { #define OPT_ROTATION 1015 #define OPT_RENDER_DRIVER 1016 #define OPT_NO_MIPMAPS 1017 +#define OPT_CODEC_OPTIONS 1018 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { static const struct option long_options[] = { {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, {"bit-rate", required_argument, NULL, 'b'}, + {"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS}, {"crop", required_argument, NULL, OPT_CROP}, {"display", required_argument, NULL, OPT_DISPLAY_ID}, {"fullscreen", no_argument, NULL, 'f'}, @@ -647,6 +658,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_NO_MIPMAPS: opts->mipmaps = false; break; + case OPT_CODEC_OPTIONS: + opts->codec_options = optarg; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 7f8d3c75..05960630 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -303,6 +303,7 @@ scrcpy(const struct scrcpy_options *options) { .display_id = options->display_id, .show_touches = options->show_touches, .stay_awake = options->stay_awake, + .codec_options = options->codec_options, }; if (!server_start(&server, options->serial, ¶ms)) { return false; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 99e72ef0..af2e3bf5 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -16,6 +16,7 @@ struct scrcpy_options { const char *window_title; const char *push_target; const char *render_driver; + const char *codec_options; enum recorder_format record_format; struct port_range port_range; uint16_t max_size; @@ -48,6 +49,7 @@ struct scrcpy_options { .window_title = NULL, \ .push_target = NULL, \ .render_driver = NULL, \ + .codec_options = NULL, \ .record_format = RECORDER_FORMAT_AUTO, \ .port_range = { \ .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ diff --git a/app/src/server.c b/app/src/server.c index b98cec2f..94881e51 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -270,6 +270,7 @@ execute_server(struct server *server, const struct server_params *params) { display_id_string, params->show_touches ? "true" : "false", params->stay_awake ? "true" : "false", + params->codec_options ? params->codec_options : "-", }; #ifdef SERVER_DEBUGGER LOGI("Server debugger waiting for a client on device port " diff --git a/app/src/server.h b/app/src/server.h index 13d0b451..5b51695a 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -44,6 +44,7 @@ struct server { struct server_params { const char *crop; + const char *codec_options; struct port_range port_range; uint16_t max_size; uint32_t bit_rate; diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java new file mode 100644 index 00000000..1897bda3 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java @@ -0,0 +1,112 @@ +package com.genymobile.scrcpy; + +import java.util.ArrayList; +import java.util.List; + +public class CodecOption { + private String key; + private Object value; + + public CodecOption(String key, Object value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public Object getValue() { + return value; + } + + public static List parse(String codecOptions) { + if ("-".equals(codecOptions)) { + return null; + } + + List result = new ArrayList<>(); + + boolean escape = false; + StringBuilder buf = new StringBuilder(); + + for (char c : codecOptions.toCharArray()) { + switch (c) { + case '\\': + if (escape) { + buf.append('\\'); + escape = false; + } else { + escape = true; + } + break; + case ',': + if (escape) { + buf.append(','); + escape = false; + } else { + // This comma is a separator between codec options + String codecOption = buf.toString(); + result.add(parseOption(codecOption)); + // Clear buf + buf.setLength(0); + } + break; + default: + buf.append(c); + break; + } + } + + if (buf.length() > 0) { + String codecOption = buf.toString(); + result.add(parseOption(codecOption)); + } + + return result; + } + + private static CodecOption parseOption(String option) { + int equalSignIndex = option.indexOf('='); + if (equalSignIndex == -1) { + throw new IllegalArgumentException("'=' expected"); + } + String keyAndType = option.substring(0, equalSignIndex); + if (keyAndType.length() == 0) { + throw new IllegalArgumentException("Key may not be null"); + } + + String key; + String type; + + int colonIndex = keyAndType.indexOf(':'); + if (colonIndex != -1) { + key = keyAndType.substring(0, colonIndex); + type = keyAndType.substring(colonIndex + 1); + } else { + key = keyAndType; + type = "int"; // assume int by default + } + + Object value; + String valueString = option.substring(equalSignIndex + 1); + switch (type) { + case "int": + value = Integer.parseInt(valueString); + break; + case "long": + value = Long.parseLong(valueString); + break; + case "float": + value = Float.parseFloat(valueString); + break; + case "string": + value = valueString; + break; + default: + throw new IllegalArgumentException("Invalid codec option type (int, long, float, str): " + type); + } + + return new CodecOption(key, value); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 152aa2f7..d2cd9a15 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -14,6 +14,7 @@ public class Options { private int displayId; private boolean showTouches; private boolean stayAwake; + private String codecOptions; public int getMaxSize() { return maxSize; @@ -102,4 +103,12 @@ public class Options { public void setStayAwake(boolean stayAwake) { this.stayAwake = stayAwake; } + + public String getCodecOptions() { + return codecOptions; + } + + public void setCodecOptions(String codecOptions) { + this.codecOptions = codecOptions; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 651affde..d722388c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -12,6 +12,7 @@ import android.view.Surface; import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; public class ScreenEncoder implements Device.RotationListener { @@ -25,15 +26,17 @@ public class ScreenEncoder implements Device.RotationListener { private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); + private List codecOptions; private int bitRate; private int maxFps; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; + this.codecOptions = codecOptions; } @Override @@ -61,7 +64,7 @@ public class ScreenEncoder implements Device.RotationListener { } private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { - MediaFormat format = createFormat(bitRate, maxFps); + MediaFormat format = createFormat(bitRate, maxFps, codecOptions); device.setRotationListener(this); boolean alive; try { @@ -151,7 +154,24 @@ public class ScreenEncoder implements Device.RotationListener { return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); } - private static MediaFormat createFormat(int bitRate, int maxFps) { + private static void setCodecOption(MediaFormat format, CodecOption codecOption) { + String key = codecOption.getKey(); + Object value = codecOption.getValue(); + + if (value instanceof Integer) { + format.setInteger(key, (Integer) value); + } else if (value instanceof Long) { + format.setLong(key, (Long) value); + } else if (value instanceof Float) { + format.setFloat(key, (Float) value); + } else if (value instanceof String) { + format.setString(key, (String) value); + } + + Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value); + } + + private static MediaFormat createFormat(int bitRate, int maxFps, List codecOptions) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); @@ -167,6 +187,13 @@ public class ScreenEncoder implements Device.RotationListener { // format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps); } + + if (codecOptions != null) { + for (CodecOption option : codecOptions) { + setCodecOption(format, option); + } + } + return format; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index e2c702ba..4ada08e6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -8,6 +8,7 @@ import android.os.BatteryManager; import android.os.Build; import java.io.IOException; +import java.util.List; public final class Server { @@ -19,6 +20,7 @@ public final class Server { private static void scrcpy(Options options) throws IOException { Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); final Device device = new Device(options); + List codecOptions = CodecOption.parse(options.getCodecOptions()); boolean mustDisableShowTouchesOnCleanUp = false; int restoreStayOn = -1; @@ -49,8 +51,9 @@ public final class Server { CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn); boolean tunnelForward = options.isTunnelForward(); + try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { - ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps()); + ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions); if (options.getControl()) { final Controller controller = new Controller(device, connection); @@ -116,7 +119,7 @@ public final class Server { "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - final int expectedParameters = 12; + final int expectedParameters = 13; if (args.length != expectedParameters) { throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters"); } @@ -157,6 +160,9 @@ public final class Server { boolean stayAwake = Boolean.parseBoolean(args[11]); options.setStayAwake(stayAwake); + String codecOptions = args[12]; + options.setCodecOptions(codecOptions); + return options; } diff --git a/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java b/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java new file mode 100644 index 00000000..ad802258 --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java @@ -0,0 +1,114 @@ +package com.genymobile.scrcpy; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class CodecOptionsTest { + + @Test + public void testIntegerImplicit() { + List codecOptions = CodecOption.parse("some_key=5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertEquals(5, option.getValue()); + } + + @Test + public void testInteger() { + List codecOptions = CodecOption.parse("some_key:int=5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Integer); + Assert.assertEquals(5, option.getValue()); + } + + @Test + public void testLong() { + List codecOptions = CodecOption.parse("some_key:long=5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Long); + Assert.assertEquals(5L, option.getValue()); + } + + @Test + public void testFloat() { + List codecOptions = CodecOption.parse("some_key:float=4.5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Float); + Assert.assertEquals(4.5f, option.getValue()); + } + + @Test + public void testString() { + List codecOptions = CodecOption.parse("some_key:string=some_value"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof String); + Assert.assertEquals("some_value", option.getValue()); + } + + @Test + public void testStringEscaped() { + List codecOptions = CodecOption.parse("some_key:string=warning\\,this_is_not=a_new_key"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof String); + Assert.assertEquals("warning,this_is_not=a_new_key", option.getValue()); + } + + @Test + public void testList() { + List codecOptions = CodecOption.parse("a=1,b:int=2,c:long=3,d:float=4.5,e:string=a\\,b=c"); + + Assert.assertEquals(5, codecOptions.size()); + + CodecOption option; + + option = codecOptions.get(0); + Assert.assertEquals("a", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Integer); + Assert.assertEquals(1, option.getValue()); + + option = codecOptions.get(1); + Assert.assertEquals("b", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Integer); + Assert.assertEquals(2, option.getValue()); + + option = codecOptions.get(2); + Assert.assertEquals("c", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Long); + Assert.assertEquals(3L, option.getValue()); + + option = codecOptions.get(3); + Assert.assertEquals("d", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Float); + Assert.assertEquals(4.5f, option.getValue()); + + option = codecOptions.get(4); + Assert.assertEquals("e", option.getKey()); + Assert.assertTrue(option.getValue() instanceof String); + Assert.assertEquals("a,b=c", option.getValue()); + } +}