diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index c8b6609e..9743e44a 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -13,6 +13,7 @@ _scrcpy() { --camera-ar= --camera-id= --camera-facing= + --camera-fps= --camera-size= --crop= -d --select-usb @@ -156,6 +157,7 @@ _scrcpy() { |--audio-output-buffer \ |--camera-ar \ |--camera-id \ + |--camera-fps \ |--camera-size \ |--crop \ |--display-id \ diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 823e6b9e..1ad96ad5 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -20,6 +20,7 @@ arguments=( '--camera-ar=[Select the camera size by its aspect ratio]' '--camera-id=[Specify the camera id to mirror]' '--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)' + '--camera-fps=[Specify the camera capture frame rate]' '--camera-size=[Specify an explicit camera capture size]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index c473adb5..e3b1b6f0 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -93,6 +93,12 @@ Select the device camera by its facing direction. Possible values are "front", "back" and "external". +.TP +.BI "\-\-camera\-fps " fps +Specify the camera capture frame rate. + +If not specified, Android's default frame rate (30 fps) is used. + .TP .BI "\-\-camera\-size " width\fRx\fIheight Specify an explicit camera capture size. diff --git a/app/src/cli.c b/app/src/cli.c index df73edac..b82d332d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -88,6 +88,7 @@ enum { OPT_CAMERA_SIZE, OPT_CAMERA_FACING, OPT_CAMERA_AR, + OPT_CAMERA_FPS, }; struct sc_option { @@ -234,6 +235,14 @@ static const struct sc_option options[] = { .argdesc = "x", .text = "Specify an explicit camera capture size.", }, + { + .longopt_id = OPT_CAMERA_FPS, + .longopt = "camera-fps", + .argdesc = "value", + .text = "Specify the camera capture frame rate.\n" + "If not specified, Android's default frame rate (30 fps) is " + "used.", + }, { // Not really deprecated (--codec has never been released), but without // declaring an explicit --codec option, getopt_long() partial matching @@ -1746,6 +1755,18 @@ parse_camera_facing(const char *optarg, enum sc_camera_facing *facing) { return false; } +static bool +parse_camera_fps(const char *s, uint16_t *camera_fps) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "camera fps"); + if (!ok) { + return false; + } + + *camera_fps = (uint16_t) value; + return true; +} + static bool parse_time_limit(const char *s, sc_tick *tick) { long value; @@ -2154,6 +2175,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_CAMERA_FPS: + if (!parse_camera_fps(optarg, &opts->camera_fps)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; @@ -2277,6 +2303,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } else if (opts->camera_id || opts->camera_ar || opts->camera_facing != SC_CAMERA_FACING_ANY + || opts->camera_fps || opts->camera_size) { LOGE("Camera options are only available with --video-source=camera"); return false; diff --git a/app/src/options.c b/app/src/options.c index 589a5a22..8601678b 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -14,6 +14,7 @@ const struct scrcpy_options scrcpy_options_default = { .camera_id = NULL, .camera_size = NULL, .camera_ar = NULL, + .camera_fps = 0, .log_level = SC_LOG_LEVEL_INFO, .video_codec = SC_CODEC_H264, .audio_codec = SC_CODEC_OPUS, diff --git a/app/src/options.h b/app/src/options.h index 40f04670..a712f443 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -133,6 +133,7 @@ struct scrcpy_options { const char *camera_id; const char *camera_size; const char *camera_ar; + uint16_t camera_fps; enum sc_log_level log_level; enum sc_codec video_codec; enum sc_codec audio_codec; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 1f4c2a7c..64067cf6 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -376,6 +376,7 @@ scrcpy(struct scrcpy_options *options) { .camera_id = options->camera_id, .camera_size = options->camera_size, .camera_ar = options->camera_ar, + .camera_fps = options->camera_fps, .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, .clipboard_autosync = options->clipboard_autosync, diff --git a/app/src/server.c b/app/src/server.c index 0c40bccb..8a91952a 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -308,6 +308,9 @@ execute_server(struct sc_server *server, if (params->camera_ar) { ADD_PARAM("camera_ar=%s", params->camera_ar); } + if (params->camera_fps) { + ADD_PARAM("camera_fps=%" PRIu16, params->camera_fps); + } if (params->show_touches) { ADD_PARAM("show_touches=true"); } diff --git a/app/src/server.h b/app/src/server.h index 71d22fe8..ed1f307e 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -37,6 +37,7 @@ struct sc_server_params { const char *camera_id; const char *camera_size; const char *camera_ar; + uint16_t camera_fps; struct sc_port_range port_range; uint32_t tunnel_host; uint16_t tunnel_port; diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java index e4aba872..9edc600c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java @@ -19,6 +19,7 @@ import android.media.MediaCodec; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; +import android.util.Range; import android.view.Surface; import java.io.IOException; @@ -38,6 +39,7 @@ public class CameraCapture extends SurfaceCapture { private final Size explicitSize; private int maxSize; private final CameraAspectRatio aspectRatio; + private final int fps; private String cameraId; private Size size; @@ -49,12 +51,13 @@ public class CameraCapture extends SurfaceCapture { private final AtomicBoolean disconnected = new AtomicBoolean(); - public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio) { + public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, int fps) { this.explicitCameraId = explicitCameraId; this.cameraFacing = cameraFacing; this.explicitSize = explicitSize; this.maxSize = maxSize; this.aspectRatio = aspectRatio; + this.fps = fps; } @Override @@ -304,6 +307,11 @@ public class CameraCapture extends SurfaceCapture { private CaptureRequest createCaptureRequest(Surface surface) throws CameraAccessException { CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); requestBuilder.addTarget(surface); + + if (fps > 0) { + requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<>(fps, fps)); + } + return requestBuilder.build(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java index 7806cf51..329f2570 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -9,8 +9,10 @@ import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; import android.hardware.camera2.params.StreamConfigurationMap; import android.media.MediaCodec; +import android.util.Range; import java.util.List; +import java.util.TreeSet; public final class LogUtils { @@ -97,7 +99,15 @@ public final class LogUtils { builder.append(" (").append(getCameraFacingName(facing)).append(", "); Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); - builder.append(activeSize.width()).append("x").append(activeSize.height()).append(')'); + builder.append(activeSize.width()).append("x").append(activeSize.height()).append(", "); + + // Capture frame rates for low-FPS mode are the same for every resolution + Range[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + TreeSet uniqueLowFps = new TreeSet<>(); + for (Range range : lowFpsRanges) { + uniqueLowFps.add(range.getUpper()); + } + builder.append("fps=").append(uniqueLowFps).append(')'); if (includeSizes) { StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index eec19e52..843fe9f1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -28,6 +28,7 @@ public class Options { private Size cameraSize; private CameraFacing cameraFacing; private CameraAspectRatio cameraAspectRatio; + private int cameraFps; private boolean showTouches; private boolean stayAwake; private List videoCodecOptions; @@ -136,6 +137,10 @@ public class Options { return cameraAspectRatio; } + public int getCameraFps() { + return cameraFps; + } + public boolean getShowTouches() { return showTouches; } @@ -384,6 +389,9 @@ public class Options { options.cameraAspectRatio = parseCameraAspectRatio(value); } break; + case "camera_fps": + options.cameraFps = Integer.parseInt(value); + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 9789f7f2..ca72d584 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -144,7 +144,7 @@ public final class Server { surfaceCapture = new ScreenCapture(device); } else { surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(), - options.getMaxSize(), options.getCameraAspectRatio()); + options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps()); } SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());