diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 179987b3..956926ac 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -19,6 +19,7 @@ _scrcpy() { --camera-high-speed --camera-size= --camera-torch + --camera-zoom= --capture-orientation= --crop= -d --select-usb @@ -199,6 +200,7 @@ _scrcpy() { |--camera-fps \ |--camera-size \ |--camera-torch \ + |--camera-zoom \ |--crop \ |--display-id \ |--max-fps \ diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 03ed4c75..15feb054 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -26,6 +26,7 @@ arguments=( '--camera-fps=[Specify the camera capture frame rate]' '--camera-size=[Specify an explicit camera capture size]' '--camera-torch[Turn on the camera torch when the camera starts]' + '--camera-zoom[Specify the camera zoom initial value]' '--capture-orientation=[Set the capture video orientation]:orientation:(0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270)' '--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 a2045132..dfa1164f 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -135,6 +135,10 @@ Specify an explicit camera capture size. .BI \-\-camera\-torch Turn on the camera torch when the camera starts. +.TP +.BI "\-\-camera-zoom " zoom +Specify the camera zoom initial value. + .TP .BI "\-\-capture\-orientation " value Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'. diff --git a/app/src/cli.c b/app/src/cli.c index 1549bd3e..efddf0bc 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -115,6 +115,7 @@ enum { OPT_NO_VD_DESTROY_CONTENT, OPT_DISPLAY_IME_POLICY, OPT_CAMERA_TORCH, + OPT_CAMERA_ZOOM, }; struct sc_option { @@ -319,6 +320,12 @@ static const struct sc_option options[] = { .longopt = "camera-torch", .text = "Turn on the camera torch when the camera starts.", }, + { + .longopt_id = OPT_CAMERA_ZOOM, + .longopt = "camera-zoom", + .argdesc = "zoom", + .text = "Specify the camera zoom initial value.", + }, { .longopt_id = OPT_CAPTURE_ORIENTATION, .longopt = "capture-orientation", @@ -2797,6 +2804,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_CAMERA_TORCH: opts->camera_torch = true; break; + case OPT_CAMERA_ZOOM: + opts->camera_zoom = optarg; + break; case OPT_NO_WINDOW: opts->window = false; break; diff --git a/app/src/options.c b/app/src/options.c index 2d187564..2a791841 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -16,6 +16,7 @@ const struct scrcpy_options scrcpy_options_default = { .camera_id = NULL, .camera_size = NULL, .camera_ar = NULL, + .camera_zoom = NULL, .camera_fps = 0, .log_level = SC_LOG_LEVEL_INFO, .video_codec = SC_CODEC_H264, diff --git a/app/src/options.h b/app/src/options.h index 1770ee5e..bf876040 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -241,6 +241,7 @@ struct scrcpy_options { const char *camera_id; const char *camera_size; const char *camera_ar; + const char *camera_zoom; uint16_t camera_fps; enum sc_log_level log_level; enum sc_codec video_codec; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index db2b6732..f777ef9e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -470,6 +470,7 @@ scrcpy(struct scrcpy_options *options) { .kill_adb_on_close = options->kill_adb_on_close, .camera_high_speed = options->camera_high_speed, .camera_torch = options->camera_torch, + .camera_zoom = options->camera_zoom, .vd_destroy_content = options->vd_destroy_content, .vd_system_decorations = options->vd_system_decorations, .list = options->list, diff --git a/app/src/server.c b/app/src/server.c index 954c59e2..c4ee8e3d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -360,6 +360,10 @@ execute_server(struct sc_server *server, if (params->camera_torch) { ADD_PARAM("camera_torch=true"); } + if (params->camera_zoom) { + VALIDATE_STRING(params->camera_zoom); + ADD_PARAM("camera_zoom=%s", params->camera_zoom); + } if (params->show_touches) { ADD_PARAM("show_touches=true"); } diff --git a/app/src/server.h b/app/src/server.h index 2aeb135d..8aa462b8 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -35,6 +35,7 @@ struct sc_server_params { const char *camera_id; const char *camera_size; const char *camera_ar; + const char *camera_zoom; uint16_t camera_fps; struct sc_port_range port_range; uint32_t tunnel_host; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 183fd748..dd7a5c53 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -44,6 +44,7 @@ public class Options { private Size cameraSize; private CameraFacing cameraFacing; private CameraAspectRatio cameraAspectRatio; + private float cameraZoom = 1; private int cameraFps; private boolean cameraHighSpeed; private boolean cameraTorch; @@ -169,6 +170,10 @@ public class Options { return cameraAspectRatio; } + public float getCameraZoom() { + return cameraZoom; + } + public int getCameraFps() { return cameraFps; } @@ -473,6 +478,11 @@ public class Options { options.cameraAspectRatio = parseCameraAspectRatio(value); } break; + case "camera_zoom": + if (!value.isEmpty()) { + options.cameraZoom = Float.parseFloat(value); + } + break; case "camera_fps": options.cameraFps = Integer.parseInt(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 61e708bc..05884baa 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -23,6 +23,7 @@ import android.media.MediaCodecList; import android.os.Build; import android.util.Range; +import java.text.DecimalFormat; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -172,6 +173,18 @@ public final class LogUtils { Ln.w("Could not get available frame rates for camera " + id, e); } + if (Build.VERSION.SDK_INT >= AndroidVersions.API_30_ANDROID_11) { + try { + Range zoomRange = characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE); + if (zoomRange != null) { + String zoom = getFormattedZoomRange(zoomRange); + builder.append(", zoom-range=").append(zoom); + } + } catch (Exception e) { + Ln.w("Could not get available zoom ranges for camera " + id, e); + } + } + builder.append(')'); if (includeSizes) { @@ -226,6 +239,11 @@ public final class LogUtils { return builder.toString(); } + private static String getFormattedZoomRange(Range range) { + DecimalFormat format = new DecimalFormat("#.##"); + return "[" + format.format(range.getLower()) + ", " + format.format(range.getUpper()) + "]"; + } + public static String buildAppListMessage() { List apps = Device.listApps(); return buildAppListMessage("List of apps:", apps); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 143da87f..2f872981 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -65,10 +65,12 @@ public class CameraCapture extends SurfaceCapture { private final Orientation captureOrientation; private final float angle; private final boolean initialTorch; + private float zoom; private String cameraId; private Size captureSize; private Size videoSize; // after OpenGL transforms + private Range zoomRange; private AffineMatrix transform; private OpenGLRunner glRunner; @@ -98,6 +100,7 @@ public class CameraCapture extends SurfaceCapture { assert captureOrientation != null; this.angle = options.getAngle(); this.initialTorch = options.getCameraTorch(); + this.zoom = options.getCameraZoom(); } @Override @@ -288,6 +291,14 @@ public class CameraCapture extends SurfaceCapture { return; } + CameraManager cameraManager = ServiceManager.getCameraManager(); + try { + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); + zoomRange = characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE); + } catch (CameraAccessException e) { + Ln.w("Could not get camera characteristics"); + } + try { requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); requestBuilder.addTarget(captureSurface); @@ -299,6 +310,11 @@ public class CameraCapture extends SurfaceCapture { Ln.i("Turn camera torch on"); requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); } + if (zoom != 1) { + zoom = clampZoom(zoom); + Ln.i("Set camera zoom: " + zoom); + requestBuilder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoom); + } CaptureRequest request = requestBuilder.build(); setRepeatingRequest(session, request); @@ -456,6 +472,15 @@ public class CameraCapture extends SurfaceCapture { }); } + private float clampZoom(float value) { + assertCameraThread(); + if (zoomRange == null) { + return value; + } + + return zoomRange.clamp(value); + } + private void assertCameraThread() { assert Thread.currentThread() == cameraThread; }