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 339a819a84..c8b6609ea1 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 c92f0ac1fa..823e6b9ec5 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 d2fb3ad578..c473adb5cf 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 96b8c26dba..69a918c3d5 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 2adb432370..589a5a2259 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 1e783bae0b..40f04670f1 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 54e794e05e..1f4c2a7cca 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 7f8a492682..0c40bccb63 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 786aea5c30..71d22fe846 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 0000000000..4fdf4c740c --- /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 949eb34394..e4aba8723e 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 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.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 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 void init() throws IOException { 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 @@ private static String selectCamera(String explicitCameraId, CameraFacing cameraF 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 void release() { @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 11af3cca7e..eec19e5249 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 CameraFacing getCameraFacing() { return cameraFacing; } + public CameraAspectRatio getCameraAspectRatio() { + return cameraAspectRatio; + } + public boolean getShowTouches() { return showTouches; } @@ -374,6 +379,11 @@ public static Options parse(String... args) { 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 @@ private static Size parseSize(String size) { 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 1a93323a8f..4505a52331 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 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc 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());