From edb8f5b0f81e6b0d93c67378ade07e23798fc419 Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Sun, 3 Sep 2023 18:00:42 -0400 Subject: [PATCH] Add support for specifying camera resolution and frame rate This commit add three new CLI options for specifying the camera resolution and frame rate. By default, the camera's native resolution and Android's default frame rate (30 fps) are used. If the user selects a high frame rate (> 60 fps), the capture will automatically switch to the constrained high speed capture mode. Signed-off-by: Andrew Gunnerson --- app/src/cli.c | 62 +++++++ app/src/options.c | 3 + app/src/options.h | 3 + app/src/scrcpy.c | 3 + app/src/server.c | 9 + app/src/server.h | 3 + .../com/genymobile/scrcpy/CameraEncoder.java | 171 ++++++++++++++---- .../scrcpy/ConfigurationException.java | 4 + .../java/com/genymobile/scrcpy/Options.java | 24 +++ .../java/com/genymobile/scrcpy/Server.java | 3 +- 10 files changed, 252 insertions(+), 33 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index d0c5b0c197..c6dd317d45 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -35,6 +35,9 @@ enum { OPT_DISPLAY_ID, OPT_CAMERA_ID, OPT_CAMERA_POSITION, + OPT_CAMERA_WIDTH, + OPT_CAMERA_HEIGHT, + OPT_CAMERA_FPS, OPT_ROTATION, OPT_RENDER_DRIVER, OPT_NO_MIPMAPS, @@ -268,6 +271,24 @@ static const struct sc_option options[] = { "Valid values are: all, front, back, external\n" "Default is all.", }, + { + .longopt_id = OPT_CAMERA_WIDTH, + .longopt = "camera-width", + .argdesc = "value", + .text = "Specify the camera capture resolution's width when using --video-source=camera.", + }, + { + .longopt_id = OPT_CAMERA_HEIGHT, + .longopt = "camera-height", + .argdesc = "value", + .text = "Specify the camera capture resolution's width when using --video-source=camera.", + }, + { + .longopt_id = OPT_CAMERA_FPS, + .longopt = "camera-fps", + .argdesc = "value", + .text = "Specify the camera capture frame rate when using --video-source=camera.", + }, { .longopt_id = OPT_DISPLAY_BUFFER, .longopt = "display-buffer", @@ -1701,6 +1722,32 @@ parse_camera_position(const char *optarg, enum sc_camera_position *position) { return false; } +static bool +parse_camera_dimension(const char *s, uint16_t *dimension) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, + "camera dimension"); + if (!ok) { + return false; + } + + *dimension = (uint16_t) value; + return true; +} + +static bool +parse_camera_fps(const char *s, uint16_t *fps) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, + "camera fps"); + if (!ok) { + return false; + } + + *fps = (uint16_t) value; + return true; +} + static bool parse_time_limit(const char *s, sc_tick *tick) { long value; @@ -1748,6 +1795,21 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_CAMERA_ID: opts->camera_id = optarg; break; + case OPT_CAMERA_WIDTH: + if (!parse_camera_dimension(optarg, &opts->camera_width)) { + return false; + } + break; + case OPT_CAMERA_HEIGHT: + if (!parse_camera_dimension(optarg, &opts->camera_height)) { + return false; + } + break; + case OPT_CAMERA_FPS: + if (!parse_camera_fps(optarg, &opts->camera_fps)) { + return false; + } + break; case OPT_CAMERA_POSITION: if (!parse_camera_position(optarg, &opts->camera_position)) { return false; diff --git a/app/src/options.c b/app/src/options.c index 6345411b95..ec75b22410 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -42,6 +42,9 @@ const struct scrcpy_options scrcpy_options_default = { .display_id = 0, .camera_id = NULL, .camera_position = SC_CAMERA_POSITION_ALL, + .camera_width = 0, + .camera_height = 0, + .camera_fps = 0, .display_buffer = 0, .audio_buffer = SC_TICK_FROM_MS(50), .audio_output_buffer = SC_TICK_FROM_MS(5), diff --git a/app/src/options.h b/app/src/options.h index cadfb669e2..a371410318 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -154,6 +154,9 @@ struct scrcpy_options { uint32_t display_id; const char* camera_id; enum sc_camera_position camera_position; + uint16_t camera_width; + uint16_t camera_height; + uint16_t camera_fps; sc_tick display_buffer; sc_tick audio_buffer; sc_tick audio_output_buffer; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index ef5eb6aafd..0bae9a0e63 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -366,6 +366,9 @@ scrcpy(struct scrcpy_options *options) { .display_id = options->display_id, .camera_id = options->camera_id, .camera_position = options->camera_position, + .camera_width = options->camera_width, + .camera_height = options->camera_height, + .camera_fps = options->camera_fps, .video = options->video, .audio = options->audio, .show_touches = options->show_touches, diff --git a/app/src/server.c b/app/src/server.c index bb78850f08..a68175eded 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -293,6 +293,15 @@ execute_server(struct sc_server *server, break; } } + if (params->camera_width) { + ADD_PARAM("camera_width=%" PRIu16, params->camera_width); + } + if (params->camera_height) { + ADD_PARAM("camera_height=%" PRIu16, params->camera_height); + } + 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 019bf7e0ea..df628753ba 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -45,6 +45,9 @@ struct sc_server_params { uint32_t display_id; const char *camera_id; enum sc_camera_position camera_position; + uint16_t camera_width; + uint16_t camera_height; + uint16_t camera_fps; bool video; bool audio; bool show_touches; diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraEncoder.java b/server/src/main/java/com/genymobile/scrcpy/CameraEncoder.java index a517aac521..3113537c38 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/CameraEncoder.java @@ -6,14 +6,19 @@ import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession; import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureFailure; import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.OutputConfiguration; +import android.hardware.camera2.params.SessionConfiguration; import android.hardware.camera2.params.StreamConfigurationMap; 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.util.Arrays; @@ -22,29 +27,103 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; public class CameraEncoder extends SurfaceEncoder { private final String cameraId; - private final CameraPosition cameraPosition; + private final int cameraWidth; + private final int cameraHeight; + private final int cameraFps; + private final boolean isHighSpeed; private int maxSize; - private String actualCameraId; private CameraDevice cameraDevice; private final Handler cameraHandler; + private final Executor cameraExecutor; - public CameraEncoder(int maxSize, String cameraId, CameraPosition cameraPosition, Streamer streamer, - int videoBitRate, int maxFps, List codecOptions, String encoderName, boolean downsizeOnError) { + public CameraEncoder(int maxSize, String cameraId, CameraPosition cameraPosition, + int cameraWidth, int cameraHeight, int cameraFps, Streamer streamer, + int videoBitRate, int maxFps, List codecOptions, String encoderName, boolean downsizeOnError) + throws ConfigurationException { super(streamer, videoBitRate, maxFps, codecOptions, encoderName, downsizeOnError); this.maxSize = maxSize; - this.cameraId = cameraId; - this.cameraPosition = cameraPosition; + this.cameraId = findCameraId(cameraId, cameraPosition); + this.cameraWidth = cameraWidth; + this.cameraHeight = cameraHeight; + this.cameraFps = cameraFps; + this.isHighSpeed = isHighSpeed(cameraId, cameraFps); HandlerThread cameraThread = new HandlerThread("camera"); cameraThread.start(); cameraHandler = new Handler(cameraThread.getLooper()); + cameraExecutor = new HandlerExecutor(cameraHandler); + } + + private static String findCameraId(String id, CameraPosition position) throws ConfigurationException { + if (id != null) { + if (position.matches(id)) { + return id; + } + + Ln.e(String.format("--camera=%s doesn't match --camera-postion=%s", id, + position.getName())); + throw new ConfigurationException("--camera doesn't match --camera-position"); + } else { + try { + String[] cameraIds = Workarounds.getCameraManager().getCameraIdList(); + for (String cameraId : cameraIds) { + if (position.matches(cameraId)) { + return cameraId; + } + } + } catch (CameraAccessException e) { + throw new ConfigurationException("Failed to query camera ID list", e); + } + + Ln.e("--camera-postion doesn't match any camera"); + throw new ConfigurationException("--camera-position doesn't match any camera"); + } + } + + private static boolean isHighSpeed(String id, int fps) throws ConfigurationException { + if (fps == 0) { + return false; + } + + try { + CameraManager cameraManager = Workarounds.getCameraManager(); + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); + + StreamConfigurationMap streamConfig = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + assert streamConfig != null; + Range[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + assert lowFpsRanges != null; + + for (Range range : lowFpsRanges) { + if (range.getUpper() == fps) { + return false; + } + } + + android.util.Size[] highFpsSizes = streamConfig.getHighSpeedVideoSizes(); + for (android.util.Size size : highFpsSizes) { + Range[] highFpsRanges = streamConfig.getHighSpeedVideoFpsRangesFor(size); + + for (Range range : highFpsRanges) { + if (range.getUpper() == fps) { + return true; + } + } + } + + throw new ConfigurationException("Camera does not support " + fps + " FPS"); + } catch (CameraAccessException e) { + Ln.w("Failed to query supported FPS ranges", e); + return false; + } } @SuppressLint("MissingPermission") @@ -95,13 +174,18 @@ public void onError(CameraDevice camera, int error) { } } - @TargetApi(Build.VERSION_CODES.N) + @TargetApi(Build.VERSION_CODES.P) private CameraCaptureSession createCaptureSession(CameraDevice camera, Surface surface) throws CameraAccessException, InterruptedException { Ln.v("Create Capture Session"); + int sessionType = isHighSpeed ? SessionConfiguration.SESSION_HIGH_SPEED + : SessionConfiguration.SESSION_REGULAR; + List outputs = Collections.singletonList(new OutputConfiguration(surface)); CompletableFuture future = new CompletableFuture<>(); - camera.createCaptureSession(Collections.singletonList(surface), new CameraCaptureSession.StateCallback() { + + SessionConfiguration config = new SessionConfiguration( + sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { Ln.v("Create Capture Session Success"); @@ -112,7 +196,9 @@ public void onConfigured(CameraCaptureSession session) { public void onConfigureFailed(CameraCaptureSession session) { future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR)); } - }, cameraHandler); + }); + + camera.createCaptureSession(config); try { return future.get(); @@ -127,7 +213,7 @@ private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest re Ln.v("Set Repeating Request"); CompletableFuture future = new CompletableFuture<>(); - session.setRepeatingRequest(request, new CameraCaptureSession.CaptureCallback() { + CameraCaptureSession.CaptureCallback callback = new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber) { @@ -139,7 +225,16 @@ public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request CaptureFailure failure) { future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR)); } - }, cameraHandler); + }; + + if (isHighSpeed) { + CameraConstrainedHighSpeedCaptureSession highSpeedSession = + (CameraConstrainedHighSpeedCaptureSession) session; + List requests = highSpeedSession.createHighSpeedRequestList(request); + highSpeedSession.setRepeatingBurst(requests, callback, cameraHandler); + } else { + session.setRepeatingRequest(request, callback, cameraHandler); + } try { future.get(); @@ -156,33 +251,26 @@ protected void initialize() throws ConfigurationException { } } + @TargetApi(Build.VERSION_CODES.N) @Override protected Size getSize() throws ConfigurationException { try { - if (cameraId != null) { - if (!cameraPosition.matches(cameraId)) { - Ln.e(String.format("--camera=%s doesn't match --camera-postion=%s", cameraId, - cameraPosition.getName())); - throw new ConfigurationException("--camera doesn't match --camera-position"); - } - actualCameraId = cameraId; - } else { - actualCameraId = null; - String[] cameraIds = Workarounds.getCameraManager().getCameraIdList(); - for (String id : cameraIds) { - if (cameraPosition.matches(id)) { - actualCameraId = id; - break; - } - } - if (actualCameraId == null) { - Ln.e("--camera-postion doesn't match any camera"); - throw new ConfigurationException("--camera-position doesn't match any camera"); + if (maxSize > 0) { + if (cameraWidth > maxSize) { + throw new ConfigurationException("--camera-width exceeds --max-size"); + } else if (cameraHeight > maxSize) { + throw new ConfigurationException("--camera-height exceeds --max-size"); } } + if (cameraWidth > 0 && cameraHeight > 0) { + return new Size(cameraWidth, cameraHeight); + } + + // Otherwise, find a size smaller than the maximum that has the same aspect ratio as the + // sensor's native aspect ratio. CameraCharacteristics characteristics = Workarounds.getCameraManager() - .getCameraCharacteristics(actualCameraId); + .getCameraCharacteristics(cameraId); Rect sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); float aspectRatio = (float) sensorSize.width() / sensorSize.height(); @@ -224,11 +312,17 @@ protected void setSize(int size) { private void setSurfaceInternal(Surface surface) throws CameraAccessException { try { - cameraDevice = openCamera(actualCameraId); + cameraDevice = openCamera(cameraId); CameraCaptureSession session = createCaptureSession(cameraDevice, surface); CaptureRequest.Builder requestBuilder = cameraDevice .createCaptureRequest(CameraDevice.TEMPLATE_RECORD); requestBuilder.addTarget(surface); + + if (cameraFps > 0) { + requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, + new Range<>(cameraFps, cameraFps)); + } + CaptureRequest request = requestBuilder.build(); setRepeatingRequest(session, request); } catch (CameraAccessException e) { @@ -253,4 +347,17 @@ protected void dispose() { cameraDevice.close(); } } + + private static class HandlerExecutor implements Executor { + private final Handler handler; + + public HandlerExecutor(Handler handler) { + this.handler = handler; + } + + @Override + public void execute(Runnable command) { + handler.post(command); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java b/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java index 76c8f52edc..6efa7411d3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java +++ b/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java @@ -4,4 +4,8 @@ public class ConfigurationException extends Exception { public ConfigurationException(String message) { super(message); } + + public ConfigurationException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 3970fd1bec..a933d6402c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -26,6 +26,9 @@ public class Options { private int displayId; private String cameraId; private CameraPosition cameraPosition = CameraPosition.ALL; + private int cameraWidth; + private int cameraHeight; + private int cameraFps; private boolean showTouches; private boolean stayAwake; private List videoCodecOptions; @@ -125,6 +128,18 @@ public CameraPosition getCameraPosition() { return cameraPosition; } + public int getCameraWidth() { + return cameraWidth; + } + + public int getCameraHeight() { + return cameraHeight; + } + + public int getCameraFps() { + return cameraFps; + } + public boolean getShowTouches() { return showTouches; } @@ -301,6 +316,15 @@ public static Options parse(String... args) { } options.cameraPosition = cameraPosition; break; + case "camera_width": + options.cameraWidth = Integer.parseInt(value); + break; + case "camera_height": + options.cameraHeight = Integer.parseInt(value); + break; + case "camera_fps": + options.cameraFps = Integer.parseInt(value); + break; case "show_touches": options.showTouches = 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 ac84910bd5..ed2d36bd9f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -139,7 +139,8 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc asyncProcessors.add(screenEncoder); } else { CameraEncoder cameraEncoder = new CameraEncoder(options.getMaxSize(), options.getCameraId(), - options.getCameraPosition(), videoStreamer, options.getVideoBitRate(), options.getMaxFps(), + options.getCameraPosition(), options.getCameraWidth(), options.getCameraHeight(), + options.getCameraFps(), videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); asyncProcessors.add(cameraEncoder); }