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 <https://github.com/Genymobile/scrcpy/pull/4213>
Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<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 {
|
||||
@@ -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")
|
||||
|
||||
@@ -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<CodecOption> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user