Skip to content

Commit

Permalink
Add option --max-fps
Browse files Browse the repository at this point in the history
Add an option to limit the capture frame rate. It only works for devices
with Android >= 10.

Fixes <#488>
  • Loading branch information
rom1v committed Nov 17, 2019
1 parent fb97681 commit 1d97d72
Show file tree
Hide file tree
Showing 10 changed files with 87 additions and 12 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ scrcpy --bit-rate 2M
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

Expand Down
4 changes: 4 additions & 0 deletions app/scrcpy.1
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ Force recording format (either mp4 or mkv).
.B \-h, \-\-help
Print this help.

.TP
.BI \-\-max\-fps " value
Limit the framerate of screen capture (only supported on devices with Android >= 10).

.TP
.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.
Expand Down
33 changes: 33 additions & 0 deletions app/src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ static void usage(const char *arg0) {
" -h, --help\n"
" Print this help.\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"
" Limit both the width and height of the video to value. The\n"
" other dimension is computed so that the device aspect-ratio\n"
Expand Down Expand Up @@ -269,6 +273,28 @@ parse_max_size(char *optarg, uint16_t *max_size) {
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
parse_window_position(char *optarg, int16_t *position) {
char *endptr;
Expand Down Expand Up @@ -374,6 +400,7 @@ guess_record_format(const char *filename) {
#define OPT_WINDOW_WIDTH 1009
#define OPT_WINDOW_HEIGHT 1010
#define OPT_WINDOW_BORDERLESS 1011
#define OPT_MAX_FPS 1012

static bool
parse_args(struct args *args, int argc, char *argv[]) {
Expand All @@ -383,6 +410,7 @@ parse_args(struct args *args, int argc, char *argv[]) {
{"crop", required_argument, NULL, OPT_CROP},
{"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'},
{"max-fps", required_argument, NULL, OPT_MAX_FPS},
{"max-size", required_argument, NULL, 'm'},
{"no-control", no_argument, NULL, 'n'},
{"no-display", no_argument, NULL, 'N'},
Expand Down Expand Up @@ -438,6 +466,11 @@ parse_args(struct args *args, int argc, char *argv[]) {
case 'h':
args->help = true;
break;
case OPT_MAX_FPS:
if (!parse_max_fps(optarg, &opts->max_fps)) {
return false;
}
break;
case 'm':
if (!parse_max_size(optarg, &opts->max_size)) {
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 @@ -280,6 +280,7 @@ scrcpy(const struct scrcpy_options *options) {
.local_port = options->port,
.max_size = options->max_size,
.bit_rate = options->bit_rate,
.max_fps = options->max_fps,
.control = options->control,
};
if (!server_start(&server, options->serial, &params)) {
Expand Down
2 changes: 2 additions & 0 deletions app/src/scrcpy.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct scrcpy_options {
uint16_t port;
uint16_t max_size;
uint32_t bit_rate;
uint16_t max_fps;
int16_t window_x;
int16_t window_y;
uint16_t window_width;
Expand All @@ -43,6 +44,7 @@ struct scrcpy_options {
.port = DEFAULT_LOCAL_PORT, \
.max_size = DEFAULT_LOCAL_PORT, \
.bit_rate = DEFAULT_BIT_RATE, \
.max_fps = 0, \
.window_x = -1, \
.window_y = -1, \
.window_width = 0, \
Expand Down
3 changes: 3 additions & 0 deletions app/src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,10 @@ static process_t
execute_server(struct server *server, const struct server_params *params) {
char max_size_string[6];
char bit_rate_string[11];
char max_fps_string[6];
sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
sprintf(max_fps_string, "%"PRIu16, params->max_fps);
const char *const cmd[] = {
"shell",
"CLASSPATH=/data/local/tmp/" SERVER_FILENAME,
Expand All @@ -134,6 +136,7 @@ execute_server(struct server *server, const struct server_params *params) {
SCRCPY_VERSION,
max_size_string,
bit_rate_string,
max_fps_string,
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 @@ -35,6 +35,7 @@ struct server_params {
uint16_t local_port;
uint16_t max_size;
uint32_t bit_rate;
uint16_t max_fps;
bool control;
};

Expand Down
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 @@ -5,6 +5,7 @@
public class Options {
private int maxSize;
private int bitRate;
private int maxFps;
private boolean tunnelForward;
private Rect crop;
private boolean sendFrameMeta; // send PTS so that the client may record properly
Expand All @@ -26,6 +27,14 @@ public void setBitRate(int bitRate) {
this.bitRate = bitRate;
}

public int getMaxFps() {
return maxFps;
}

public void setMaxFps(int maxFps) {
this.maxFps = maxFps;
}

public boolean isTunnelForward() {
return tunnelForward;
}
Expand Down
21 changes: 16 additions & 5 deletions server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Build;
import android.os.IBinder;
import android.os.Looper;
import android.view.Surface;
Expand All @@ -26,18 +27,20 @@ public class ScreenEncoder implements Device.RotationListener {
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);

private int bitRate;
private int maxFps;
private int iFrameInterval;
private boolean sendFrameMeta;
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.bitRate = bitRate;
this.maxFps = maxFps;
this.iFrameInterval = iFrameInterval;
}

public ScreenEncoder(boolean sendFrameMeta, int bitRate) {
this(sendFrameMeta, bitRate, DEFAULT_I_FRAME_INTERVAL);
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) {
this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL);
}

@Override
Expand All @@ -60,7 +63,7 @@ public void streamScreen(Device device, FileDescriptor fd) throws IOException {
// <https://github.com/Genymobile/scrcpy/issues/921>
Looper.prepareMainLooper();

MediaFormat format = createFormat(bitRate, iFrameInterval);
MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval);
device.setRotationListener(this);
boolean alive;
try {
Expand Down Expand Up @@ -143,7 +146,8 @@ private static MediaCodec createCodec() throws IOException {
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();
format.setString(MediaFormat.KEY_MIME, "video/avc");
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
Expand All @@ -153,6 +157,13 @@ private static MediaFormat createFormat(int bitRate, int iFrameInterval) throws
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
// 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
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;
}

Expand Down
17 changes: 10 additions & 7 deletions server/src/main/java/com/genymobile/scrcpy/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ private static void scrcpy(Options options) throws IOException {
final Device device = new Device(options);
boolean tunnelForward = options.isTunnelForward();
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()) {
Controller controller = new Controller(device, connection);
Expand Down Expand Up @@ -77,8 +77,8 @@ private static Options createOptions(String... args) {
+ "(" + BuildConfig.VERSION_NAME + ")");
}

if (args.length != 7) {
throw new IllegalArgumentException("Expecting 7 parameters");
if (args.length != 8) {
throw new IllegalArgumentException("Expecting 8 parameters");
}

Options options = new Options();
Expand All @@ -89,17 +89,20 @@ private static Options createOptions(String... args) {
int bitRate = Integer.parseInt(args[2]);
options.setBitRate(bitRate);

int maxFps = Integer.parseInt(args[3]);
options.setMaxFps(maxFps);

// 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);

Rect crop = parseCrop(args[4]);
Rect crop = parseCrop(args[5]);
options.setCrop(crop);

boolean sendFrameMeta = Boolean.parseBoolean(args[5]);
boolean sendFrameMeta = Boolean.parseBoolean(args[6]);
options.setSendFrameMeta(sendFrameMeta);

boolean control = Boolean.parseBoolean(args[6]);
boolean control = Boolean.parseBoolean(args[7]);
options.setControl(control);

return options;
Expand Down

0 comments on commit 1d97d72

Please sign in to comment.