diff --git a/app/scrcpy.1 b/app/scrcpy.1 index b3c57064c5..27ba5d792c 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -151,6 +151,10 @@ Set the initial window height. Default is 0 (automatic).\n +.TP +.BI "\-P, \-\-\codec\-profile " value +Request specific encoding codec profile, see AVC profiles at: https://developer.android.com/reference/android/media/MediaCodecInfo.CodecProfileLevel for valid codec profile values. Default is 0 (automatic). + .SH SHORTCUTS .TP diff --git a/app/src/cli.c b/app/src/cli.c index 4b093c49da..4f998708c5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -130,6 +130,12 @@ scrcpy_print_usage(const char *arg0) { " Set the initial window width.\n" " Default is 0 (automatic).\n" "\n" + " -P, --codec-profile value\n" + " Request specific encoding codec profile, see AVC profiles at:\n" + " https://developer.android.com/reference/android/media/MediaCodecInfo.CodecProfileLevel\n" + " for valid codec profile values.\n" + " Default is 0 (automatic).\n" + "\n" "Shortcuts:\n" "\n" " " CTRL_OR_CMD "+f\n" @@ -368,6 +374,35 @@ parse_record_format(const char *optarg, enum recorder_format *format) { return false; } +static bool +parse_codec_profile(const char *optarg, uint32_t *codec_profile) { + long value; + // long may be 32 bits (it is the case on mingw), so do not use more than + // 31 bits (long is signed) + bool ok = parse_integer_arg(optarg, &value, true, 0, 0x7FFFFFFF, "codec-profile"); + if (!ok) { + return false; + } + + switch (value) { + case 0: // Automatic + case 0x01: // AVCProfileBaseline + case 0x02: // AVCProfileMain + case 0x04: // AVCProfileExtended + case 0x08: // AVCProfileHigh + case 0x10: // AVCProfileHigh10 + case 0x20: // AVCProfileHigh422 + case 0x40: // AVCProfileHigh444 + case 0x10000: // AVCProfileConstrainedBaseline + case 0x80000: // AVCProfileConstrainedHigh + *codec_profile = (uint32_t) value; + return true; + default: + LOGE("Invalid codec-profile, Use -h for help"); + return false; + } +} + static enum recorder_format guess_record_format(const char *filename) { size_t len = strlen(filename); @@ -431,6 +466,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, {"window-borderless", no_argument, NULL, OPT_WINDOW_BORDERLESS}, + {"codec-profile", required_argument, NULL, 'P'}, {NULL, 0, NULL, 0 }, }; @@ -439,7 +475,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:P:", long_options, NULL)) != -1) { switch (c) { case 'b': @@ -549,6 +585,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_PREFER_TEXT: opts->prefer_text = true; break; + case 'P': + if (!parse_codec_profile(optarg, &opts->codec_profile)) { + return false; + } + 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..0057c75b83 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_profile = options->codec_profile, }; if (!server_start(&server, options->serial, ¶ms)) { return false; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index e29298f2c1..4d5ad9fc34 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -34,6 +34,7 @@ struct scrcpy_options { bool render_expired_frames; bool prefer_text; bool window_borderless; + uint32_t codec_profile; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -64,6 +65,7 @@ struct scrcpy_options { .render_expired_frames = false, \ .prefer_text = false, \ .window_borderless = false, \ + .codec_profile = 0, \ } bool diff --git a/app/src/server.c b/app/src/server.c index 4b2c1866cc..1d9efe8b5a 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -238,6 +238,7 @@ execute_server(struct server *server, const struct server_params *params) { sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); sprintf(max_fps_string, "%"PRIu16, params->max_fps); sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation); + sprintf(codec_profile_string, "%"PRIu32, params->codec_profile); const char *const cmd[] = { "shell", "CLASSPATH=" DEVICE_SERVER_PATH, @@ -254,6 +255,7 @@ execute_server(struct server *server, const struct server_params *params) { bit_rate_string, max_fps_string, lock_video_orientation_string, + codec_profile_string, server->tunnel_forward ? "true" : "false", params->crop ? params->crop : "-", "true", // always send frame meta (packet boundaries + timestamp) @@ -327,6 +329,7 @@ bool server_start(struct server *server, const char *serial, const struct server_params *params) { server->port_range = params->port_range; + server->codec_profile = params->codec_profile; if (serial) { server->serial = SDL_strdup(serial); diff --git a/app/src/server.h b/app/src/server.h index d84a5cc8c2..974cb4190a 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -19,6 +19,7 @@ struct server { uint16_t local_port; // selected from port_range bool tunnel_enabled; bool tunnel_forward; // use "adb forward" instead of "adb reverse" + uint32_t codec_profile; }; #define SERVER_INITIALIZER { \ @@ -34,6 +35,7 @@ struct server { .local_port = 0, \ .tunnel_enabled = false, \ .tunnel_forward = false, \ + .codec_profile = 0, } struct server_params { @@ -44,6 +46,7 @@ struct server_params { uint16_t max_fps; int8_t lock_video_orientation; bool control; + uint32_t codec_profile; }; // init default values diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index d9a29452b0..2b87b952bb 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 int codecProfile; public int getMaxSize() { return maxSize; @@ -75,4 +76,12 @@ public boolean getControl() { public void setControl(boolean control) { this.control = control; } + + public int getCodecProfile() { + return codecProfile; + } + + public void setCodecProfile(int codecProfile) { + this.codecProfile = codecProfile; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 3e9772ee05..b886affb9b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -30,19 +30,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; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation, int iFrameInterval) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation, int codecProfile, int iFrameInterval) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; this.lockedVideoOrientation = lockedVideoOrientation; + this.codecProfile = codecProfile; 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, int codecProfile) { + this(sendFrameMeta, bitRate, maxFps, lockedVideoOrientation, codecProfile, DEFAULT_I_FRAME_INTERVAL); } @Override @@ -64,6 +66,7 @@ public void streamScreen(Device device, FileDescriptor fd) throws IOException { try { do { MediaCodec codec = createCodec(); + setCodecProfile(codec, format); IBinder display = createDisplay(); ScreenInfo screenInfo = device.getScreenInfo(); Rect contentRect = screenInfo.getContentRect(); @@ -139,6 +142,22 @@ private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, 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) { + 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); + } + } + private static MediaCodec createCodec() throws IOException { return MediaCodec.createEncoderByType("video/avc"); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 2b0d32a239..71d02abf69 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -20,7 +20,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.getCodecProfile()); if (options.getControl()) { Controller controller = new Controller(device, connection); @@ -77,11 +77,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 +98,20 @@ private static Options createOptions(String... args) { int lockedVideoOrientation = Integer.parseInt(args[4]); options.setLockedVideoOrientation(lockedVideoOrientation); + int codecProfile = Integer.parseInt(args[5]); + options.setCodecProfile(codecProfile); + // 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;