Add option --max-fps

Add an option to limit the capture frame rate. It only works for devices
with Android >= 10.

Fixes <https://github.com/Genymobile/scrcpy/issues/488>
This commit is contained in:
Romain Vimont 2019-11-17 22:07:19 +01:00
parent fb976816f9
commit 1d97d7213d
10 changed files with 87 additions and 12 deletions

View file

@ -134,6 +134,14 @@ scrcpy --bit-rate 2M
scrcpy -b 2M # short version scrcpy -b 2M # short version
``` ```
### Limit capture frame rate
On device with Android >= 10, the capture frame rate can be limited:
```bash
scrcpy --max-fps 15
```
### Crop ### Crop

View file

@ -41,6 +41,10 @@ Force recording format (either mp4 or mkv).
.B \-h, \-\-help .B \-h, \-\-help
Print this help. Print this help.
.TP
.BI \-\-max\-fps " value
Limit the framerate of screen capture (only supported on devices with Android >= 10).
.TP .TP
.BI "\-m, \-\-max\-size " value .BI "\-m, \-\-max\-size " value
Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved.

View file

@ -50,6 +50,10 @@ static void usage(const char *arg0) {
" -h, --help\n" " -h, --help\n"
" Print this help.\n" " Print this help.\n"
"\n" "\n"
" --max-fps value\n"
" Limit the frame rate of screen capture (only supported on\n"
" devices with Android >= 10).\n"
"\n"
" -m, --max-size value\n" " -m, --max-size value\n"
" Limit both the width and height of the video to value. The\n" " Limit both the width and height of the video to value. The\n"
" other dimension is computed so that the device aspect-ratio\n" " other dimension is computed so that the device aspect-ratio\n"
@ -269,6 +273,28 @@ parse_max_size(char *optarg, uint16_t *max_size) {
return true; return true;
} }
static bool
parse_max_fps(const char *optarg, uint16_t *max_fps) {
char *endptr;
if (*optarg == '\0') {
LOGE("Max FPS parameter is empty");
return false;
}
long value = strtol(optarg, &endptr, 0);
if (*endptr != '\0') {
LOGE("Invalid max FPS: %s", optarg);
return false;
}
if (value & ~0xffff) {
// in practice, it should not be higher than 60
LOGE("Max FPS value is invalid: %ld", value);
return false;
}
*max_fps = (uint16_t) value;
return true;
}
static bool static bool
parse_window_position(char *optarg, int16_t *position) { parse_window_position(char *optarg, int16_t *position) {
char *endptr; char *endptr;
@ -374,6 +400,7 @@ guess_record_format(const char *filename) {
#define OPT_WINDOW_WIDTH 1009 #define OPT_WINDOW_WIDTH 1009
#define OPT_WINDOW_HEIGHT 1010 #define OPT_WINDOW_HEIGHT 1010
#define OPT_WINDOW_BORDERLESS 1011 #define OPT_WINDOW_BORDERLESS 1011
#define OPT_MAX_FPS 1012
static bool static bool
parse_args(struct args *args, int argc, char *argv[]) { parse_args(struct args *args, int argc, char *argv[]) {
@ -383,6 +410,7 @@ parse_args(struct args *args, int argc, char *argv[]) {
{"crop", required_argument, NULL, OPT_CROP}, {"crop", required_argument, NULL, OPT_CROP},
{"fullscreen", no_argument, NULL, 'f'}, {"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{"max-fps", required_argument, NULL, OPT_MAX_FPS},
{"max-size", required_argument, NULL, 'm'}, {"max-size", required_argument, NULL, 'm'},
{"no-control", no_argument, NULL, 'n'}, {"no-control", no_argument, NULL, 'n'},
{"no-display", no_argument, NULL, 'N'}, {"no-display", no_argument, NULL, 'N'},
@ -438,6 +466,11 @@ parse_args(struct args *args, int argc, char *argv[]) {
case 'h': case 'h':
args->help = true; args->help = true;
break; break;
case OPT_MAX_FPS:
if (!parse_max_fps(optarg, &opts->max_fps)) {
return false;
}
break;
case 'm': case 'm':
if (!parse_max_size(optarg, &opts->max_size)) { if (!parse_max_size(optarg, &opts->max_size)) {
return false; return false;

View file

@ -280,6 +280,7 @@ scrcpy(const struct scrcpy_options *options) {
.local_port = options->port, .local_port = options->port,
.max_size = options->max_size, .max_size = options->max_size,
.bit_rate = options->bit_rate, .bit_rate = options->bit_rate,
.max_fps = options->max_fps,
.control = options->control, .control = options->control,
}; };
if (!server_start(&server, options->serial, &params)) { if (!server_start(&server, options->serial, &params)) {

View file

@ -18,6 +18,7 @@ struct scrcpy_options {
uint16_t port; uint16_t port;
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps;
int16_t window_x; int16_t window_x;
int16_t window_y; int16_t window_y;
uint16_t window_width; uint16_t window_width;
@ -43,6 +44,7 @@ struct scrcpy_options {
.port = DEFAULT_LOCAL_PORT, \ .port = DEFAULT_LOCAL_PORT, \
.max_size = DEFAULT_LOCAL_PORT, \ .max_size = DEFAULT_LOCAL_PORT, \
.bit_rate = DEFAULT_BIT_RATE, \ .bit_rate = DEFAULT_BIT_RATE, \
.max_fps = 0, \
.window_x = -1, \ .window_x = -1, \
.window_y = -1, \ .window_y = -1, \
.window_width = 0, \ .window_width = 0, \

View file

@ -118,8 +118,10 @@ static process_t
execute_server(struct server *server, const struct server_params *params) { execute_server(struct server *server, const struct server_params *params) {
char max_size_string[6]; char max_size_string[6];
char bit_rate_string[11]; char bit_rate_string[11];
char max_fps_string[6];
sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
sprintf(max_fps_string, "%"PRIu16, params->max_fps);
const char *const cmd[] = { const char *const cmd[] = {
"shell", "shell",
"CLASSPATH=/data/local/tmp/" SERVER_FILENAME, "CLASSPATH=/data/local/tmp/" SERVER_FILENAME,
@ -134,6 +136,7 @@ execute_server(struct server *server, const struct server_params *params) {
SCRCPY_VERSION, SCRCPY_VERSION,
max_size_string, max_size_string,
bit_rate_string, bit_rate_string,
max_fps_string,
server->tunnel_forward ? "true" : "false", server->tunnel_forward ? "true" : "false",
params->crop ? params->crop : "-", params->crop ? params->crop : "-",
"true", // always send frame meta (packet boundaries + timestamp) "true", // always send frame meta (packet boundaries + timestamp)

View file

@ -35,6 +35,7 @@ struct server_params {
uint16_t local_port; uint16_t local_port;
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps;
bool control; bool control;
}; };

View file

@ -5,6 +5,7 @@ import android.graphics.Rect;
public class Options { public class Options {
private int maxSize; private int maxSize;
private int bitRate; private int bitRate;
private int maxFps;
private boolean tunnelForward; private boolean tunnelForward;
private Rect crop; private Rect crop;
private boolean sendFrameMeta; // send PTS so that the client may record properly private boolean sendFrameMeta; // send PTS so that the client may record properly
@ -26,6 +27,14 @@ public class Options {
this.bitRate = bitRate; this.bitRate = bitRate;
} }
public int getMaxFps() {
return maxFps;
}
public void setMaxFps(int maxFps) {
this.maxFps = maxFps;
}
public boolean isTunnelForward() { public boolean isTunnelForward() {
return tunnelForward; return tunnelForward;
} }

View file

@ -6,6 +6,7 @@ import android.graphics.Rect;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper; import android.os.Looper;
import android.view.Surface; import android.view.Surface;
@ -26,18 +27,20 @@ public class ScreenEncoder implements Device.RotationListener {
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
private int bitRate; private int bitRate;
private int maxFps;
private int iFrameInterval; private int iFrameInterval;
private boolean sendFrameMeta; private boolean sendFrameMeta;
private long ptsOrigin; private long ptsOrigin;
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int iFrameInterval) { public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) {
this.sendFrameMeta = sendFrameMeta; this.sendFrameMeta = sendFrameMeta;
this.bitRate = bitRate; this.bitRate = bitRate;
this.maxFps = maxFps;
this.iFrameInterval = iFrameInterval; this.iFrameInterval = iFrameInterval;
} }
public ScreenEncoder(boolean sendFrameMeta, int bitRate) { public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) {
this(sendFrameMeta, bitRate, DEFAULT_I_FRAME_INTERVAL); this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL);
} }
@Override @Override
@ -60,7 +63,7 @@ public class ScreenEncoder implements Device.RotationListener {
// <https://github.com/Genymobile/scrcpy/issues/921> // <https://github.com/Genymobile/scrcpy/issues/921>
Looper.prepareMainLooper(); Looper.prepareMainLooper();
MediaFormat format = createFormat(bitRate, iFrameInterval); MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval);
device.setRotationListener(this); device.setRotationListener(this);
boolean alive; boolean alive;
try { try {
@ -143,7 +146,8 @@ public class ScreenEncoder implements Device.RotationListener {
return MediaCodec.createEncoderByType("video/avc"); return MediaCodec.createEncoderByType("video/avc");
} }
private static MediaFormat createFormat(int bitRate, int iFrameInterval) throws IOException { @SuppressWarnings("checkstyle:MagicNumber")
private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) {
MediaFormat format = new MediaFormat(); MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, "video/avc"); format.setString(MediaFormat.KEY_MIME, "video/avc");
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
@ -153,6 +157,13 @@ public class ScreenEncoder implements Device.RotationListener {
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
// display the very first frame, and recover from bad quality when no new frames // display the very first frame, and recover from bad quality when no new frames
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs
if (maxFps > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
format.setFloat(MediaFormat.KEY_MAX_FPS_TO_ENCODER, maxFps);
} else {
Ln.w("Max FPS is only supported since Android 10, the option has been ignored");
}
}
return format; return format;
} }

View file

@ -17,7 +17,7 @@ public final class Server {
final Device device = new Device(options); final Device device = new Device(options);
boolean tunnelForward = options.isTunnelForward(); boolean tunnelForward = options.isTunnelForward();
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate()); ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps());
if (options.getControl()) { if (options.getControl()) {
Controller controller = new Controller(device, connection); Controller controller = new Controller(device, connection);
@ -77,8 +77,8 @@ public final class Server {
+ "(" + BuildConfig.VERSION_NAME + ")"); + "(" + BuildConfig.VERSION_NAME + ")");
} }
if (args.length != 7) { if (args.length != 8) {
throw new IllegalArgumentException("Expecting 7 parameters"); throw new IllegalArgumentException("Expecting 8 parameters");
} }
Options options = new Options(); Options options = new Options();
@ -89,17 +89,20 @@ public final class Server {
int bitRate = Integer.parseInt(args[2]); int bitRate = Integer.parseInt(args[2]);
options.setBitRate(bitRate); options.setBitRate(bitRate);
int maxFps = Integer.parseInt(args[3]);
options.setMaxFps(maxFps);
// use "adb forward" instead of "adb tunnel"? (so the server must listen) // use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(args[3]); boolean tunnelForward = Boolean.parseBoolean(args[4]);
options.setTunnelForward(tunnelForward); options.setTunnelForward(tunnelForward);
Rect crop = parseCrop(args[4]); Rect crop = parseCrop(args[5]);
options.setCrop(crop); options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[5]); boolean sendFrameMeta = Boolean.parseBoolean(args[6]);
options.setSendFrameMeta(sendFrameMeta); options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[6]); boolean control = Boolean.parseBoolean(args[7]);
options.setControl(control); options.setControl(control);
return options; return options;