From dd36d6135fb08f8d3a99f9e9eb4ec8e19af0a660 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 26 Oct 2023 23:54:34 +0200 Subject: [PATCH] Support camera size selection using -m/--camera-ar In addition to --camera-size to specify an explicit size, make it possible to select the camera size automatically, respecting the maximum size (already used for display mirroring) and an aspect ratio. For example, "scrcpy --video-source=camera" followed by: - (no additional arguments) : mirrors at the maximum size, any a-r - -m1920 : only consider valid sizes having both dimensions not above 1920 - --camera-ar=4:3 : only consider valid sizes having an aspect ratio of 4:3 (+/- 10%) - -m2048 --camera-ar=1.6 : only consider valid sizes having both dimensions not above 2048 and an aspect ratio of 1.6 (+/- 10%) PR #4213 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- app/data/bash-completion/scrcpy | 2 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 6 + app/src/cli.c | 27 ++++- app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 5 + app/src/server.h | 1 + .../genymobile/scrcpy/CameraAspectRatio.java | 37 ++++++ .../com/genymobile/scrcpy/CameraCapture.java | 112 +++++++++++++++++- .../java/com/genymobile/scrcpy/Options.java | 26 ++++ .../java/com/genymobile/scrcpy/Server.java | 3 +- 13 files changed, 215 insertions(+), 8 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 339a819a..c8b6609e 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -10,6 +10,7 @@ _scrcpy() { --audio-source= --audio-output-buffer= -b --video-bit-rate= + --camera-ar= --camera-id= --camera-facing= --camera-size= @@ -153,6 +154,7 @@ _scrcpy() { |--audio-codec-options \ |--audio-encoder \ |--audio-output-buffer \ + |--camera-ar \ |--camera-id \ |--camera-size \ |--crop \ diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index c92f0ac1..823e6b9e 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -17,6 +17,7 @@ arguments=( '--audio-source=[Select the audio source]:source:(output mic)' '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' + '--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-size=[Specify an explicit camera capture size]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index d2fb3ad5..c473adb5 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -75,6 +75,12 @@ Encode the video at the given bit rate, expressed in bits/s. Unit suffixes are s Default is 8M (8000000). +.TP +.BI "\-\-camera\-ar " ar +Select the camera size by its aspect ratio (+/- 10%). + +Possible values are "sensor" (use the camera sensor aspect ratio), ":" (e.g. "4:3") and "" (e.g. "1.6"). + .TP .BI "\-\-camera\-id " id Specify the device camera id to mirror. diff --git a/app/src/cli.c b/app/src/cli.c index 96b8c26d..69a918c3 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -87,6 +87,7 @@ enum { OPT_CAMERA_ID, OPT_CAMERA_SIZE, OPT_CAMERA_FACING, + OPT_CAMERA_AR, }; struct sc_option { @@ -203,6 +204,15 @@ static const struct sc_option options[] = { .longopt = "bit-rate", .argdesc = "value", }, + { + .longopt_id = OPT_CAMERA_AR, + .longopt = "camera-ar", + .argdesc = "ar", + .text = "Select the camera size by its aspect ratio (+/- 10%).\n" + "Possible values are \"sensor\" (use the camera sensor aspect " + "ratio), \":\" (e.g. \"4:3\") or \"\" (e.g. " + "\"1.6\")." + }, { .longopt_id = OPT_CAMERA_ID, .longopt = "camera-id", @@ -2130,6 +2140,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_CAMERA_AR: + opts->camera_ar = optarg; + break; case OPT_CAMERA_ID: opts->camera_id = optarg; break; @@ -2245,9 +2258,16 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } - if (!opts->camera_size) { - LOGE("Camera size must be specified by --camera-size"); - return false; + if (opts->camera_size) { + if (opts->max_size) { + LOGE("Could not specify both --camera-size and -m/--max-size"); + return false; + } + + if (opts->camera_ar) { + LOGE("Could not specify both --camera-size and --camera-ar"); + return false; + } } if (opts->control) { @@ -2255,6 +2275,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->control = false; } } else if (opts->camera_id + || opts->camera_ar || opts->camera_facing != SC_CAMERA_FACING_ANY || opts->camera_size) { LOGE("Camera options are only available with --video-source=camera"); diff --git a/app/src/options.c b/app/src/options.c index 2adb4323..589a5a22 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -13,6 +13,7 @@ const struct scrcpy_options scrcpy_options_default = { .audio_encoder = NULL, .camera_id = NULL, .camera_size = NULL, + .camera_ar = NULL, .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 1e783bae..40f04670 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -132,6 +132,7 @@ struct scrcpy_options { const char *audio_encoder; const char *camera_id; const char *camera_size; + const char *camera_ar; 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 54e794e0..1f4c2a7c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -375,6 +375,7 @@ scrcpy(struct scrcpy_options *options) { .audio_encoder = options->audio_encoder, .camera_id = options->camera_id, .camera_size = options->camera_size, + .camera_ar = options->camera_ar, .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 7f8a4926..0c40bccb 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -77,6 +77,7 @@ sc_server_params_destroy(struct sc_server_params *params) { free((char *) params->audio_encoder); free((char *) params->tcpip_dst); free((char *) params->camera_id); + free((char *) params->camera_ar); } static bool @@ -105,6 +106,7 @@ sc_server_params_copy(struct sc_server_params *dst, COPY(audio_encoder); COPY(tcpip_dst); COPY(camera_id); + COPY(camera_ar); #undef COPY return true; @@ -303,6 +305,9 @@ execute_server(struct sc_server *server, ADD_PARAM("camera_facing=%s", sc_server_get_camera_facing_name(params->camera_facing)); } + if (params->camera_ar) { + ADD_PARAM("camera_ar=%s", params->camera_ar); + } if (params->show_touches) { ADD_PARAM("show_touches=true"); } diff --git a/app/src/server.h b/app/src/server.h index 786aea5c..71d22fe8 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -36,6 +36,7 @@ struct sc_server_params { const char *audio_encoder; const char *camera_id; const char *camera_size; + const char *camera_ar; struct sc_port_range port_range; uint32_t tunnel_host; uint16_t tunnel_port; diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java b/server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java new file mode 100644 index 00000000..4fdf4c74 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java @@ -0,0 +1,37 @@ +package com.genymobile.scrcpy; + +public final class CameraAspectRatio { + private static final float SENSOR = -1; + + private float ar; + + private CameraAspectRatio(float ar) { + this.ar = ar; + } + + public static CameraAspectRatio fromFloat(float ar) { + if (ar < 0) { + throw new IllegalArgumentException("Invalid aspect ratio: " + ar); + } + return new CameraAspectRatio(ar); + } + + public static CameraAspectRatio fromFraction(int w, int h) { + if (w <= 0 || h <= 0) { + throw new IllegalArgumentException("Invalid aspect ratio: " + w + ":" + h); + } + return new CameraAspectRatio((float) w / h); + } + + public static CameraAspectRatio sensorAspectRatio() { + return new CameraAspectRatio(SENSOR); + } + + public boolean isSensor() { + return ar == SENSOR; + } + + public float getAspectRatio() { + return ar; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java index 949eb343..e4aba872 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java @@ -4,6 +4,7 @@ import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; @@ -13,6 +14,8 @@ 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; @@ -21,16 +24,23 @@ import android.view.Surface; import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; public class CameraCapture extends SurfaceCapture { private final String explicitCameraId; private final CameraFacing cameraFacing; private final Size explicitSize; + private int maxSize; + private final CameraAspectRatio aspectRatio; + + private String cameraId; + private Size size; private HandlerThread cameraThread; private Handler cameraHandler; @@ -39,10 +49,12 @@ public class CameraCapture extends SurfaceCapture { private final AtomicBoolean disconnected = new AtomicBoolean(); - public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize) { + public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio) { this.explicitCameraId = explicitCameraId; this.cameraFacing = cameraFacing; this.explicitSize = explicitSize; + this.maxSize = maxSize; + this.aspectRatio = aspectRatio; } @Override @@ -53,11 +65,16 @@ public class CameraCapture extends SurfaceCapture { cameraExecutor = new HandlerExecutor(cameraHandler); try { - String cameraId = selectCamera(explicitCameraId, cameraFacing); + cameraId = selectCamera(explicitCameraId, cameraFacing); if (cameraId == null) { throw new IOException("No matching camera found"); } + size = selectSize(cameraId, explicitSize, maxSize, aspectRatio); + if (size == null) { + throw new IOException("Could not select camera size"); + } + Ln.i("Using camera '" + cameraId + "'"); cameraDevice = openCamera(cameraId); } catch (CameraAccessException | InterruptedException e) { @@ -91,6 +108,82 @@ public class CameraCapture extends SurfaceCapture { return null; } + @TargetApi(Build.VERSION_CODES.N) + private static Size selectSize(String cameraId, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio) throws CameraAccessException { + if (explicitSize != null) { + return explicitSize; + } + + CameraManager cameraManager = ServiceManager.getCameraManager(); + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); + + StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class); + Stream stream = Arrays.stream(sizes); + if (maxSize > 0) { + stream = stream.filter(it -> it.getWidth() <= maxSize && it.getHeight() <= maxSize); + } + + Float targetAspectRatio = resolveAspectRatio(aspectRatio, characteristics); + if (targetAspectRatio != null) { + stream = stream.filter(it -> { + float ar = ((float) it.getWidth() / it.getHeight()); + float arRatio = ar / targetAspectRatio; + // Accept if the aspect ratio is the target aspect ratio + or - 10% + return arRatio >= 0.9f && arRatio <= 1.1f; + }); + } + + Optional selected = stream.max((s1, s2) -> { + // Greater width is better + int cmp = Integer.compare(s1.getWidth(), s2.getWidth()); + if (cmp != 0) { + return cmp; + } + + if (targetAspectRatio != null) { + // Closer to the target aspect ratio is better + float ar1 = ((float) s1.getWidth() / s1.getHeight()); + float arRatio1 = ar1 / targetAspectRatio; + float distance1 = Math.abs(1 - arRatio1); + + float ar2 = ((float) s2.getWidth() / s2.getHeight()); + float arRatio2 = ar2 / targetAspectRatio; + float distance2 = Math.abs(1 - arRatio2); + + // Reverse the order because lower distance is better + cmp = Float.compare(distance2, distance1); + if (cmp != 0) { + return cmp; + } + } + + // Greater height is better + return Integer.compare(s1.getHeight(), s2.getHeight()); + }); + + if (selected.isPresent()) { + android.util.Size size = selected.get(); + return new Size(size.getWidth(), size.getHeight()); + } + + // Not found + return null; + } + + private static Float resolveAspectRatio(CameraAspectRatio ratio, CameraCharacteristics characteristics) { + if (ratio == null) { + return null; + } + + if (ratio.isSensor()) { + Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + return (float) activeSize.width() / activeSize.height(); + } + + return ratio.getAspectRatio(); + } + @Override public void start(Surface surface) throws IOException { try { @@ -114,12 +207,23 @@ public class CameraCapture extends SurfaceCapture { @Override public Size getSize() { - return explicitSize; + return size; } @Override public boolean setMaxSize(int maxSize) { - return false; + if (explicitSize != null) { + return false; + } + + this.maxSize = maxSize; + try { + size = selectSize(cameraId, null, maxSize, aspectRatio); + return size != null; + } catch (CameraAccessException e) { + Ln.w("Could not select camera size", e); + return false; + } } @SuppressLint("MissingPermission") diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 11af3cca..eec19e52 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -27,6 +27,7 @@ public class Options { private String cameraId; private Size cameraSize; private CameraFacing cameraFacing; + private CameraAspectRatio cameraAspectRatio; private boolean showTouches; private boolean stayAwake; private List videoCodecOptions; @@ -131,6 +132,10 @@ public class Options { return cameraFacing; } + public CameraAspectRatio getCameraAspectRatio() { + return cameraAspectRatio; + } + public boolean getShowTouches() { return showTouches; } @@ -374,6 +379,11 @@ public class Options { options.cameraFacing = facing; } break; + case "camera_ar": + if (!value.isEmpty()) { + options.cameraAspectRatio = parseCameraAspectRatio(value); + } + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; @@ -427,4 +437,20 @@ public class Options { int height = Integer.parseInt(tokens[1]); return new Size(width, height); } + + private static CameraAspectRatio parseCameraAspectRatio(String ar) { + if ("sensor".equals(ar)) { + return CameraAspectRatio.sensorAspectRatio(); + } + + String[] tokens = ar.split(":"); + if (tokens.length == 2) { + int w = Integer.parseInt(tokens[0]); + int h = Integer.parseInt(tokens[1]); + return CameraAspectRatio.fromFraction(w, h); + } + + float floatAr = Float.parseFloat(tokens[0]); + return CameraAspectRatio.fromFloat(floatAr); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 1a93323a..4505a523 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -137,7 +137,8 @@ public final class Server { if (options.getVideoSource() == VideoSource.DISPLAY) { surfaceCapture = new ScreenCapture(device); } else { - surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize()); + surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(), + options.getMaxSize(), options.getCameraAspectRatio()); } SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());