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:
parent
fb976816f9
commit
1d97d7213d
10 changed files with 87 additions and 12 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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, ¶ms)) {
|
if (!server_start(&server, options->serial, ¶ms)) {
|
||||||
|
|
|
@ -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, \
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue