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