mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-07-29 12:19:15 +00:00
Add --codec-options
Add a command-line parameter to pass custom options to the device encoder (as a comma-separated list of "key[:type]=value"). The list of possible codec options is available in the Android documentation: <https://d.android.com/reference/android/media/MediaFormat> PR #1325 <https://github.com/Genymobile/scrcpy/pull/1325> Refs #1226 <https://github.com/Genymobile/scrcpy/pull/1226> Co-authored-by: Romain Vimont <rom@rom1v.com>
This commit is contained in:
parent
c7155a1954
commit
080a4ee365
11 changed files with 302 additions and 5 deletions
112
server/src/main/java/com/genymobile/scrcpy/CodecOption.java
Normal file
112
server/src/main/java/com/genymobile/scrcpy/CodecOption.java
Normal file
|
@ -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<CodecOption> parse(String codecOptions) {
|
||||
if ("-".equals(codecOptions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<CodecOption> 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<CodecOption> 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<CodecOption> 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<CodecOption> 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 {
|
|||
// <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
|
||||
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
|
||||
}
|
||||
|
||||
if (codecOptions != null) {
|
||||
for (CodecOption option : codecOptions) {
|
||||
setCodecOption(format, option);
|
||||
}
|
||||
}
|
||||
|
||||
return format;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<CodecOption> 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;
|
||||
}
|
||||
|
||||
|
|
114
server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java
Normal file
114
server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java
Normal file
|
@ -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<CodecOption> 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<CodecOption> 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<CodecOption> 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<CodecOption> 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<CodecOption> 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<CodecOption> 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<CodecOption> 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());
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue