Accept float values for --max-fps

Android accepts a float value, there is no reason to limit the option
to be an integer.

In particular, it allows to capture at a rate lower than 1 fps. For
example, to capture 1 frame every 5 seconds:

    scrcpy --video-source=camera --max-fps=0.2

It was already possible to pass a float manually:

    scrcpy --video-source=camera \
        --video-codec-options=max-fps-to-encoder:float=0.2

But accepting a float directly for --max-fps is more convenient.

Refs <https://developer.android.com/reference/android/media/MediaFormat#KEY_MAX_FPS_TO_ENCODER>
This commit is contained in:
Romain Vimont 2024-09-13 20:03:50 +02:00
parent 6451ad271a
commit 265a15e0b1
8 changed files with 69 additions and 14 deletions

View File

@ -1447,6 +1447,26 @@ parse_integers_arg(const char *s, const char sep, size_t max_items, long *out,
return count; return count;
} }
static bool
parse_float_arg(const char *s, float *out, float min, float max,
const char *name) {
float value;
bool ok = sc_str_parse_float(s, &value);
if (!ok) {
LOGE("Could not parse %s: %s", name, s);
return false;
}
if (value < min || value > max) {
LOGE("Could not parse %s: value (%f) out-of-range (%f; %f)",
name, value, min, max);
return false;
}
*out = value;
return true;
}
static bool static bool
parse_bit_rate(const char *s, uint32_t *bit_rate) { parse_bit_rate(const char *s, uint32_t *bit_rate) {
long value; long value;
@ -1474,14 +1494,14 @@ parse_max_size(const char *s, uint16_t *max_size) {
} }
static bool static bool
parse_max_fps(const char *s, uint16_t *max_fps) { parse_max_fps(const char *s, float *max_fps) {
long value; float value;
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max fps"); bool ok = parse_float_arg(s, &value, 0, (float) (1 << 16), "max fps");
if (!ok) { if (!ok) {
return false; return false;
} }
*max_fps = (uint16_t) value; *max_fps = value;
return true; return true;
} }

View File

@ -240,7 +240,7 @@ struct scrcpy_options {
uint16_t max_size; uint16_t max_size;
uint32_t video_bit_rate; uint32_t video_bit_rate;
uint32_t audio_bit_rate; uint32_t audio_bit_rate;
uint16_t max_fps; float max_fps;
enum sc_lock_video_orientation lock_video_orientation; enum sc_lock_video_orientation lock_video_orientation;
enum sc_orientation display_orientation; enum sc_orientation display_orientation;
enum sc_orientation record_orientation; enum sc_orientation record_orientation;

View File

@ -321,7 +321,7 @@ execute_server(struct sc_server *server,
ADD_PARAM("max_size=%" PRIu16, params->max_size); ADD_PARAM("max_size=%" PRIu16, params->max_size);
} }
if (params->max_fps) { if (params->max_fps) {
ADD_PARAM("max_fps=%" PRIu16, params->max_fps); ADD_PARAM("max_fps=%f" , params->max_fps);
} }
if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
ADD_PARAM("lock_video_orientation=%" PRIi8, ADD_PARAM("lock_video_orientation=%" PRIi8,

View File

@ -44,7 +44,7 @@ struct sc_server_params {
uint16_t max_size; uint16_t max_size;
uint32_t video_bit_rate; uint32_t video_bit_rate;
uint32_t audio_bit_rate; uint32_t audio_bit_rate;
uint16_t max_fps; float max_fps;
int8_t lock_video_orientation; int8_t lock_video_orientation;
bool control; bool control;
uint32_t display_id; uint32_t display_id;

View File

@ -147,6 +147,25 @@ sc_str_parse_integer_with_suffix(const char *s, long *out) {
return true; return true;
} }
bool
sc_str_parse_float(const char *s, float *out) {
char *endptr;
if (*s == '\0') {
return false;
}
errno = 0;
float value = strtof(s, &endptr);
if (errno == ERANGE) {
return false;
}
if (*endptr != '\0') {
return false;
}
*out = value;
return true;
}
bool bool
sc_str_list_contains(const char *list, char sep, const char *s) { sc_str_list_contains(const char *list, char sep, const char *s) {
char *p; char *p;

View File

@ -66,6 +66,14 @@ sc_str_parse_integers(const char *s, const char sep, size_t max_items,
bool bool
sc_str_parse_integer_with_suffix(const char *s, long *out); sc_str_parse_integer_with_suffix(const char *s, long *out);
/**
* `Parse `s` as a float into `out`
*
* Return true if the conversion succeeded, false otherwise.
*/
bool
sc_str_parse_float(const char *s, float *out);
/** /**
* Search `s` in the list separated by `sep` * Search `s` in the list separated by `sep`
* *

View File

@ -29,7 +29,7 @@ public class Options {
private boolean audioDup; private boolean audioDup;
private int videoBitRate = 8000000; private int videoBitRate = 8000000;
private int audioBitRate = 128000; private int audioBitRate = 128000;
private int maxFps; private float maxFps;
private int lockVideoOrientation = -1; private int lockVideoOrientation = -1;
private boolean tunnelForward; private boolean tunnelForward;
private Rect crop; private Rect crop;
@ -113,7 +113,7 @@ public class Options {
return audioBitRate; return audioBitRate;
} }
public int getMaxFps() { public float getMaxFps() {
return maxFps; return maxFps;
} }
@ -321,7 +321,7 @@ public class Options {
options.audioBitRate = Integer.parseInt(value); options.audioBitRate = Integer.parseInt(value);
break; break;
case "max_fps": case "max_fps":
options.maxFps = Integer.parseInt(value); options.maxFps = parseFloat("max_fps", value);
break; break;
case "lock_video_orientation": case "lock_video_orientation":
options.lockVideoOrientation = Integer.parseInt(value); options.lockVideoOrientation = Integer.parseInt(value);
@ -493,4 +493,12 @@ public class Options {
float floatAr = Float.parseFloat(tokens[0]); float floatAr = Float.parseFloat(tokens[0]);
return CameraAspectRatio.fromFloat(floatAr); return CameraAspectRatio.fromFloat(floatAr);
} }
private static float parseFloat(String key, String value) {
try {
return Float.parseFloat(value);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid float value for " + key + ": \"" + value + "\"");
}
}
} }

View File

@ -39,7 +39,7 @@ public class SurfaceEncoder implements AsyncProcessor {
private final String encoderName; private final String encoderName;
private final List<CodecOption> codecOptions; private final List<CodecOption> codecOptions;
private final int videoBitRate; private final int videoBitRate;
private final int maxFps; private final float maxFps;
private final boolean downsizeOnError; private final boolean downsizeOnError;
private boolean firstFrameSent; private boolean firstFrameSent;
@ -48,8 +48,8 @@ public class SurfaceEncoder implements AsyncProcessor {
private Thread thread; private Thread thread;
private final AtomicBoolean stopped = new AtomicBoolean(); private final AtomicBoolean stopped = new AtomicBoolean();
public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, int maxFps, List<CodecOption> codecOptions, String encoderName, public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, float maxFps, List<CodecOption> codecOptions,
boolean downsizeOnError) { String encoderName, boolean downsizeOnError) {
this.capture = capture; this.capture = capture;
this.streamer = streamer; this.streamer = streamer;
this.videoBitRate = videoBitRate; this.videoBitRate = videoBitRate;
@ -225,7 +225,7 @@ public class SurfaceEncoder implements AsyncProcessor {
} }
} }
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) { private static MediaFormat createFormat(String videoMimeType, int bitRate, float maxFps, List<CodecOption> codecOptions) {
MediaFormat format = new MediaFormat(); MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, videoMimeType); format.setString(MediaFormat.KEY_MIME, videoMimeType);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);