Skip to content

Commit

Permalink
Support camera size selection using -m/--camera-ar
Browse files Browse the repository at this point in the history
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 <#4213>

Co-authored-by: Simon Chan <[email protected]>
  • Loading branch information
rom1v and yume-chan committed Oct 31, 2023
1 parent faebb7d commit dd36d61
Show file tree
Hide file tree
Showing 13 changed files with 215 additions and 8 deletions.
2 changes: 2 additions & 0 deletions app/data/bash-completion/scrcpy
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ _scrcpy() {
--audio-source=
--audio-output-buffer=
-b --video-bit-rate=
--camera-ar=
--camera-id=
--camera-facing=
--camera-size=
Expand Down Expand Up @@ -153,6 +154,7 @@ _scrcpy() {
|--audio-codec-options \
|--audio-encoder \
|--audio-output-buffer \
|--camera-ar \
|--camera-id \
|--camera-size \
|--crop \
Expand Down
1 change: 1 addition & 0 deletions app/data/zsh-completion/_scrcpy
Original file line number Diff line number Diff line change
Expand Up @@ -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]'
Expand Down
6 changes: 6 additions & 0 deletions app/scrcpy.1
Original file line number Diff line number Diff line change
Expand Up @@ -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), "<num>:<den>" (e.g. "4:3") and "<value>" (e.g. "1.6").

.TP
.BI "\-\-camera\-id " id
Specify the device camera id to mirror.
Expand Down
27 changes: 24 additions & 3 deletions app/src/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ enum {
OPT_CAMERA_ID,
OPT_CAMERA_SIZE,
OPT_CAMERA_FACING,
OPT_CAMERA_AR,
};

struct sc_option {
Expand Down Expand Up @@ -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), \"<num>:<den>\" (e.g. \"4:3\") or \"<value>\" (e.g. "
"\"1.6\")."
},
{
.longopt_id = OPT_CAMERA_ID,
.longopt = "camera-id",
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -2245,16 +2258,24 @@ 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) {
LOGI("Camera video source: control disabled");
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");
Expand Down
1 change: 1 addition & 0 deletions app/src/options.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions app/src/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions app/src/scrcpy.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions app/src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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");
}
Expand Down
1 change: 1 addition & 0 deletions app/src/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
37 changes: 37 additions & 0 deletions server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
112 changes: 108 additions & 4 deletions server/src/main/java/com/genymobile/scrcpy/CameraCapture.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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<android.util.Size> 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<android.util.Size> 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 {
Expand All @@ -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")
Expand Down
Loading

0 comments on commit dd36d61

Please sign in to comment.