Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encoding options #1296

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/scrcpy.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 16 additions & 7 deletions app/src/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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[]) {
Expand All @@ -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'},
Expand All @@ -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'},
Expand All @@ -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 },
};

Expand All @@ -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':
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions app/src/scrcpy.c
Original file line number Diff line number Diff line change
Expand Up @@ -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, &params)) {
return false;
Expand Down
2 changes: 2 additions & 0 deletions app/src/scrcpy.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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, \
Expand Down
1 change: 1 addition & 0 deletions app/src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions app/src/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ struct server_params {
uint16_t max_fps;
int8_t lock_video_orientation;
bool control;
char *codec_options;
};

// init default values
Expand Down
85 changes: 85 additions & 0 deletions server/src/main/java/com/genymobile/scrcpy/CodecOptions.java
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(so this class would not be needed at all)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think its a smart idea to let the user pass down any key=value pair he wants...
He can totally mess up and give random values and things that might even break the encoding and he won't be aware that he was giving something wrong.

I don't know about the other options but in case of profile and level it is suggested that if you provide a profile, you will also set a level, this is why when only profile (without level) is provided by the user, there is a calculation based on the screen size that should decide the level.

My goal was not to allow the user to pass down any key=value pair he wants, but to allow you or whoever wants to, to expand on the current CodecOptions and easily add new options that he can choose how they will be parsed and what will happen with them and where in the code.

Copy link
Collaborator

@rom1v rom1v Apr 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think its a smart idea to let the user pass down any key=value pair he wants...

It would allow to configure the codec generically, without having to build the app.

For advanced options, I would prefer a generic way.

He can totally mess up and give random values and things that might even break the encoding and he won't be aware that he was giving something wrong.

At least, we can warn if the provided key is not supported: containsKey(). (EDIT: in fact, it just tells whether we set a key for the format)

User can already pass random values (a bitrate of 1Mbps for instance), which is "ignored" by the codec without warnings.

I don't know about the other options but in case of profile and level it is suggested that if you provide a profile, you will also set a level, this is why when only profile (without level) is provided by the user, there is a calculation based on the screen size that should decide the level.

Isn't it done automatically if you don't provide the level? #1226 (comment)

Btw, contrary to what I said here, there is no need to specify the type in the string, it can be requested: getValueTypeForKey

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, it will be done your way :)
Thanks for the good lesson.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rom1v
Just a quick question, what if the key is supported but value is not supported by the device?
If we are assuming the codec ignores bad values shouldn't the user know about it somehow?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getValueTypeForKey is API29+ ...
What do you suggest?
We can go back to what you said, assuming int unless specified key:type=value

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rom1v Ping

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if the key is supported but value is not supported by the device?

The device will just ignore it I guess. Like if for now you pass --max-fps but the encoder does not support it.

getValueTypeForKey is API29+

:(

We can go back to what you said, assuming int unless specified key:type=value

Hmm, yes... I have no better solution then :(

Copy link
Author

@tzah4748 tzah4748 Apr 26, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so I've made the changes on our side, ill need to update my dev branch and make a new PR.
@rom1v
Just to make things clear before i make a new PR, the expected value types are
Integer/int - String/str - Float/float - Long/long
Yes?


public static final String PROFILE_OPTION = "profile";
public static final String LEVEL_OPTION = "level";

private static final LinkedHashMap<Integer, String> levelsTable = new LinkedHashMap<Integer, String>() {
{
// 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<String, String> options;

CodecOptions(HashMap<String, String> 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<Integer, String> 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;
}
}
}
16 changes: 16 additions & 0 deletions server/src/main/java/com/genymobile/scrcpy/NumberUtils.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
9 changes: 9 additions & 0 deletions server/src/main/java/com/genymobile/scrcpy/Options.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
53 changes: 48 additions & 5 deletions server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Loading