diff --git a/app/scrcpy.1 b/app/scrcpy.1 index b3c57064c5..755bd981c0 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -151,6 +151,14 @@ Set the initial window height. Default is 0 (automatic).\n +.TP +.BI "\-\-\codec\-options " 'options' +options is a list of key=value pairs seperated by comma\n +for the device encoder.\n +For a list of possible codec options:\n +https://developer.android.com/reference/android/media/MediaCodecInfo\n +Currently supported keys: [profile, level].\n + .SH SHORTCUTS .TP diff --git a/app/src/cli.c b/app/src/cli.c index 4b093c49da..f62b7f9fe6 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -130,6 +130,13 @@ scrcpy_print_usage(const char *arg0) { " Set the initial window width.\n" " Default is 0 (automatic).\n" "\n" + " --codec-options 'options'\n" + " 'options' is a list of key=value pairs seperated by comma\n" + " for the device encoder.\n" + " For a list of possible codec options:\n" + " https://developer.android.com/reference/android/media/MediaCodecInfo\n" + " Currently supported keys: [profile, level].\n" + "\n" "Shortcuts:\n" "\n" " " CTRL_OR_CMD "+f\n" @@ -398,6 +405,7 @@ guess_record_format(const char *filename) { #define OPT_WINDOW_BORDERLESS 1011 #define OPT_MAX_FPS 1012 #define OPT_LOCK_VIDEO_ORIENTATION 1013 +#define OPT_CODEC_OPTIONS 1014 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -407,8 +415,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"crop", required_argument, NULL, OPT_CROP}, {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, - {"lock-video-orientation", required_argument, NULL, - OPT_LOCK_VIDEO_ORIENTATION}, + {"lock-video-orientation", required_argument, NULL, OPT_LOCK_VIDEO_ORIENTATION}, {"max-fps", required_argument, NULL, OPT_MAX_FPS}, {"max-size", required_argument, NULL, 'm'}, {"no-control", no_argument, NULL, 'n'}, @@ -417,8 +424,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"record", required_argument, NULL, 'r'}, {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, - {"render-expired-frames", no_argument, NULL, - OPT_RENDER_EXPIRED_FRAMES}, + {"render-expired-frames", no_argument, NULL, OPT_RENDER_EXPIRED_FRAMES}, {"serial", required_argument, NULL, 's'}, {"show-touches", no_argument, NULL, 't'}, {"turn-screen-off", no_argument, NULL, 'S'}, @@ -429,8 +435,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"window-y", required_argument, NULL, OPT_WINDOW_Y}, {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, - {"window-borderless", no_argument, NULL, - OPT_WINDOW_BORDERLESS}, + {"window-borderless", no_argument, NULL, OPT_WINDOW_BORDERLESS}, + {"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS}, {NULL, 0, NULL, 0 }, }; @@ -439,7 +445,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { optind = 0; // reset to start from the first argument in tests int c; - while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, + while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv:", long_options, NULL)) != -1) { switch (c) { case 'b': @@ -549,6 +555,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_PREFER_TEXT: opts->prefer_text = true; 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 4d9ad88b0b..5cc0f616dc 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -286,6 +286,7 @@ scrcpy(const struct scrcpy_options *options) { .max_fps = options->max_fps, .lock_video_orientation = options->lock_video_orientation, .control = options->control, + .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 e29298f2c1..17f2a4795d 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -15,6 +15,7 @@ struct scrcpy_options { const char *record_filename; const char *window_title; const char *push_target; + const char *codec_options; enum recorder_format record_format; struct port_range port_range; uint16_t max_size; @@ -42,6 +43,7 @@ struct scrcpy_options { .record_filename = NULL, \ .window_title = NULL, \ .push_target = 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 4b2c1866cc..55a8151b16 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -254,6 +254,7 @@ execute_server(struct server *server, const struct server_params *params) { bit_rate_string, max_fps_string, lock_video_orientation_string, + params->codec_options ? params->codec_options : "-", server->tunnel_forward ? "true" : "false", params->crop ? params->crop : "-", "true", // always send frame meta (packet boundaries + timestamp) diff --git a/app/src/server.h b/app/src/server.h index d84a5cc8c2..9bc25671df 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -44,6 +44,7 @@ struct server_params { uint16_t max_fps; int8_t lock_video_orientation; bool control; + char *codec_options; }; // init default values 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 0000000000..84b273efc2 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CodecOptions.java @@ -0,0 +1,85 @@ +package com.genymobile.scrcpy; + +import android.media.MediaCodecInfo; +import android.os.Build; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +public class CodecOptions { + + public static final String PROFILE_OPTION = "profile"; + public static final String LEVEL_OPTION = "level"; + + private static final LinkedHashMap levelsTable = new LinkedHashMap() { + { + // Adding all possible level and their properties + // 3rd property, bitrate was added but not sure if needed for now. + // Source: https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels + put(MediaCodecInfo.CodecProfileLevel.AVCLevel1, "485,99,64"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel1b, "485,99,128"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel11, "3000,396,192"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel12, "6000,396,384"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel13, "11880,396,768"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel2, "11880,396,2000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel21, "19800,792,4000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel22, "20250,1620,4000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel3, "40500,1620,10000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel31, "108000,3600,14000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel32, "216000,5120,20000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel4, "245760,8192,20000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel41, "245760,8192,50000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel42, "522240,8704,50000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel5, "589824,22080,135000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel51, "983040,36864,240000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel52, "2073600,36864,240000"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + put(MediaCodecInfo.CodecProfileLevel.AVCLevel6, "4177920,139264,240000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel61, "8355840,139264,480000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel62, "16711680,139264,800000"); + } + } + }; + + private HashMap options; + + CodecOptions(HashMap options) { + this.options = options; + } + + /** + * The purpose of this function is to return the lowest possible codec profile level + * that supports the given width/height/bitrate of the stream + * @param width of the device + * @param height of the device + * @param bitRate at which we stream + * @return the lowest possible level that should support the given properties. + */ + public static int calculateLevel(int width, int height, int bitRate) { + // Calculations source: https://stackoverflow.com/questions/32100635/vlc-2-2-and-levels + int macroblocks = (int)( Math.ceil(width/16.0) * Math.ceil(height/16.0) ); + int macroblocks_s = macroblocks * 60; + for (Map.Entry entry : levelsTable.entrySet()) { + String[] levelProperties = entry.getValue().split(","); + int levelMacroblocks_s = Integer.parseInt(levelProperties[0]); + int levelMacroblocks = Integer.parseInt(levelProperties[1]); + if(macroblocks_s > levelMacroblocks_s) continue; + if(macroblocks > levelMacroblocks) continue; + Ln.i("Level selected based on screen size calculation: " + entry.getKey()); + return entry.getKey(); + } + return 0; + } + + public 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 0000000000..dafaaea486 --- /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 d9a29452b0..3ccba37f5f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -11,6 +11,7 @@ public class Options { private Rect crop; private boolean sendFrameMeta; // send PTS so that the client may record properly private boolean control; + private CodecOptions codecOptions; public int getMaxSize() { return maxSize; @@ -75,4 +76,12 @@ public boolean getControl() { public void setControl(boolean control) { this.control = control; } + + public CodecOptions getCodecOptions() { + return codecOptions; + } + + 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 3e9772ee05..db8f2abda3 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.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 @@ -32,17 +34,19 @@ public class ScreenEncoder implements Device.RotationListener { private int iFrameInterval; private boolean sendFrameMeta; private long ptsOrigin; + private CodecOptions codecOptions; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation, 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.codecOptions = codecOptions; this.iFrameInterval = iFrameInterval; } - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation) { - this(sendFrameMeta, bitRate, maxFps, lockedVideoOrientation, 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 @@ -70,6 +74,7 @@ public void streamScreen(Device device, FileDescriptor fd) throws IOException { Rect videoRect = screenInfo.getVideoSize().toRect(); int videoRotation = device.getVideoRotation(screenInfo.getRotation()); setSize(format, videoRotation, videoRect.width(), videoRect.height()); + setCodecProfile(codec, format, videoRect); configure(codec, format); Surface surface = codec.createInputSurface(); setDisplaySurface(display, surface, videoRotation, contentRect, videoRect); @@ -139,14 +144,52 @@ private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, IO.writeFully(fd, headerBuffer); } + private void setCodecProfile(MediaCodec codec, MediaFormat format, Rect videoRect) { + int profile = (int)codecOptions.parseValue(CodecOptions.PROFILE_OPTION); + int level = (int)codecOptions.parseValue(CodecOptions.LEVEL_OPTION); + int suggestedLevel = CodecOptions.calculateLevel(videoRect.width(), videoRect.height(), bitRate); + boolean profileSupported = false; + + if(profile == 0) return; + for (MediaCodecInfo.CodecProfileLevel profileLevel : + codec.getCodecInfo().getCapabilitiesForType(MIMETYPE_VIDEO_AVC).profileLevels) { + if(profileLevel.profile == profile) { + profileSupported = true; + break; + } + } + if(profileSupported) { + // Profile (SDK Level 21). + format.setInteger(MediaFormat.KEY_PROFILE, profile); + if(level != 0) { + if(suggestedLevel != 0 && level != suggestedLevel) + Ln.w("Requested codec profile level is different from the suggested level"); + } else { + level = suggestedLevel; // If no level was given, use the pre calculated level. + } + // Level (SDK Level 23). + // We ask again because suggested level can be 0 and we cant set the level to 0, + // in that case we let the encoder choose the level. + if (level != 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + format.setInteger(MediaFormat.KEY_LEVEL, level); + } + } + } else { + Ln.w("Device doesn't support the requested codec profile.\n" + + "Profile and level will be chosen automatically."); + } + + } + 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 2b0d32a239..7dd53ff405 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 java.io.File; import java.io.IOException; +import java.util.HashMap; public final class Server { @@ -20,7 +21,7 @@ private static void scrcpy(Options options) throws IOException { boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), - options.getLockedVideoOrientation()); + options.getLockedVideoOrientation(), options.getCodecOptions()); if (options.getControl()) { Controller controller = new Controller(device, connection); @@ -77,11 +78,11 @@ private static Options createOptions(String... args) { String clientVersion = args[0]; if (!clientVersion.equals(BuildConfig.VERSION_NAME)) { throw new IllegalArgumentException( - "The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")"); + "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - if (args.length != 9) { - throw new IllegalArgumentException("Expecting 9 parameters"); + if (args.length != 10) { + throw new IllegalArgumentException("Expecting 10 parameters"); } Options options = new Options(); @@ -98,17 +99,20 @@ private static Options createOptions(String... args) { int lockedVideoOrientation = Integer.parseInt(args[4]); options.setLockedVideoOrientation(lockedVideoOrientation); + 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[5]); + boolean tunnelForward = Boolean.parseBoolean(args[6]); options.setTunnelForward(tunnelForward); - Rect crop = parseCrop(args[6]); + Rect crop = parseCrop(args[7]); options.setCrop(crop); - boolean sendFrameMeta = Boolean.parseBoolean(args[7]); + boolean sendFrameMeta = Boolean.parseBoolean(args[8]); options.setSendFrameMeta(sendFrameMeta); - boolean control = Boolean.parseBoolean(args[8]); + boolean control = Boolean.parseBoolean(args[9]); options.setControl(control); return options; @@ -131,6 +135,18 @@ private static Rect parseCrop(String crop) { 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();