diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecOptions.java b/server/src/main/java/com/genymobile/scrcpy/CodecOptions.java new file mode 100644 index 00000000..0b6ae152 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CodecOptions.java @@ -0,0 +1,25 @@ +package com.genymobile.scrcpy; + +import java.util.HashMap; + +public class CodecOptions { + static final String PROFILE_OPTION = "profile"; + static final String LEVEL_OPTION = "level"; + + private HashMap options; + + CodecOptions(HashMap options) { + this.options = options; + } + + Object parseValue(String profileOption) { + String value = options.get(profileOption); + switch (profileOption) { + case PROFILE_OPTION: + case LEVEL_OPTION: + return NumberUtils.tryParseInt(value); + default: + return null; + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/NumberUtils.java b/server/src/main/java/com/genymobile/scrcpy/NumberUtils.java new file mode 100644 index 00000000..dafaaea4 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/NumberUtils.java @@ -0,0 +1,16 @@ +package com.genymobile.scrcpy; + +public class NumberUtils { + + public static int tryParseInt(final String str) { + return tryParseInt(str, 0); + } + + public static int tryParseInt(final String str, int defaultValue) { + try { + return Integer.parseInt(str); + } catch (NumberFormatException e) { + return defaultValue; + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 2b87b952..3ccba37f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -11,7 +11,7 @@ public class Options { private Rect crop; private boolean sendFrameMeta; // send PTS so that the client may record properly private boolean control; - private int codecProfile; + private CodecOptions codecOptions; public int getMaxSize() { return maxSize; @@ -77,11 +77,11 @@ public class Options { this.control = control; } - public int getCodecProfile() { - return codecProfile; + public CodecOptions getCodecOptions() { + return codecOptions; } - public void setCodecProfile(int codecProfile) { - this.codecProfile = codecProfile; + public void setCodecOptions(CodecOptions 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 b886affb..7f5aaf7f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -15,6 +15,8 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.util.concurrent.atomic.AtomicBoolean; +import static android.media.MediaFormat.MIMETYPE_VIDEO_AVC; + public class ScreenEncoder implements Device.RotationListener { private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds @@ -30,21 +32,21 @@ public class ScreenEncoder implements Device.RotationListener { private int maxFps; private int lockedVideoOrientation; private int iFrameInterval; - private int codecProfile; private boolean sendFrameMeta; private long ptsOrigin; + private CodecOptions codecOptions; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation, int codecProfile, int iFrameInterval) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation, CodecOptions codecOptions, int iFrameInterval) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; this.lockedVideoOrientation = lockedVideoOrientation; - this.codecProfile = codecProfile; + this.codecOptions = codecOptions; this.iFrameInterval = iFrameInterval; } - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation, int codecProfile) { - this(sendFrameMeta, bitRate, maxFps, lockedVideoOrientation, codecProfile, DEFAULT_I_FRAME_INTERVAL); + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation, CodecOptions codecOptions) { + this(sendFrameMeta, bitRate, maxFps, lockedVideoOrientation, codecOptions, DEFAULT_I_FRAME_INTERVAL); } @Override @@ -142,30 +144,35 @@ public class ScreenEncoder implements Device.RotationListener { IO.writeFully(fd, headerBuffer); } - private void setCodecProfile(MediaCodec codec, MediaFormat format) throws IOException { - if(codecProfile == 0) return; - int level = 0; - for (MediaCodecInfo.CodecProfileLevel profileLevel : codec.getCodecInfo().getCapabilitiesForType("video/avc").profileLevels) { - if(profileLevel.profile == codecProfile) { + private void setCodecProfile(MediaCodec codec, MediaFormat format) { + int profile = (int)codecOptions.parseValue(CodecOptions.PROFILE_OPTION); + int level = (int)codecOptions.parseValue(CodecOptions.LEVEL_OPTION); + if(profile == 0) return; + for (MediaCodecInfo.CodecProfileLevel profileLevel : codec.getCodecInfo().getCapabilitiesForType(MIMETYPE_VIDEO_AVC).profileLevels) { + if(profileLevel.profile == profile) { level = Math.max(level, profileLevel.level); } } - if(level == 0) throw new IOException("Device doesn't support the requested codec profile."); - // Profile (SDK Level 21) and Level (SDK Level 23). - format.setInteger(MediaFormat.KEY_PROFILE, codecProfile); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - format.setInteger(MediaFormat.KEY_LEVEL, level); + if(level == 0) { + Ln.w("Device doesn't support the requested codec profile.\n" + + "Profile and level will be chosen automatically."); + } else { + // Profile (SDK Level 21) and Level (SDK Level 23). + format.setInteger(MediaFormat.KEY_PROFILE, profile); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + format.setInteger(MediaFormat.KEY_LEVEL, level); + } } } private static MediaCodec createCodec() throws IOException { - return MediaCodec.createEncoderByType("video/avc"); + return MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC); } @SuppressWarnings("checkstyle:MagicNumber") private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) { MediaFormat format = new MediaFormat(); - format.setString(MediaFormat.KEY_MIME, "video/avc"); + format.setString(MediaFormat.KEY_MIME, MIMETYPE_VIDEO_AVC); 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 71d02abf..7dd53ff4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -6,6 +6,7 @@ import android.os.Build; import java.io.File; import java.io.IOException; +import java.util.HashMap; public final class Server { @@ -20,7 +21,7 @@ public final class Server { boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), - options.getLockedVideoOrientation(), options.getCodecProfile()); + options.getLockedVideoOrientation(), options.getCodecOptions()); if (options.getControl()) { Controller controller = new Controller(device, connection); @@ -98,8 +99,8 @@ public final class Server { int lockedVideoOrientation = Integer.parseInt(args[4]); options.setLockedVideoOrientation(lockedVideoOrientation); - int codecProfile = Integer.parseInt(args[5]); - options.setCodecProfile(codecProfile); + CodecOptions codecOptions = parseCodecOptions(args[5]); + options.setCodecOptions(codecOptions); // use "adb forward" instead of "adb tunnel"? (so the server must listen) boolean tunnelForward = Boolean.parseBoolean(args[6]); @@ -134,6 +135,18 @@ public final class Server { return new Rect(x, y, x + width, y + height); } + private static CodecOptions parseCodecOptions(String codecOptions) { + HashMap codecOptionsMap = new HashMap<>(); + if (!"-".equals(codecOptions)) { + String[] pairs = codecOptions.split(","); + for (String pair : pairs) { + String[] option = pair.split("="); + codecOptionsMap.put(option[0], option.length > 1 ? option[1] : null); + } + } + return new CodecOptions(codecOptionsMap); + } + private static void unlinkSelf() { try { new File(SERVER_PATH).delete();